mirror of
https://github.com/esphome/esphome.git
synced 2026-10-10 05:23:07 +00:00
Merge remote-tracking branch 'origin/dev' into jesserockz-2026-446
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
"""Tests for web_server authentication codegen."""
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.core import CORE
|
||||
|
||||
_DEFAULT_CHANGE_WARNING = "default will change to 'digest' in ESPHome 2027.1.0"
|
||||
|
||||
|
||||
def _has_define(name: str) -> bool:
|
||||
return any(d.name == name for d in CORE.defines)
|
||||
|
||||
|
||||
def test_web_server_auth_default_is_basic_with_deprecation_warning(
|
||||
generate_main: Callable[[str], str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Auth without an explicit type builds Basic and warns about the upcoming default change."""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/web_server/web_server_auth_default.yaml"
|
||||
)
|
||||
|
||||
assert '->set_auth_username("admin");' in main_cpp
|
||||
assert '->set_auth_password("password");' in main_cpp
|
||||
assert _has_define("USE_WEBSERVER_AUTH")
|
||||
assert not _has_define("USE_WEBSERVER_AUTH_DIGEST")
|
||||
assert _DEFAULT_CHANGE_WARNING in caplog.text
|
||||
|
||||
|
||||
def test_web_server_auth_explicit_basic_no_warning(
|
||||
generate_main: Callable[[str], str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Auth type basic builds Basic and does not warn."""
|
||||
generate_main("tests/component_tests/web_server/web_server_auth_basic.yaml")
|
||||
|
||||
assert _has_define("USE_WEBSERVER_AUTH")
|
||||
assert not _has_define("USE_WEBSERVER_AUTH_DIGEST")
|
||||
assert _DEFAULT_CHANGE_WARNING not in caplog.text
|
||||
|
||||
|
||||
def test_web_server_auth_explicit_digest(
|
||||
generate_main: Callable[[str], str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Auth type digest builds Digest and does not warn."""
|
||||
generate_main("tests/component_tests/web_server/web_server_auth_digest.yaml")
|
||||
|
||||
assert _has_define("USE_WEBSERVER_AUTH")
|
||||
assert _has_define("USE_WEBSERVER_AUTH_DIGEST")
|
||||
assert _DEFAULT_CHANGE_WARNING not in caplog.text
|
||||
|
||||
|
||||
def test_web_server_without_auth(
|
||||
generate_main: Callable[[str], str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Without an auth block, no auth is compiled in and no warning is emitted."""
|
||||
generate_main("tests/component_tests/web_server/web_server_no_auth.yaml")
|
||||
|
||||
assert not _has_define("USE_WEBSERVER_AUTH")
|
||||
assert not _has_define("USE_WEBSERVER_AUTH_DIGEST")
|
||||
assert _DEFAULT_CHANGE_WARNING not in caplog.text
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
web_server:
|
||||
auth:
|
||||
username: admin
|
||||
password: password
|
||||
type: basic
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
web_server:
|
||||
auth:
|
||||
username: admin
|
||||
password: password
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
web_server:
|
||||
auth:
|
||||
username: admin
|
||||
password: password
|
||||
type: digest
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
web_server:
|
||||
@@ -1,32 +1,20 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
|
||||
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
|
||||
test_display: !include ../../test_build_components/common/test_display/test_display.yaml
|
||||
|
||||
xl9535:
|
||||
id: expander
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
id: gsl3670_display
|
||||
spi_id: spi_bus
|
||||
model: t-display-s3-pro
|
||||
# The model's default DC pin (GPIO9) clashes with the shared i2c bus SCL
|
||||
# pin, so override it onto a free pin for this test.
|
||||
dc_pin: GPIO5
|
||||
|
||||
psram:
|
||||
mode: quad
|
||||
|
||||
touchscreen:
|
||||
# Firmware downloaded from the model's default release URL and cached.
|
||||
- platform: gsl3670
|
||||
model: seeed-reterminal-d1001
|
||||
display: gsl3670_display
|
||||
display: test_display_screen
|
||||
interrupt_pin: 18
|
||||
# Explicit firmware URL + SHA-256 override.
|
||||
- platform: gsl3670
|
||||
model: seeed-reterminal-d1001
|
||||
display: gsl3670_display
|
||||
display: test_display_screen
|
||||
reset_pin: 10
|
||||
interrupt_pin: 11
|
||||
firmware:
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
display:
|
||||
- platform: ssd1306_i2c
|
||||
i2c_id: i2c_bus
|
||||
id: gt911_ssd1306_i2c_display
|
||||
model: SSD1306_128X64
|
||||
reset_pin: ${display_reset_pin}
|
||||
pages:
|
||||
- id: gt911_page1
|
||||
lambda: |-
|
||||
it.rectangle(0, 0, it.get_width(), it.get_height());
|
||||
|
||||
touchscreen:
|
||||
- platform: gt911
|
||||
i2c_id: i2c_bus
|
||||
id: gt911_touchscreen
|
||||
display: gt911_ssd1306_i2c_display
|
||||
display: test_display_screen
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
reset_pin: ${reset_pin}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
substitutions:
|
||||
display_reset_pin: "10"
|
||||
interrupt_pin: "20"
|
||||
reset_pin: "21"
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
test_display: !include ../../test_build_components/common/test_display/test_display.yaml
|
||||
gt911: !include common.yaml
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
substitutions:
|
||||
display_reset_pin: "10"
|
||||
interrupt_pin: "12"
|
||||
reset_pin: "13"
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
test_display: !include ../../test_build_components/common/test_display/test_display.yaml
|
||||
gt911: !include common.yaml
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
substitutions:
|
||||
display_reset_pin: "10"
|
||||
interrupt_pin: "20"
|
||||
reset_pin: "21"
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
test_display: !include ../../test_build_components/common/test_display/test_display.yaml
|
||||
gt911: !include common.yaml
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# The test_display platform (and its external_components entry) is provided by
|
||||
# the shared package included from the test.*.yaml files. These extra instances
|
||||
# exercise the remaining `dimensions` code paths: the width/height map form and
|
||||
# the default when omitted. The package's own `test_display_screen` covers the
|
||||
# "WIDTHxHEIGHT" string form.
|
||||
display:
|
||||
- platform: test_display
|
||||
id: test_display_wh_dimensions
|
||||
dimensions:
|
||||
width: 320
|
||||
height: 240
|
||||
- platform: test_display
|
||||
id: test_display_default_dimensions
|
||||
@@ -0,0 +1,36 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_ID, CONF_WIDTH
|
||||
from esphome.core import CoroPriority, coroutine_with_priority
|
||||
|
||||
test_display_ns = cg.esphome_ns.namespace("test_display")
|
||||
TestDisplay = test_display_ns.class_("TestDisplay", display.Display)
|
||||
|
||||
CONFIG_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(TestDisplay),
|
||||
cv.Optional(CONF_DIMENSIONS, default="100x100"): cv.Any(
|
||||
cv.dimensions,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_WIDTH): cv.int_,
|
||||
cv.Required(CONF_HEIGHT): cv.int_,
|
||||
}
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
|
||||
dimensions = config[CONF_DIMENSIONS]
|
||||
if isinstance(dimensions, dict):
|
||||
width, height = dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT]
|
||||
else:
|
||||
width, height = dimensions
|
||||
cg.add(var.set_dimensions(width, height))
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/core/color.h"
|
||||
|
||||
namespace esphome::test_display {
|
||||
|
||||
/** A no-op display that draws nothing and uses no pins.
|
||||
*
|
||||
* It exists purely to satisfy components that require a display (for example
|
||||
* touchscreens, which read the display dimensions) in configurations - most
|
||||
* notably YAML build tests - where a real display driver would only get in the
|
||||
* way by occupying GPIO pins and pulling in bus dependencies.
|
||||
*/
|
||||
class TestDisplay : public display::Display {
|
||||
public:
|
||||
void update() override { this->do_update_(); }
|
||||
|
||||
void set_dimensions(int width, int height) {
|
||||
this->width_ = width;
|
||||
this->height_ = height;
|
||||
}
|
||||
|
||||
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
|
||||
|
||||
void draw_pixel_at(int x, int y, Color color) override {}
|
||||
|
||||
protected:
|
||||
int get_width_internal() override { return this->width_; }
|
||||
int get_height_internal() override { return this->height_; }
|
||||
|
||||
int width_{0};
|
||||
int height_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::test_display
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
test_display: !include ../../test_build_components/common/test_display/test_display.yaml
|
||||
test_display_extra: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
test_display: !include ../../test_build_components/common/test_display/test_display.yaml
|
||||
test_display_extra: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
test_display: !include ../../test_build_components/common/test_display/test_display.yaml
|
||||
test_display_extra: !include common.yaml
|
||||
@@ -1,19 +1,8 @@
|
||||
display:
|
||||
- platform: ssd1306_i2c
|
||||
i2c_id: i2c_bus
|
||||
id: tt21100_ssd1306_i2c_display
|
||||
model: SSD1306_128X64
|
||||
reset_pin: ${disp_reset_pin}
|
||||
pages:
|
||||
- id: tt21100_page1
|
||||
lambda: |-
|
||||
it.rectangle(0, 0, it.get_width(), it.get_height());
|
||||
|
||||
touchscreen:
|
||||
- platform: tt21100
|
||||
i2c_id: i2c_bus
|
||||
id: tt21100_touchscreen
|
||||
display: tt21100_ssd1306_i2c_display
|
||||
display: test_display_screen
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
reset_pin: ${reset_pin}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
substitutions:
|
||||
disp_reset_pin: GPIO12
|
||||
interrupt_pin: GPIO15
|
||||
reset_pin: GPIO4
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
test_display: !include ../../test_build_components/common/test_display/test_display.yaml
|
||||
tt21100: !include common.yaml
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
substitutions:
|
||||
disp_reset_pin: GPIO0
|
||||
interrupt_pin: GPIO15
|
||||
reset_pin: GPIO16
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
test_display: !include ../../test_build_components/common/test_display/test_display.yaml
|
||||
tt21100: !include common.yaml
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
substitutions:
|
||||
disp_reset_pin: GPIO10
|
||||
interrupt_pin: GPIO2
|
||||
reset_pin: GPIO3
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
test_display: !include ../../test_build_components/common/test_display/test_display.yaml
|
||||
tt21100: !include common.yaml
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
<<: !include common_v2.yaml
|
||||
packages:
|
||||
web_server: !include common_v2.yaml
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<<: !include common_v2.yaml
|
||||
packages:
|
||||
web_server: !include common_v2.yaml
|
||||
|
||||
web_server:
|
||||
auth:
|
||||
username: admin
|
||||
password: password
|
||||
type: digest
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
<<: !include common_v2.yaml
|
||||
packages:
|
||||
web_server: !include common_v2.yaml
|
||||
|
||||
web_server:
|
||||
auth:
|
||||
username: admin
|
||||
password: password
|
||||
type: digest
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
<<: !include common_v2.yaml
|
||||
packages:
|
||||
web_server: !include common_v2.yaml
|
||||
|
||||
web_server:
|
||||
auth:
|
||||
username: admin
|
||||
password: password
|
||||
type: basic
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
<<: !include common_v1.yaml
|
||||
packages:
|
||||
web_server: !include common_v1.yaml
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
<<: !include common_v1.yaml
|
||||
packages:
|
||||
web_server: !include common_v1.yaml
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
<<: !include common_v3.yaml
|
||||
packages:
|
||||
web_server: !include common_v3.yaml
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
packages:
|
||||
web_server: !include common_v2.yaml
|
||||
|
||||
web_server:
|
||||
auth:
|
||||
username: admin
|
||||
password: password
|
||||
type: basic
|
||||
@@ -10,7 +10,10 @@ sys.path.insert(0, str((Path(__file__).parent / ".." / ".." / "script").resolve(
|
||||
|
||||
import merge_component_configs # noqa: E402
|
||||
|
||||
from esphome import yaml_util # noqa: E402
|
||||
|
||||
deduplicate_by_id = merge_component_configs.deduplicate_by_id
|
||||
prepare_component_body = merge_component_configs.prepare_component_body
|
||||
|
||||
|
||||
def test_identical_duplicate_ids_collapse() -> None:
|
||||
@@ -99,3 +102,56 @@ def test_nested_lists_are_checked() -> None:
|
||||
}
|
||||
with pytest.raises(ValueError, match="dup"):
|
||||
deduplicate_by_id(data)
|
||||
|
||||
|
||||
def test_nested_package_includes_are_fully_expanded(tmp_path: Path) -> None:
|
||||
"""A package include that itself pulls in another package expands fully.
|
||||
|
||||
Mirrors web_server's tests, where test.yaml includes common_v2, which
|
||||
includes common (holding the wifi/network config). Without recursive
|
||||
expansion the nested include is dropped and network config is lost.
|
||||
"""
|
||||
(tmp_path / "common.yaml").write_text("wifi:\n ssid: MySSID\n")
|
||||
(tmp_path / "common_v2.yaml").write_text(
|
||||
"packages:\n device_base: !include common.yaml\nweb_server:\n port: 8080\n"
|
||||
)
|
||||
(tmp_path / "test.yaml").write_text(
|
||||
"packages:\n web_server: !include common_v2.yaml\n"
|
||||
"web_server:\n auth:\n username: admin\n"
|
||||
)
|
||||
|
||||
comp_data = yaml_util.load_yaml(tmp_path / "test.yaml")
|
||||
result = prepare_component_body(comp_data, "web_server", tmp_path)
|
||||
|
||||
assert "packages" not in result
|
||||
assert result["wifi"] == {"ssid": "MySSID"}
|
||||
assert result["web_server"] == {"port": 8080, "auth": {"username": "admin"}}
|
||||
|
||||
|
||||
def test_common_bus_package_is_left_for_caller(tmp_path: Path) -> None:
|
||||
"""Common bus packages are not expanded inline; the caller re-adds them."""
|
||||
comp_data = {
|
||||
"packages": {
|
||||
"i2c": {"sda": 21, "scl": 22},
|
||||
"device_base": {"wifi": {"ssid": "MySSID"}},
|
||||
},
|
||||
}
|
||||
result = prepare_component_body(comp_data, "mycomp", tmp_path)
|
||||
|
||||
# The bus package's body must not be merged in, and the packages key is
|
||||
# dropped entirely for the caller to re-add the common bus package.
|
||||
assert "packages" not in result
|
||||
assert "sda" not in result
|
||||
assert result["wifi"] == {"ssid": "MySSID"}
|
||||
|
||||
|
||||
def test_list_style_packages_are_expanded(tmp_path: Path) -> None:
|
||||
"""List-style package includes are expanded and the key removed."""
|
||||
(tmp_path / "common.yaml").write_text("wifi:\n ssid: MySSID\n")
|
||||
(tmp_path / "test.yaml").write_text("packages:\n - !include common.yaml\n")
|
||||
|
||||
comp_data = yaml_util.load_yaml(tmp_path / "test.yaml")
|
||||
result = prepare_component_body(comp_data, "mycomp", tmp_path)
|
||||
|
||||
assert "packages" not in result
|
||||
assert result["wifi"] == {"ssid": "MySSID"}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# Shared "test display" package for component tests.
|
||||
#
|
||||
# Provides a no-op display (id: test_display_screen) that uses no pins and no
|
||||
# bus, so tests that only need a display to exist -- touchscreens especially --
|
||||
# don't have to instantiate a real driver and fight it over GPIOs. Include it
|
||||
# like a common bus package; the consuming test does NOT need to declare
|
||||
# external_components itself:
|
||||
#
|
||||
# packages:
|
||||
# test_display: !include ../../test_build_components/common/test_display/test_display.yaml
|
||||
#
|
||||
# then point the touchscreen (or other display consumer) at `test_display_screen`.
|
||||
#
|
||||
# The test_display platform lives at tests/components/test_display/components/ and
|
||||
# is loaded via external_components. The source path is written relative to the
|
||||
# build directory (tests/test_build_components/build/), which every test --
|
||||
# standalone or grouped -- is generated into, so this always resolves to the
|
||||
# component under tests/components/test_display/.
|
||||
external_components:
|
||||
- source: ../../components/test_display/components
|
||||
components: [test_display]
|
||||
|
||||
display:
|
||||
- platform: test_display
|
||||
id: test_display_screen
|
||||
dimensions: 240x320
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Tests for LibreTiny board detection, including renamed-board migration."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.libretiny import _detect_variant
|
||||
from esphome.components.libretiny.const import (
|
||||
FAMILY_LN882H,
|
||||
KEY_COMPONENT_DATA,
|
||||
KEY_LIBRETINY,
|
||||
)
|
||||
from esphome.components.ln882x import COMPONENT_DATA
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BOARD, CONF_FAMILY
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ln882x_core_data() -> None:
|
||||
"""Populate CORE the way the ln882x component schema does."""
|
||||
CORE.data[KEY_LIBRETINY] = {KEY_COMPONENT_DATA: COMPONENT_DATA}
|
||||
|
||||
|
||||
def test_detect_variant_known_board_passes(ln882x_core_data: None) -> None:
|
||||
"""A current board id resolves its family without warnings."""
|
||||
result = _detect_variant({CONF_BOARD: "generic-ln882h"})
|
||||
assert result[CONF_BOARD] == "generic-ln882h"
|
||||
assert result[CONF_FAMILY] == FAMILY_LN882H
|
||||
|
||||
|
||||
def test_detect_variant_renamed_board_migrates(
|
||||
ln882x_core_data: None, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""A pre-rename board id validates against the new id, with a warning."""
|
||||
result = _detect_variant({CONF_BOARD: "generic-ln882hki"})
|
||||
assert result[CONF_BOARD] == "generic-ln882h"
|
||||
assert result[CONF_FAMILY] == FAMILY_LN882H
|
||||
assert "renamed to 'generic-ln882h'" in caplog.text
|
||||
|
||||
|
||||
def test_detect_variant_renamed_board_does_not_mutate_input(
|
||||
ln882x_core_data: None,
|
||||
) -> None:
|
||||
"""Migration copies the config; the caller's dict keeps the old id."""
|
||||
value = {CONF_BOARD: "generic-ln882hki"}
|
||||
_detect_variant(value)
|
||||
assert value[CONF_BOARD] == "generic-ln882hki"
|
||||
|
||||
|
||||
def test_detect_variant_unknown_board_still_raises(ln882x_core_data: None) -> None:
|
||||
"""Ids outside the rename map keep the family-override error."""
|
||||
with pytest.raises(cv.Invalid, match="This board is unknown"):
|
||||
_detect_variant({CONF_BOARD: "not-a-real-board"})
|
||||
Reference in New Issue
Block a user