1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-17 17:23:45 +01:00

Merge remote-tracking branch 'upstream/dev' into integration

This commit is contained in:
J. Nick Koston
2025-10-15 15:20:28 -10:00
47 changed files with 865 additions and 214 deletions

View File

@@ -69,7 +69,7 @@ def run_schema_validation(config: ConfigType) -> None:
{
"id": "display_id",
"model": "custom",
"dimensions": {"width": 320, "height": 240},
"dimensions": {"width": 260, "height": 260},
"draw_rounding": 13,
"init_sequence": [[0xA0, 0x01]],
},
@@ -336,7 +336,7 @@ def test_native_generation(
main_cpp = generate_main(component_fixture_path("native.yaml"))
assert (
"mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, display::DISPLAY_ROTATION_0_DEGREES, 1>()"
"mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, display::DISPLAY_ROTATION_0_DEGREES, 1, 1>()"
in main_cpp
)
assert "set_init_sequence({240, 1, 8, 242" in main_cpp

View File

@@ -0,0 +1,19 @@
<<: !include common.yaml
esp32_ble_tracker:
max_connections: 9
bluetooth_proxy:
active: true
connection_slots: 9
esp32_hosted:
active_high: true
variant: ESP32C6
reset_pin: GPIO54
cmd_pin: GPIO19
clk_pin: GPIO18
d0_pin: GPIO14
d1_pin: GPIO15
d2_pin: GPIO16
d3_pin: GPIO17

View File

@@ -7,8 +7,8 @@ display:
id: ili9xxx_display
model: GC9A01A
invert_colors: True
cs_pin: 10
dc_pin: 6
cs_pin: 11
dc_pin: 7
pages:
- id: page1
lambda: |-

View File

@@ -5,6 +5,7 @@ esp32:
advanced:
enable_lwip_mdns_queries: true
enable_lwip_bridge_interface: true
disable_libc_locks_in_iram: false # Test explicit opt-out of RAM optimization
wifi:
ssid: MySSID

View File

@@ -4,6 +4,7 @@ esp32:
type: esp-idf
advanced:
execute_from_psram: true
disable_libc_locks_in_iram: true # Test default RAM optimization enabled
psram:
mode: octal

View File

@@ -10,7 +10,7 @@ display:
invert_colors: true
show_test_card: true
spi_mode: mode0
draw_rounding: 8
draw_rounding: 4
use_axis_flips: true
init_sequence:
- [0xd0, 1, 2, 3]

View File

@@ -1,7 +1,7 @@
substitutions:
dc_pin: GPIO14
cs_pin: GPIO13
enable_pin: GPIO16
enable_pin: GPIO17
reset_pin: GPIO20
packages:

View File

@@ -73,9 +73,11 @@ def test_main_all_tests_should_run(
mock_should_run_clang_format.return_value = True
mock_should_run_python_linters.return_value = True
# Mock list-components.py output
# Mock list-components.py output (now returns JSON with --changed-with-deps)
mock_result = Mock()
mock_result.stdout = "wifi\napi\nsensor\n"
mock_result.stdout = json.dumps(
{"directly_changed": ["wifi", "api"], "all_changed": ["wifi", "api", "sensor"]}
)
mock_subprocess_run.return_value = mock_result
# Run main function with mocked argv
@@ -116,7 +118,7 @@ def test_main_no_tests_should_run(
# Mock empty list-components.py output
mock_result = Mock()
mock_result.stdout = ""
mock_result.stdout = json.dumps({"directly_changed": [], "all_changed": []})
mock_subprocess_run.return_value = mock_result
# Run main function with mocked argv
@@ -177,7 +179,9 @@ def test_main_with_branch_argument(
# Mock list-components.py output
mock_result = Mock()
mock_result.stdout = "mqtt\n"
mock_result.stdout = json.dumps(
{"directly_changed": ["mqtt"], "all_changed": ["mqtt"]}
)
mock_subprocess_run.return_value = mock_result
with patch("sys.argv", ["script.py", "-b", "main"]):
@@ -192,7 +196,7 @@ def test_main_with_branch_argument(
# Check that list-components.py was called with branch
mock_subprocess_run.assert_called_once()
call_args = mock_subprocess_run.call_args[0][0]
assert "--changed" in call_args
assert "--changed-with-deps" in call_args
assert "-b" in call_args
assert "main" in call_args
@@ -411,7 +415,12 @@ def test_main_filters_components_without_tests(
# Mock list-components.py output with 3 components
# wifi: has tests, sensor: has tests, airthings_ble: no tests
mock_result = Mock()
mock_result.stdout = "wifi\nsensor\nairthings_ble\n"
mock_result.stdout = json.dumps(
{
"directly_changed": ["wifi", "sensor"],
"all_changed": ["wifi", "sensor", "airthings_ble"],
}
)
mock_subprocess_run.return_value = mock_result
# Create test directory structure
@@ -436,6 +445,8 @@ def test_main_filters_components_without_tests(
patch.object(determine_jobs, "root_path", str(tmp_path)),
patch("sys.argv", ["determine-jobs.py"]),
):
# Clear the cache since we're mocking root_path
determine_jobs._component_has_tests.cache_clear()
determine_jobs.main()
# Check output

View File

@@ -287,7 +287,7 @@ def test_perform_ota_no_auth(mock_socket: Mock, mock_file: io.BytesIO) -> None:
mock_socket.recv.side_effect = recv_responses
espota2.perform_ota(mock_socket, "", mock_file, "test.bin")
espota2.perform_ota(mock_socket, None, mock_file, "test.bin")
# Should not send any auth-related data
auth_calls = [
@@ -317,7 +317,7 @@ def test_perform_ota_with_compression(mock_socket: Mock) -> None:
mock_socket.recv.side_effect = recv_responses
espota2.perform_ota(mock_socket, "", mock_file, "test.bin")
espota2.perform_ota(mock_socket, None, mock_file, "test.bin")
# Verify compressed content was sent
# Get the binary size that was sent (4 bytes after features)
@@ -347,7 +347,7 @@ def test_perform_ota_auth_without_password(mock_socket: Mock) -> None:
with pytest.raises(
espota2.OTAError, match="ESP requests password, but no password given"
):
espota2.perform_ota(mock_socket, "", mock_file, "test.bin")
espota2.perform_ota(mock_socket, None, mock_file, "test.bin")
@pytest.mark.usefixtures("mock_time")
@@ -413,7 +413,7 @@ def test_perform_ota_sha256_auth_without_password(mock_socket: Mock) -> None:
with pytest.raises(
espota2.OTAError, match="ESP requests password, but no password given"
):
espota2.perform_ota(mock_socket, "", mock_file, "test.bin")
espota2.perform_ota(mock_socket, None, mock_file, "test.bin")
def test_perform_ota_unexpected_auth_response(mock_socket: Mock) -> None:
@@ -450,7 +450,7 @@ def test_perform_ota_unsupported_version(mock_socket: Mock) -> None:
mock_socket.recv.side_effect = responses
with pytest.raises(espota2.OTAError, match="Device uses unsupported OTA version"):
espota2.perform_ota(mock_socket, "", mock_file, "test.bin")
espota2.perform_ota(mock_socket, None, mock_file, "test.bin")
@pytest.mark.usefixtures("mock_time")
@@ -471,7 +471,7 @@ def test_perform_ota_upload_error(mock_socket: Mock, mock_file: io.BytesIO) -> N
mock_socket.recv.side_effect = recv_responses
with pytest.raises(espota2.OTAError, match="Error receiving acknowledge chunk OK"):
espota2.perform_ota(mock_socket, "", mock_file, "test.bin")
espota2.perform_ota(mock_socket, None, mock_file, "test.bin")
@pytest.mark.usefixtures("mock_socket_constructor", "mock_resolve_ip")
@@ -706,7 +706,7 @@ def test_perform_ota_version_differences(
]
mock_socket.recv.side_effect = recv_responses
espota2.perform_ota(mock_socket, "", mock_file, "test.bin")
espota2.perform_ota(mock_socket, None, mock_file, "test.bin")
# For v1.0, verify that we only get the expected number of recv calls
# v1.0 doesn't have chunk acknowledgments, so fewer recv calls
@@ -732,7 +732,7 @@ def test_perform_ota_version_differences(
]
mock_socket.recv.side_effect = recv_responses_v2
espota2.perform_ota(mock_socket, "", mock_file, "test.bin")
espota2.perform_ota(mock_socket, None, mock_file, "test.bin")
# For v2.0, verify more recv calls due to chunk acknowledgments
assert mock_socket.recv.call_count == 9 # v2.0 has 9 recv calls (includes chunk OK)

View File

@@ -1062,7 +1062,7 @@ def test_upload_program_ota_with_file_arg(
assert exit_code == 0
assert host == "192.168.1.100"
mock_run_ota.assert_called_once_with(
["192.168.1.100"], 3232, "", Path("custom.bin")
["192.168.1.100"], 3232, None, Path("custom.bin")
)
@@ -1119,7 +1119,9 @@ def test_upload_program_ota_with_mqtt_resolution(
expected_firmware = (
tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin"
)
mock_run_ota.assert_called_once_with(["192.168.1.100"], 3232, "", expected_firmware)
mock_run_ota.assert_called_once_with(
["192.168.1.100"], 3232, None, expected_firmware
)
@patch("esphome.__main__.importlib.import_module")
@@ -1976,3 +1978,292 @@ def test_command_clean_all_args_used() -> None:
# Verify the correct configuration paths were passed
mock_clean_all.assert_any_call(["/path/to/config1"])
mock_clean_all.assert_any_call(["/path/to/config2", "/path/to/config3"])
def test_upload_program_ota_static_ip_with_mqttip(
mock_mqtt_get_ip: Mock,
mock_run_ota: Mock,
tmp_path: Path,
) -> None:
"""Test upload_program with static IP and MQTTIP (issue #11260).
This tests the scenario where a device has manual_ip (static IP) configured
and MQTT is also configured. The devices list contains both the static IP
and "MQTTIP" magic string. This previously failed because only the first
device was checked for MQTT resolution.
"""
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path)
mock_mqtt_get_ip.return_value = ["192.168.2.50"] # Different subnet
mock_run_ota.return_value = (0, "192.168.1.100")
config = {
CONF_OTA: [
{
CONF_PLATFORM: CONF_ESPHOME,
CONF_PORT: 3232,
}
],
CONF_MQTT: {
CONF_BROKER: "mqtt.local",
},
}
args = MockArgs(username="user", password="pass", client_id="client")
# Simulates choose_upload_log_host returning static IP + MQTTIP
devices = ["192.168.1.100", "MQTTIP"]
exit_code, host = upload_program(config, args, devices)
assert exit_code == 0
assert host == "192.168.1.100"
# Verify MQTT was resolved
mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client")
# Verify espota2.run_ota was called with both IPs
expected_firmware = (
tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin"
)
mock_run_ota.assert_called_once_with(
["192.168.1.100", "192.168.2.50"], 3232, None, expected_firmware
)
def test_upload_program_ota_multiple_mqttip_resolves_once(
mock_mqtt_get_ip: Mock,
mock_run_ota: Mock,
tmp_path: Path,
) -> None:
"""Test that MQTT resolution only happens once even with multiple MQTT magic strings."""
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path)
mock_mqtt_get_ip.return_value = ["192.168.2.50", "192.168.2.51"]
mock_run_ota.return_value = (0, "192.168.2.50")
config = {
CONF_OTA: [
{
CONF_PLATFORM: CONF_ESPHOME,
CONF_PORT: 3232,
}
],
CONF_MQTT: {
CONF_BROKER: "mqtt.local",
},
}
args = MockArgs(username="user", password="pass", client_id="client")
# Multiple MQTT magic strings in the list
devices = ["MQTTIP", "MQTT", "192.168.1.100"]
exit_code, host = upload_program(config, args, devices)
assert exit_code == 0
assert host == "192.168.2.50"
# Verify MQTT was only resolved once despite multiple MQTT magic strings
mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client")
# Verify espota2.run_ota was called with all unique IPs
expected_firmware = (
tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin"
)
mock_run_ota.assert_called_once_with(
["192.168.2.50", "192.168.2.51", "192.168.1.100"], 3232, None, expected_firmware
)
def test_upload_program_ota_mqttip_deduplication(
mock_mqtt_get_ip: Mock,
mock_run_ota: Mock,
tmp_path: Path,
) -> None:
"""Test that duplicate IPs are filtered when MQTT returns same IP as static IP."""
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path)
# MQTT returns the same IP as the static IP
mock_mqtt_get_ip.return_value = ["192.168.1.100"]
mock_run_ota.return_value = (0, "192.168.1.100")
config = {
CONF_OTA: [
{
CONF_PLATFORM: CONF_ESPHOME,
CONF_PORT: 3232,
}
],
CONF_MQTT: {
CONF_BROKER: "mqtt.local",
},
}
args = MockArgs(username="user", password="pass", client_id="client")
devices = ["192.168.1.100", "MQTTIP"]
exit_code, host = upload_program(config, args, devices)
assert exit_code == 0
assert host == "192.168.1.100"
# Verify MQTT was resolved
mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client")
# Verify espota2.run_ota was called with deduplicated IPs (only one instance of 192.168.1.100)
# Note: Current implementation doesn't dedupe, so we'll get the IP twice
# This test documents current behavior - deduplication could be future enhancement
mock_run_ota.assert_called_once()
call_args = mock_run_ota.call_args[0]
# Should contain both the original IP and MQTT-resolved IP (even if duplicate)
assert "192.168.1.100" in call_args[0]
@patch("esphome.components.api.client.run_logs")
def test_show_logs_api_static_ip_with_mqttip(
mock_run_logs: Mock,
mock_mqtt_get_ip: Mock,
) -> None:
"""Test show_logs with static IP and MQTTIP (issue #11260).
This tests the scenario where a device has manual_ip (static IP) configured
and MQTT is also configured. The devices list contains both the static IP
and "MQTTIP" magic string.
"""
setup_core(
config={
"logger": {},
CONF_API: {},
CONF_MQTT: {CONF_BROKER: "mqtt.local"},
},
platform=PLATFORM_ESP32,
)
mock_run_logs.return_value = 0
mock_mqtt_get_ip.return_value = ["192.168.2.50"]
args = MockArgs(username="user", password="pass", client_id="client")
# Simulates choose_upload_log_host returning static IP + MQTTIP
devices = ["192.168.1.100", "MQTTIP"]
result = show_logs(CORE.config, args, devices)
assert result == 0
# Verify MQTT was resolved
mock_mqtt_get_ip.assert_called_once_with(CORE.config, "user", "pass", "client")
# Verify run_logs was called with both IPs
mock_run_logs.assert_called_once_with(
CORE.config, ["192.168.1.100", "192.168.2.50"]
)
@patch("esphome.components.api.client.run_logs")
def test_show_logs_api_multiple_mqttip_resolves_once(
mock_run_logs: Mock,
mock_mqtt_get_ip: Mock,
) -> None:
"""Test that MQTT resolution only happens once for show_logs with multiple MQTT magic strings."""
setup_core(
config={
"logger": {},
CONF_API: {},
CONF_MQTT: {CONF_BROKER: "mqtt.local"},
},
platform=PLATFORM_ESP32,
)
mock_run_logs.return_value = 0
mock_mqtt_get_ip.return_value = ["192.168.2.50", "192.168.2.51"]
args = MockArgs(username="user", password="pass", client_id="client")
# Multiple MQTT magic strings in the list
devices = ["MQTTIP", "192.168.1.100", "MQTT"]
result = show_logs(CORE.config, args, devices)
assert result == 0
# Verify MQTT was only resolved once despite multiple MQTT magic strings
mock_mqtt_get_ip.assert_called_once_with(CORE.config, "user", "pass", "client")
# Verify run_logs was called with all unique IPs (MQTT strings replaced with IPs)
# Note: "MQTT" is a different magic string from "MQTTIP", but both trigger MQTT resolution
# The _resolve_network_devices helper filters out both after first resolution
mock_run_logs.assert_called_once_with(
CORE.config, ["192.168.2.50", "192.168.2.51", "192.168.1.100"]
)
def test_upload_program_ota_mqtt_timeout_fallback(
mock_mqtt_get_ip: Mock,
mock_run_ota: Mock,
tmp_path: Path,
) -> None:
"""Test upload_program falls back to other devices when MQTT times out."""
setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path)
# MQTT times out
mock_mqtt_get_ip.side_effect = EsphomeError("Failed to find IP via MQTT")
mock_run_ota.return_value = (0, "192.168.1.100")
config = {
CONF_OTA: [
{
CONF_PLATFORM: CONF_ESPHOME,
CONF_PORT: 3232,
}
],
CONF_MQTT: {
CONF_BROKER: "mqtt.local",
},
}
args = MockArgs(username="user", password="pass", client_id="client")
# Static IP first, MQTTIP second
devices = ["192.168.1.100", "MQTTIP"]
exit_code, host = upload_program(config, args, devices)
# Should succeed using the static IP even though MQTT failed
assert exit_code == 0
assert host == "192.168.1.100"
# Verify MQTT was attempted
mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client")
# Verify espota2.run_ota was called with only the static IP (MQTT failed)
expected_firmware = (
tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin"
)
mock_run_ota.assert_called_once_with(
["192.168.1.100"], 3232, None, expected_firmware
)
@patch("esphome.components.api.client.run_logs")
def test_show_logs_api_mqtt_timeout_fallback(
mock_run_logs: Mock,
mock_mqtt_get_ip: Mock,
) -> None:
"""Test show_logs falls back to other devices when MQTT times out."""
setup_core(
config={
"logger": {},
CONF_API: {},
CONF_MQTT: {CONF_BROKER: "mqtt.local"},
},
platform=PLATFORM_ESP32,
)
mock_run_logs.return_value = 0
# MQTT times out
mock_mqtt_get_ip.side_effect = EsphomeError("Failed to find IP via MQTT")
args = MockArgs(username="user", password="pass", client_id="client")
# Static IP first, MQTTIP second
devices = ["192.168.1.100", "MQTTIP"]
result = show_logs(CORE.config, args, devices)
# Should succeed using the static IP even though MQTT failed
assert result == 0
# Verify MQTT was attempted
mock_mqtt_get_ip.assert_called_once_with(CORE.config, "user", "pass", "client")
# Verify run_logs was called with only the static IP (MQTT failed)
mock_run_logs.assert_called_once_with(CORE.config, ["192.168.1.100"])