mirror of
https://github.com/esphome/esphome.git
synced 2026-02-21 00:45:35 -07:00
Merge branch 'integration' into memory_api
This commit is contained in:
2
Doxyfile
2
Doxyfile
@@ -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 = 2025.12.0-dev
|
||||
PROJECT_NUMBER = 2025.11.0b1
|
||||
|
||||
# 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
|
||||
|
||||
@@ -476,8 +476,9 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
||||
auto *light = static_cast<light::LightState *>(entity);
|
||||
ListEntitiesLightResponse msg;
|
||||
auto traits = light->get_traits();
|
||||
auto supported_modes = traits.get_supported_color_modes();
|
||||
// Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values
|
||||
msg.supported_color_modes = &traits.get_supported_color_modes();
|
||||
msg.supported_color_modes = &supported_modes;
|
||||
if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
|
||||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
|
||||
msg.min_mireds = traits.get_min_mireds();
|
||||
|
||||
@@ -334,12 +334,14 @@ def _is_framework_url(source: str) -> str:
|
||||
# - https://github.com/espressif/arduino-esp32/releases
|
||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(3, 3, 2),
|
||||
"latest": cv.Version(3, 3, 2),
|
||||
"dev": cv.Version(3, 3, 2),
|
||||
"latest": cv.Version(3, 3, 4),
|
||||
"dev": cv.Version(3, 3, 4),
|
||||
}
|
||||
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(3, 3, 2): cv.Version(55, 3, 31, "1"),
|
||||
cv.Version(3, 3, 1): cv.Version(55, 3, 31, "1"),
|
||||
cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(3, 3, 2): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(3, 3, 1): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"),
|
||||
cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"),
|
||||
cv.Version(3, 2, 0): cv.Version(54, 3, 20),
|
||||
@@ -357,8 +359,8 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"dev": cv.Version(5, 5, 1),
|
||||
}
|
||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"),
|
||||
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"),
|
||||
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
|
||||
cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
|
||||
cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
|
||||
@@ -373,9 +375,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
# The platform-espressif32 version
|
||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||
PLATFORM_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(55, 3, 31, "1"),
|
||||
"latest": cv.Version(55, 3, 31, "1"),
|
||||
"dev": cv.Version(55, 3, 31, "1"),
|
||||
"recommended": cv.Version(55, 3, 31, "2"),
|
||||
"latest": cv.Version(55, 3, 31, "2"),
|
||||
"dev": cv.Version(55, 3, 31, "2"),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -406,7 +406,7 @@ void LightCall::transform_parameters_() {
|
||||
}
|
||||
}
|
||||
ColorMode LightCall::compute_color_mode_() {
|
||||
const auto &supported_modes = this->parent_->get_traits().get_supported_color_modes();
|
||||
auto supported_modes = this->parent_->get_traits().get_supported_color_modes();
|
||||
int supported_count = supported_modes.size();
|
||||
|
||||
// Some lights don't support any color modes (e.g. monochromatic light), leave it at unknown.
|
||||
|
||||
@@ -18,7 +18,8 @@ class LightTraits {
|
||||
public:
|
||||
LightTraits() = default;
|
||||
|
||||
const ColorModeMask &get_supported_color_modes() const { return this->supported_color_modes_; }
|
||||
// Return by value to avoid dangling reference when get_traits() returns a temporary
|
||||
ColorModeMask get_supported_color_modes() const { return this->supported_color_modes_; }
|
||||
void set_supported_color_modes(ColorModeMask supported_color_modes) {
|
||||
this->supported_color_modes_ = supported_color_modes;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2025.12.0-dev"
|
||||
__version__ = "2025.11.0b1"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -412,7 +412,12 @@ template<typename... Ts> class WaitUntilAction : public Action<Ts...>, public Co
|
||||
|
||||
void setup() override {
|
||||
// Start with loop disabled - only enable when there's work to do
|
||||
this->disable_loop();
|
||||
// IMPORTANT: Only disable if num_running_ is 0, otherwise play_complex() was already
|
||||
// called before our setup() (e.g., from on_boot trigger at same priority level)
|
||||
// and we must not undo its enable_loop() call
|
||||
if (this->num_running_ == 0) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void play_complex(const Ts &...x) override {
|
||||
|
||||
@@ -30,6 +30,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.helpers import get_int_env, get_str_env
|
||||
from esphome.log import AnsiFore, color
|
||||
from esphome.types import ConfigType
|
||||
from esphome.util import safe_print
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -154,8 +155,12 @@ def show_discover(config, username=None, password=None, client_id=None):
|
||||
|
||||
|
||||
def get_esphome_device_ip(
|
||||
config, username=None, password=None, client_id=None, timeout=25
|
||||
):
|
||||
config: ConfigType,
|
||||
username: str | None = None,
|
||||
password: str | None = None,
|
||||
client_id: str | None = None,
|
||||
timeout: int | float = 25,
|
||||
) -> list[str]:
|
||||
if CONF_MQTT not in config:
|
||||
raise EsphomeError(
|
||||
"Cannot discover IP via MQTT as the config does not include the mqtt: "
|
||||
@@ -166,6 +171,10 @@ def get_esphome_device_ip(
|
||||
"Cannot discover IP via MQTT as the config does not include the device name: "
|
||||
"component"
|
||||
)
|
||||
if not config[CONF_MQTT].get(CONF_BROKER):
|
||||
raise EsphomeError(
|
||||
"Cannot discover IP via MQTT as the broker is not configured"
|
||||
)
|
||||
|
||||
dev_name = config[CONF_ESPHOME][CONF_NAME]
|
||||
dev_ip = None
|
||||
|
||||
@@ -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
|
||||
47
tests/integration/fixtures/wait_until_on_boot.yaml
Normal file
47
tests/integration/fixtures/wait_until_on_boot.yaml
Normal file
@@ -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");
|
||||
}
|
||||
91
tests/integration/test_wait_until_on_boot.py
Normal file
91
tests/integration/test_wait_until_on_boot.py
Normal file
@@ -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."
|
||||
)
|
||||
@@ -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
tests/unit_tests/test_mqtt.py
Normal file
91
tests/unit_tests/test_mqtt.py
Normal 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)
|
||||
Reference in New Issue
Block a user