Merge branch 'integration' into memory_api

This commit is contained in:
J. Nick Koston
2025-11-12 12:02:04 -06:00
33 changed files with 314 additions and 199 deletions
@@ -1,2 +0,0 @@
packages:
common: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
<<: !include common.yaml
@@ -1 +0,0 @@
<<: !include common.yaml
@@ -1,15 +0,0 @@
packages:
common: !include common.yaml
matrix_keypad:
id: keypad
rows:
- pin: 10
- pin: 11
columns:
- pin: 12
- pin: 13
keys: "1234"
has_pulldowns: true
on_key:
- lambda: ESP_LOGI("KEY", "key %d pressed", x);
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
<<: !include common.yaml
@@ -1,4 +0,0 @@
substitutions:
pin: GPIO4
<<: !include common.yaml
@@ -1,4 +0,0 @@
substitutions:
pin: GPIO1
<<: !include common.yaml
@@ -1,2 +0,0 @@
packages:
common: !include common.yaml
@@ -1,8 +0,0 @@
substitutions:
scl_pin: GPIO40
sda_pin: GPIO41
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
<<: !include common.yaml
@@ -1,2 +0,0 @@
packages:
common: !include common.yaml
@@ -1,48 +0,0 @@
<<: !include ../logger/common-usb_serial_jtag.yaml
esphome:
on_boot:
then:
- uart.write:
id: uart_1
data: 'Hello World'
- uart.write:
id: uart_1
data: [0x00, 0x20, 0x42]
uart:
- id: uart_1
tx_pin: 4
rx_pin: 5
flow_control_pin: 6
baud_rate: 9600
data_bits: 8
rx_buffer_size: 512
rx_full_threshold: 10
rx_timeout: 1
parity: EVEN
stop_bits: 2
- id: uart_2
tx_pin: 7
rx_pin: 8
flow_control_pin: 9
baud_rate: 9600
data_bits: 8
rx_buffer_size: 512
rx_full_threshold: 10
rx_timeout: 1
parity: EVEN
stop_bits: 2
- id: uart_3
tx_pin: 10
rx_pin: 11
flow_control_pin: 12
baud_rate: 9600
data_bits: 8
rx_buffer_size: 512
rx_full_threshold: 10
rx_timeout: 1
parity: EVEN
stop_bits: 2
@@ -1,9 +0,0 @@
substitutions:
scl_pin: GPIO40
sda_pin: GPIO41
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-s3-idf.yaml
<<: !include common.yaml
@@ -1,11 +0,0 @@
substitutions:
clk_pin: GPIO40
miso_pin: GPIO41
mosi_pin: GPIO6
cs_pin: GPIO19
packages:
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
uart_bridge_2: !include ../../test_build_components/common/uart_bridge_2/esp32-s3-idf.yaml
<<: !include common.yaml
@@ -1,9 +0,0 @@
substitutions:
scl_pin: GPIO40
sda_pin: GPIO41
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml
@@ -1,11 +0,0 @@
substitutions:
clk_pin: GPIO40
miso_pin: GPIO41
mosi_pin: GPIO6
cs_pin: GPIO19
packages:
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml
@@ -1,9 +0,0 @@
substitutions:
scl_pin: GPIO40
sda_pin: GPIO41
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml
@@ -1,11 +0,0 @@
substitutions:
clk_pin: GPIO40
miso_pin: GPIO41
mosi_pin: GPIO6
cs_pin: GPIO19
packages:
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml
@@ -1,9 +0,0 @@
substitutions:
scl_pin: GPIO40
sda_pin: GPIO41
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml
@@ -1,11 +0,0 @@
substitutions:
clk_pin: GPIO40
miso_pin: GPIO41
mosi_pin: GPIO6
cs_pin: GPIO19
packages:
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
uart_bridge_4: !include ../../test_build_components/common/uart_bridge_4/esp32-s3-idf.yaml
<<: !include common.yaml
@@ -0,0 +1,47 @@
# Test for wait_until in on_boot automation
# Reproduces bug where wait_until in on_boot would hang forever
# because WaitUntilAction::setup() would disable_loop() after
# play_complex() had already enabled it.
esphome:
name: wait-until-on-boot
on_boot:
then:
- logger.log: "on_boot: Starting wait_until test"
- globals.set:
id: on_boot_started
value: 'true'
- wait_until:
condition:
lambda: return id(test_flag);
timeout: 5s
- logger.log: "on_boot: wait_until completed successfully"
host:
logger:
level: DEBUG
globals:
- id: on_boot_started
type: bool
initial_value: 'false'
- id: test_flag
type: bool
initial_value: 'false'
api:
actions:
- action: set_test_flag
then:
- globals.set:
id: test_flag
value: 'true'
- action: check_on_boot_started
then:
- lambda: |-
if (id(on_boot_started)) {
ESP_LOGI("test", "on_boot has started");
} else {
ESP_LOGI("test", "on_boot has NOT started");
}
@@ -0,0 +1,91 @@
"""Integration test for wait_until in on_boot automation.
This test validates that wait_until works correctly when triggered from on_boot,
which runs at the same setup priority as WaitUntilAction itself. This was broken
before the fix because WaitUntilAction::setup() would unconditionally disable_loop(),
even if play_complex() had already been called and enabled the loop.
The bug: on_boot fires during StartupTrigger::setup(), which calls WaitUntilAction::play_complex()
before WaitUntilAction::setup() has run. Then when WaitUntilAction::setup() runs, it calls
disable_loop(), undoing the enable_loop() from play_complex(), causing wait_until to hang forever.
"""
from __future__ import annotations
import asyncio
import re
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_wait_until_on_boot(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that wait_until works in on_boot automation with a condition that becomes true later."""
loop = asyncio.get_running_loop()
on_boot_started = False
on_boot_completed = False
on_boot_started_pattern = re.compile(r"on_boot: Starting wait_until test")
on_boot_complete_pattern = re.compile(r"on_boot: wait_until completed successfully")
on_boot_started_future = loop.create_future()
on_boot_complete_future = loop.create_future()
def check_output(line: str) -> None:
"""Check log output for test progress."""
nonlocal on_boot_started, on_boot_completed
if on_boot_started_pattern.search(line):
on_boot_started = True
if not on_boot_started_future.done():
on_boot_started_future.set_result(True)
if on_boot_complete_pattern.search(line):
on_boot_completed = True
if not on_boot_complete_future.done():
on_boot_complete_future.set_result(True)
async with (
run_compiled(yaml_config, line_callback=check_output),
api_client_connected() as client,
):
# Wait for on_boot to start
await asyncio.wait_for(on_boot_started_future, timeout=10.0)
assert on_boot_started, "on_boot did not start"
# At this point, on_boot is blocked in wait_until waiting for test_flag to become true
# If the bug exists, wait_until's loop is disabled and it will never complete
# even after we set the flag
# Give a moment for setup to complete
await asyncio.sleep(0.5)
# Now set the flag that wait_until is waiting for
_, services = await client.list_entities_services()
set_flag_service = next(
(s for s in services if s.name == "set_test_flag"), None
)
assert set_flag_service is not None, "set_test_flag service not found"
client.execute_service(set_flag_service, {})
# If the fix works, wait_until's loop() will check the condition and proceed
# If the bug exists, wait_until is stuck with disabled loop and will timeout
try:
await asyncio.wait_for(on_boot_complete_future, timeout=2.0)
assert on_boot_completed, (
"on_boot wait_until did not complete after flag was set"
)
except TimeoutError:
pytest.fail(
"wait_until in on_boot did not complete within 2s after condition became true. "
"This indicates the bug where WaitUntilAction::setup() disables the loop "
"after play_complex() has already enabled it."
)
+50
View File
@@ -1166,6 +1166,56 @@ def test_upload_program_ota_with_mqtt_resolution(
)
def test_upload_program_ota_with_mqtt_empty_broker(
mock_mqtt_get_ip: Mock,
mock_is_ip_address: Mock,
mock_run_ota: Mock,
tmp_path: Path,
caplog: CaptureFixture,
) -> None:
"""Test upload_program with OTA when MQTT broker is empty (issue #11653)."""
setup_core(address="192.168.1.50", platform=PLATFORM_ESP32, tmp_path=tmp_path)
mock_is_ip_address.return_value = True
mock_mqtt_get_ip.side_effect = EsphomeError(
"Cannot discover IP via MQTT as the broker is not configured"
)
mock_run_ota.return_value = (0, "192.168.1.50")
config = {
CONF_OTA: [
{
CONF_PLATFORM: CONF_ESPHOME,
CONF_PORT: 3232,
}
],
CONF_MQTT: {
CONF_BROKER: "",
},
CONF_MDNS: {
CONF_DISABLED: True,
},
}
args = MockArgs(username="user", password="pass", client_id="client")
devices = ["MQTTIP", "192.168.1.50"]
exit_code, host = upload_program(config, args, devices)
assert exit_code == 0
assert host == "192.168.1.50"
# Verify MQTT was attempted but failed gracefully
mock_mqtt_get_ip.assert_called_once_with(config, "user", "pass", "client")
# Verify we fell back to the IP address
expected_firmware = (
tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin"
)
mock_run_ota.assert_called_once_with(
["192.168.1.50"], 3232, None, expected_firmware
)
# Verify warning was logged
assert "MQTT IP discovery failed" in caplog.text
@patch("esphome.__main__.importlib.import_module")
def test_upload_program_platform_specific_handler(
mock_import: Mock,
+91
View File
@@ -0,0 +1,91 @@
"""Unit tests for esphome.mqtt module."""
from __future__ import annotations
import pytest
from esphome.const import CONF_BROKER, CONF_ESPHOME, CONF_MQTT, CONF_NAME
from esphome.core import EsphomeError
from esphome.mqtt import get_esphome_device_ip
def test_get_esphome_device_ip_empty_broker() -> None:
"""Test that get_esphome_device_ip raises EsphomeError when broker is empty."""
config = {
CONF_MQTT: {
CONF_BROKER: "",
},
CONF_ESPHOME: {
CONF_NAME: "test-device",
},
}
with pytest.raises(
EsphomeError,
match="Cannot discover IP via MQTT as the broker is not configured",
):
get_esphome_device_ip(config)
def test_get_esphome_device_ip_none_broker() -> None:
"""Test that get_esphome_device_ip raises EsphomeError when broker is None."""
config = {
CONF_MQTT: {
CONF_BROKER: None,
},
CONF_ESPHOME: {
CONF_NAME: "test-device",
},
}
with pytest.raises(
EsphomeError,
match="Cannot discover IP via MQTT as the broker is not configured",
):
get_esphome_device_ip(config)
def test_get_esphome_device_ip_missing_mqtt() -> None:
"""Test that get_esphome_device_ip raises EsphomeError when mqtt config is missing."""
config = {
CONF_ESPHOME: {
CONF_NAME: "test-device",
},
}
with pytest.raises(
EsphomeError,
match="Cannot discover IP via MQTT as the config does not include the mqtt:",
):
get_esphome_device_ip(config)
def test_get_esphome_device_ip_missing_esphome() -> None:
"""Test that get_esphome_device_ip raises EsphomeError when esphome config is missing."""
config = {
CONF_MQTT: {
CONF_BROKER: "mqtt.local",
},
}
with pytest.raises(
EsphomeError,
match="Cannot discover IP via MQTT as the config does not include the device name:",
):
get_esphome_device_ip(config)
def test_get_esphome_device_ip_missing_name() -> None:
"""Test that get_esphome_device_ip raises EsphomeError when device name is missing."""
config = {
CONF_MQTT: {
CONF_BROKER: "mqtt.local",
},
CONF_ESPHOME: {},
}
with pytest.raises(
EsphomeError,
match="Cannot discover IP via MQTT as the config does not include the device name:",
):
get_esphome_device_ip(config)