Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy

This commit is contained in:
kbx81
2026-02-19 18:31:15 -06:00
150 changed files with 2486 additions and 1640 deletions

View File

@@ -0,0 +1,5 @@
<<: !include common.yaml
esp32_ble:
io_capability: keyboard_only
disable_bt_logs: false

View File

@@ -33,6 +33,10 @@ esp32_ble_server:
- uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc42d
advertise: false
characteristics:
- id: test_lambda_characteristic
uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc12c
read: true
value: !lambda return { 1, 2 };
- id: test_change_characteristic
uuid: 2a24b789-7a1b-4535-af3e-ee76a35cc11c
read: true

View File

@@ -4,15 +4,16 @@ interval:
- interval: 60s
then:
- lambda: |-
// Test build_json
std::string json_str = esphome::json::build_json([](JsonObject root) {
// Test build_json - returns SerializationBuffer, use auto to avoid heap allocation
auto json_buf = esphome::json::build_json([](JsonObject root) {
root["sensor"] = "temperature";
root["value"] = 23.5;
root["unit"] = "°C";
});
ESP_LOGD("test", "Built JSON: %s", json_str.c_str());
ESP_LOGD("test", "Built JSON: %s", json_buf.c_str());
// Test parse_json
// Test parse_json - implicit conversion to std::string for backward compatibility
std::string json_str = json_buf;
bool parse_ok = esphome::json::parse_json(json_str, [](JsonObject root) {
if (root["sensor"].is<const char*>() && root["value"].is<float>()) {
const char* sensor = root["sensor"];
@@ -26,10 +27,10 @@ interval:
});
ESP_LOGD("test", "Parse result (JSON syntax only): %s", parse_ok ? "success" : "failed");
// Test JsonBuilder class
// Test JsonBuilder class - returns SerializationBuffer
esphome::json::JsonBuilder builder;
JsonObject obj = builder.root();
obj["test"] = "direct_builder";
obj["count"] = 42;
std::string result = builder.serialize();
auto result = builder.serialize();
ESP_LOGD("test", "JsonBuilder result: %s", result.c_str());

View File

@@ -23,8 +23,27 @@ media_player:
- media_player.stop:
- media_player.stop:
announcement: true
on_announcement:
- media_player.play:
on_turn_on:
- media_player.play:
on_turn_off:
- media_player.stop:
on_pause:
- media_player.toggle:
- media_player.turn_on:
- media_player.turn_off:
- media_player.next:
- media_player.previous:
- media_player.mute:
- media_player.unmute:
- media_player.repeat_off:
- media_player.repeat_one:
- media_player.repeat_all:
- media_player.shuffle:
- media_player.unshuffle:
- media_player.group_join:
- media_player.clear_playlist:
- wait_until:
media_player.is_idle:
- wait_until:
@@ -33,6 +52,12 @@ media_player:
media_player.is_announcing:
- wait_until:
media_player.is_paused:
- wait_until:
media_player.is_on:
- wait_until:
media_player.is_off:
- wait_until:
media_player.is_muted:
- media_player.volume_up:
- media_player.volume_down:
- media_player.volume_set: 50%

View File

@@ -0,0 +1,10 @@
sensor:
- platform: pulse_counter
name: Pulse Counter
pin: 4
use_pcnt: false
count_mode:
rising_edge: INCREMENT
falling_edge: DECREMENT
internal_filter: 13us
update_interval: 15s

View File

@@ -0,0 +1,11 @@
substitutions:
network_enable_ipv6: "false"
socket:
wifi:
ssid: MySSID
password: password1
network:
enable_ipv6: ${network_enable_ipv6}

View File

@@ -0,0 +1,4 @@
substitutions:
network_enable_ipv6: "true"
<<: !include common.yaml

View File

@@ -0,0 +1,4 @@
socket:
network:
enable_ipv6: true

View File

@@ -0,0 +1 @@
<<: !include common.yaml

View File

@@ -0,0 +1,3 @@
socket:
network:

View File

@@ -1,10 +0,0 @@
substitutions:
i2s_bclk_pin: GPIO27
i2s_lrclk_pin: GPIO26
i2s_mclk_pin: GPIO25
i2s_dout_pin: GPIO23
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml
<<: !include common-audio_dac.yaml

View File

@@ -1,5 +1,11 @@
<<: !include common.yaml
wifi:
ap:
psram:
mode: quad
media_player:
- platform: speaker
id: speaker_media_player_id
@@ -10,3 +16,4 @@ media_player:
volume_max: 0.95
volume_min: 0.0
task_stack_in_psram: true
codec_support_enabled: all

View File

@@ -1,9 +0,0 @@
substitutions:
scl_pin: GPIO2
sda_pin: GPIO3
i2s_bclk_pin: GPIO4
i2s_lrclk_pin: GPIO5
i2s_mclk_pin: GPIO6
i2s_dout_pin: GPIO7
<<: !include common-media_player.yaml

View File

@@ -1,10 +0,0 @@
substitutions:
i2s_bclk_pin: GPIO27
i2s_lrclk_pin: GPIO26
i2s_mclk_pin: GPIO25
i2s_dout_pin: GPIO4
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-ard.yaml
<<: !include common.yaml

View File

@@ -30,6 +30,7 @@ esp32_camera:
resolution: 640x480
jpeg_quality: 10
frame_buffer_location: PSRAM
pixel_format: JPEG
on_image:
then:
- lambda: |-

View File

@@ -656,7 +656,7 @@ def test_clone_or_update_recover_broken_flag_prevents_infinite_loop(
# Should raise on the second attempt when _recover_broken=False
# This hits the "if not _recover_broken: raise" path
with (
unittest.mock.patch("esphome.git.shutil.rmtree", side_effect=mock_rmtree),
unittest.mock.patch("esphome.git.rmtree", side_effect=mock_rmtree),
pytest.raises(GitCommandError, match="fatal: unable to write new index file"),
):
git.clone_or_update(
@@ -671,3 +671,114 @@ def test_clone_or_update_recover_broken_flag_prevents_infinite_loop(
stash_calls = [c for c in call_list if "stash" in c[0][0]]
# Should have exactly two stash calls
assert len(stash_calls) == 2
def test_clone_or_update_cleans_up_on_failed_ref_fetch(
tmp_path: Path, mock_run_git_command: Mock
) -> None:
"""Test that a failed ref fetch removes the incomplete clone directory.
When cloning with a specific ref, if `git clone` succeeds but the
subsequent `git fetch <ref>` fails, the clone directory should be
removed so the next attempt starts fresh instead of finding a stale
clone on the default branch.
"""
CORE.config_path = tmp_path / "test.yaml"
url = "https://github.com/test/repo"
ref = "pull/123/head"
domain = "test"
repo_dir = _compute_repo_dir(url, ref, domain)
def git_command_side_effect(
cmd: list[str], cwd: str | None = None, **kwargs: Any
) -> str:
cmd_type = _get_git_command_type(cmd)
if cmd_type == "clone":
# Simulate successful clone by creating the directory
repo_dir.mkdir(parents=True, exist_ok=True)
(repo_dir / ".git").mkdir(exist_ok=True)
return ""
if cmd_type == "fetch":
raise GitCommandError("fatal: couldn't find remote ref pull/123/head")
return ""
mock_run_git_command.side_effect = git_command_side_effect
refresh = TimePeriodSeconds(days=1)
with pytest.raises(GitCommandError, match="couldn't find remote ref"):
git.clone_or_update(
url=url,
ref=ref,
refresh=refresh,
domain=domain,
)
# The incomplete clone directory should have been removed
assert not repo_dir.exists()
# Verify clone was attempted then fetch failed
call_list = mock_run_git_command.call_args_list
clone_calls = [c for c in call_list if "clone" in c[0][0]]
assert len(clone_calls) == 1
fetch_calls = [c for c in call_list if "fetch" in c[0][0]]
assert len(fetch_calls) == 1
def test_clone_or_update_stale_clone_is_retried_after_cleanup(
tmp_path: Path, mock_run_git_command: Mock
) -> None:
"""Test that after cleanup, a subsequent call does a fresh clone.
This is the full scenario: first call fails at fetch (directory cleaned up),
second call sees no directory and clones fresh.
"""
CORE.config_path = tmp_path / "test.yaml"
url = "https://github.com/test/repo"
ref = "pull/123/head"
domain = "test"
repo_dir = _compute_repo_dir(url, ref, domain)
call_count = {"clone": 0, "fetch": 0}
def git_command_side_effect(
cmd: list[str], cwd: str | None = None, **kwargs: Any
) -> str:
cmd_type = _get_git_command_type(cmd)
if cmd_type == "clone":
call_count["clone"] += 1
repo_dir.mkdir(parents=True, exist_ok=True)
(repo_dir / ".git").mkdir(exist_ok=True)
return ""
if cmd_type == "fetch":
call_count["fetch"] += 1
if call_count["fetch"] == 1:
# First fetch fails
raise GitCommandError("fatal: couldn't find remote ref pull/123/head")
# Second fetch succeeds
return ""
if cmd_type == "reset":
return ""
return ""
mock_run_git_command.side_effect = git_command_side_effect
refresh = TimePeriodSeconds(days=1)
# First call: clone succeeds, fetch fails, directory cleaned up
with pytest.raises(GitCommandError, match="couldn't find remote ref"):
git.clone_or_update(url=url, ref=ref, refresh=refresh, domain=domain)
assert not repo_dir.exists()
# Second call: fresh clone + fetch succeeds
result_dir, _ = git.clone_or_update(
url=url, ref=ref, refresh=refresh, domain=domain
)
assert result_dir == repo_dir
assert repo_dir.exists()
assert call_count["clone"] == 2
assert call_count["fetch"] == 2