mirror of
https://github.com/esphome/esphome.git
synced 2026-10-06 19:06:37 +00:00
Merge branch 'dev' into rp2040-upload-improvements
This commit is contained in:
@@ -3,3 +3,7 @@ audio_file:
|
||||
file:
|
||||
type: local
|
||||
path: $component_dir/test.wav
|
||||
|
||||
media_source:
|
||||
- platform: audio_file
|
||||
id: audio_file_source
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
ble_nus:
|
||||
type: uart
|
||||
tx_buffer_size: 160
|
||||
rx_buffer_size: 160
|
||||
@@ -12,7 +12,9 @@
|
||||
using namespace esphome;
|
||||
|
||||
void setup() {
|
||||
App.pre_setup("livingroom", "LivingRoom", false);
|
||||
static char name[] = "livingroom";
|
||||
static char friendly_name[] = "LivingRoom";
|
||||
App.pre_setup(name, sizeof(name) - 1, friendly_name, sizeof(friendly_name) - 1);
|
||||
auto *log = new logger::Logger(115200); // NOLINT
|
||||
log->pre_setup();
|
||||
log->set_uart_selection(logger::UART_SELECTION_UART0);
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
"""Tests for rp2040 generate_boards.py."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.rp2040.generate_boards import load_boards, parse_variant_pins
|
||||
|
||||
PICO_PINS_HEADER = textwrap.dedent("""\
|
||||
#pragma once
|
||||
#define PIN_LED (25u)
|
||||
#define PIN_SERIAL1_TX (0u)
|
||||
#define PIN_SERIAL1_RX (1u)
|
||||
#define PIN_WIRE0_SDA (4u)
|
||||
#define PIN_WIRE0_SCL (5u)
|
||||
#define PIN_WIRE1_SDA (26u)
|
||||
#define PIN_WIRE1_SCL (27u)
|
||||
#define PIN_SPI0_MISO (16u)
|
||||
#define PIN_SPI0_MOSI (19u)
|
||||
#define PIN_SPI0_SCK (18u)
|
||||
#define PIN_SPI0_SS (17u)
|
||||
#include "../generic/common.h"
|
||||
""")
|
||||
|
||||
PICOW_PINS_HEADER = textwrap.dedent("""\
|
||||
#pragma once
|
||||
#include <cyw43_wrappers.h>
|
||||
#define PIN_LED (64u)
|
||||
#define PIN_WIRE0_SDA (4u)
|
||||
#define PIN_WIRE0_SCL (5u)
|
||||
#include "../generic/common.h"
|
||||
""")
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def arduino_pico(tmp_path: Path) -> Path:
|
||||
"""Create a minimal arduino-pico directory structure."""
|
||||
json_dir = tmp_path / "tools" / "json"
|
||||
json_dir.mkdir(parents=True)
|
||||
variants_dir = tmp_path / "variants"
|
||||
variants_dir.mkdir()
|
||||
|
||||
generic_dir = variants_dir / "generic"
|
||||
generic_dir.mkdir()
|
||||
(generic_dir / "common.h").write_text("#pragma once\n")
|
||||
|
||||
return tmp_path
|
||||
|
||||
|
||||
def _add_board(
|
||||
arduino_pico: Path,
|
||||
board_name: str,
|
||||
mcu: str = "rp2040",
|
||||
variant: str | None = None,
|
||||
vendor: str = "",
|
||||
name: str | None = None,
|
||||
pins_header: str | None = None,
|
||||
) -> None:
|
||||
"""Add a board JSON and variant to the fake arduino-pico tree."""
|
||||
if variant is None:
|
||||
variant = board_name
|
||||
if name is None:
|
||||
name = board_name
|
||||
|
||||
json_dir = arduino_pico / "tools" / "json"
|
||||
variants_dir = arduino_pico / "variants"
|
||||
|
||||
board_json = {
|
||||
"build": {
|
||||
"mcu": mcu,
|
||||
"variant": variant,
|
||||
},
|
||||
"name": name,
|
||||
"vendor": vendor,
|
||||
}
|
||||
(json_dir / f"{board_name}.json").write_text(json.dumps(board_json))
|
||||
|
||||
variant_dir = variants_dir / variant
|
||||
variant_dir.mkdir(exist_ok=True)
|
||||
if pins_header is not None:
|
||||
(variant_dir / "pins_arduino.h").write_text(pins_header)
|
||||
|
||||
|
||||
def test_parse_basic_pins(tmp_path: Path) -> None:
|
||||
variant_dir = tmp_path / "rpipico"
|
||||
variant_dir.mkdir()
|
||||
(variant_dir / "pins_arduino.h").write_text(PICO_PINS_HEADER)
|
||||
|
||||
pins = parse_variant_pins(variant_dir)
|
||||
assert pins["LED"] == 25
|
||||
assert pins["SDA"] == 4
|
||||
assert pins["SCL"] == 5
|
||||
assert pins["SDA1"] == 26
|
||||
assert pins["SCL1"] == 27
|
||||
assert pins["MISO"] == 16
|
||||
assert pins["MOSI"] == 19
|
||||
assert pins["SCK"] == 18
|
||||
assert pins["SS"] == 17
|
||||
assert pins["TX"] == 0
|
||||
assert pins["RX"] == 1
|
||||
|
||||
|
||||
def test_parse_cyw43_led_pin(tmp_path: Path) -> None:
|
||||
variant_dir = tmp_path / "rpipicow"
|
||||
variant_dir.mkdir()
|
||||
(variant_dir / "pins_arduino.h").write_text(PICOW_PINS_HEADER)
|
||||
|
||||
pins = parse_variant_pins(variant_dir)
|
||||
assert pins["LED"] == 64
|
||||
|
||||
|
||||
def test_parse_missing_header(tmp_path: Path) -> None:
|
||||
variant_dir = tmp_path / "noheader"
|
||||
variant_dir.mkdir()
|
||||
assert parse_variant_pins(variant_dir) == {}
|
||||
|
||||
|
||||
def test_parse_unmapped_defines_ignored(tmp_path: Path) -> None:
|
||||
variant_dir = tmp_path / "custom"
|
||||
variant_dir.mkdir()
|
||||
(variant_dir / "pins_arduino.h").write_text(
|
||||
"#define PIN_NEOPIXEL (16u)\n#define PIN_LED (25u)\n"
|
||||
)
|
||||
|
||||
pins = parse_variant_pins(variant_dir)
|
||||
assert "NEOPIXEL" not in pins
|
||||
assert pins["LED"] == 25
|
||||
|
||||
|
||||
def test_load_basic_board(arduino_pico: Path) -> None:
|
||||
_add_board(
|
||||
arduino_pico,
|
||||
"rpipico",
|
||||
vendor="Raspberry Pi",
|
||||
name="Pico",
|
||||
pins_header=PICO_PINS_HEADER,
|
||||
)
|
||||
|
||||
board_pins, boards = load_boards(arduino_pico)
|
||||
|
||||
assert "rpipico" in boards
|
||||
assert boards["rpipico"]["name"] == "Raspberry Pi Pico"
|
||||
assert boards["rpipico"]["mcu"] == "rp2040"
|
||||
assert boards["rpipico"]["max_pin"] == 29
|
||||
|
||||
assert "rpipico" in board_pins
|
||||
assert board_pins["rpipico"]["LED"] == 25
|
||||
assert board_pins["rpipico"]["SDA"] == 4
|
||||
|
||||
|
||||
def test_load_rp2350_board(arduino_pico: Path) -> None:
|
||||
_add_board(
|
||||
arduino_pico,
|
||||
"rpipico2",
|
||||
mcu="rp2350",
|
||||
vendor="Raspberry Pi",
|
||||
name="Pico 2",
|
||||
pins_header=PICO_PINS_HEADER,
|
||||
)
|
||||
|
||||
_, boards = load_boards(arduino_pico)
|
||||
|
||||
assert boards["rpipico2"]["mcu"] == "rp2350"
|
||||
assert boards["rpipico2"]["max_pin"] == 47
|
||||
|
||||
|
||||
def test_cyw43_board_has_max_virtual_pin(arduino_pico: Path) -> None:
|
||||
_add_board(
|
||||
arduino_pico,
|
||||
"rpipicow",
|
||||
vendor="Raspberry Pi",
|
||||
name="Pico W",
|
||||
pins_header=PICOW_PINS_HEADER,
|
||||
)
|
||||
|
||||
_, boards = load_boards(arduino_pico)
|
||||
|
||||
assert boards["rpipicow"]["max_virtual_pin"] == 64
|
||||
|
||||
|
||||
def test_non_cyw43_board_has_no_max_virtual_pin(arduino_pico: Path) -> None:
|
||||
_add_board(
|
||||
arduino_pico,
|
||||
"rpipico",
|
||||
vendor="Raspberry Pi",
|
||||
name="Pico",
|
||||
pins_header=PICO_PINS_HEADER,
|
||||
)
|
||||
|
||||
_, boards = load_boards(arduino_pico)
|
||||
|
||||
assert "max_virtual_pin" not in boards["rpipico"]
|
||||
|
||||
|
||||
def test_board_without_variant_header(arduino_pico: Path) -> None:
|
||||
_add_board(arduino_pico, "novariant", name="No Variant")
|
||||
|
||||
board_pins, boards = load_boards(arduino_pico)
|
||||
|
||||
assert "novariant" in boards
|
||||
assert "novariant" not in board_pins
|
||||
|
||||
|
||||
def test_shared_variant_deduplicates(arduino_pico: Path) -> None:
|
||||
"""Two boards sharing the same variant should alias."""
|
||||
_add_board(arduino_pico, "base_board", pins_header=PICO_PINS_HEADER)
|
||||
_add_board(arduino_pico, "alias_board", variant="base_board")
|
||||
|
||||
board_pins, _ = load_boards(arduino_pico)
|
||||
|
||||
assert board_pins["base_board"] == parse_variant_pins(
|
||||
arduino_pico / "variants" / "base_board"
|
||||
)
|
||||
assert board_pins["alias_board"] == "base_board"
|
||||
|
||||
|
||||
def test_display_name_with_vendor(arduino_pico: Path) -> None:
|
||||
_add_board(arduino_pico, "testboard", vendor="Acme", name="Widget")
|
||||
_, boards = load_boards(arduino_pico)
|
||||
assert boards["testboard"]["name"] == "Acme Widget"
|
||||
|
||||
|
||||
def test_display_name_without_vendor(arduino_pico: Path) -> None:
|
||||
_add_board(arduino_pico, "testboard", vendor="", name="Widget")
|
||||
_, boards = load_boards(arduino_pico)
|
||||
assert boards["testboard"]["name"] == "Widget"
|
||||
|
||||
|
||||
def test_unknown_mcu_gets_default_max_pin(arduino_pico: Path) -> None:
|
||||
_add_board(arduino_pico, "future", mcu="rp2450", pins_header=PICO_PINS_HEADER)
|
||||
_, boards = load_boards(arduino_pico)
|
||||
assert boards["future"]["max_pin"] == 29
|
||||
|
||||
|
||||
def test_placeholder_pins_filtered_out(arduino_pico: Path) -> None:
|
||||
"""Pins with placeholder values like 99 should be filtered out."""
|
||||
header = textwrap.dedent("""\
|
||||
#pragma once
|
||||
#define PIN_LED (25u)
|
||||
#define PIN_WIRE0_SDA (4u)
|
||||
#define PIN_WIRE0_SCL (5u)
|
||||
#define PIN_WIRE1_SDA (99u)
|
||||
#define PIN_WIRE1_SCL (99u)
|
||||
""")
|
||||
_add_board(arduino_pico, "placeholder", pins_header=header)
|
||||
|
||||
board_pins, boards = load_boards(arduino_pico)
|
||||
|
||||
assert "SDA1" not in board_pins["placeholder"]
|
||||
assert "SCL1" not in board_pins["placeholder"]
|
||||
assert board_pins["placeholder"]["LED"] == 25
|
||||
assert "max_virtual_pin" not in boards["placeholder"]
|
||||
|
||||
|
||||
def test_placeholder_pins_not_treated_as_virtual(arduino_pico: Path) -> None:
|
||||
"""Pin 99 should not cause max_virtual_pin to be set."""
|
||||
header = textwrap.dedent("""\
|
||||
#pragma once
|
||||
#define PIN_LED (64u)
|
||||
#define PIN_WIRE0_SDA (4u)
|
||||
#define PIN_WIRE0_SCL (5u)
|
||||
#define PIN_SPI0_MISO (99u)
|
||||
""")
|
||||
_add_board(arduino_pico, "badpin", pins_header=header)
|
||||
|
||||
board_pins, boards = load_boards(arduino_pico)
|
||||
|
||||
assert "MISO" not in board_pins["badpin"]
|
||||
assert boards["badpin"]["max_virtual_pin"] == 64
|
||||
@@ -23,6 +23,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, config
|
||||
from esphome.core.config import (
|
||||
Area,
|
||||
make_app_name_cpp,
|
||||
preload_core_config,
|
||||
valid_include,
|
||||
valid_project_name,
|
||||
@@ -969,3 +970,79 @@ def test_config_hash_different_for_different_configs() -> None:
|
||||
hash2 = CORE.config_hash
|
||||
|
||||
assert hash1 != hash2
|
||||
|
||||
|
||||
def test_make_app_name_cpp_no_mac_simple() -> None:
|
||||
"""Test simple name without MAC suffix returns string literal."""
|
||||
cpp_expr, global_decl, byte_len = make_app_name_cpp(
|
||||
"my-device", "buf", "-", add_mac_suffix=False
|
||||
)
|
||||
assert cpp_expr == '"my-device"'
|
||||
assert global_decl is None
|
||||
assert byte_len == 9
|
||||
|
||||
|
||||
def test_make_app_name_cpp_no_mac_empty() -> None:
|
||||
"""Test empty name without MAC suffix."""
|
||||
cpp_expr, global_decl, byte_len = make_app_name_cpp(
|
||||
"", "buf", "-", add_mac_suffix=False
|
||||
)
|
||||
assert cpp_expr == '""'
|
||||
assert global_decl is None
|
||||
assert byte_len == 0
|
||||
|
||||
|
||||
def test_make_app_name_cpp_mac_suffix() -> None:
|
||||
"""Test name with MAC suffix emits static buffer."""
|
||||
cpp_expr, global_decl, byte_len = make_app_name_cpp(
|
||||
"my-device", "esphome_app_name_buf", "-", add_mac_suffix=True
|
||||
)
|
||||
assert cpp_expr == "esphome_app_name_buf"
|
||||
assert global_decl is not None
|
||||
assert "static char esphome_app_name_buf[]" in global_decl
|
||||
assert "my-device-XXXXXX" in global_decl
|
||||
assert byte_len == len("my-device-XXXXXX")
|
||||
|
||||
|
||||
def test_make_app_name_cpp_mac_suffix_empty() -> None:
|
||||
"""Test empty name with MAC suffix emits empty static buffer."""
|
||||
cpp_expr, global_decl, byte_len = make_app_name_cpp(
|
||||
"", "esphome_app_name_buf", "-", add_mac_suffix=True
|
||||
)
|
||||
assert cpp_expr == "esphome_app_name_buf"
|
||||
assert global_decl is not None
|
||||
assert "static char esphome_app_name_buf[]" in global_decl
|
||||
assert byte_len == 0
|
||||
|
||||
|
||||
def test_make_app_name_cpp_mac_suffix_space_sep() -> None:
|
||||
"""Test friendly name uses space separator for MAC suffix."""
|
||||
cpp_expr, global_decl, byte_len = make_app_name_cpp(
|
||||
"My Device", "esphome_app_friendly_name_buf", " ", add_mac_suffix=True
|
||||
)
|
||||
assert cpp_expr == "esphome_app_friendly_name_buf"
|
||||
assert global_decl is not None
|
||||
assert "My Device XXXXXX" in global_decl
|
||||
assert byte_len == len("My Device XXXXXX")
|
||||
|
||||
|
||||
def test_make_app_name_cpp_non_ascii_utf8_length() -> None:
|
||||
"""Test non-ASCII characters use UTF-8 byte length."""
|
||||
_, global_decl, byte_len = make_app_name_cpp(
|
||||
"café", "buf", "-", add_mac_suffix=False
|
||||
)
|
||||
assert byte_len == len("café".encode()) # 5 bytes, not 4 chars
|
||||
assert global_decl is None
|
||||
|
||||
|
||||
def test_make_app_name_cpp_non_ascii_mac_suffix_utf8_length() -> None:
|
||||
"""Test non-ASCII with MAC suffix uses UTF-8 byte length."""
|
||||
_, _, byte_len = make_app_name_cpp("café", "buf", "-", add_mac_suffix=True)
|
||||
assert byte_len == len("café-XXXXXX".encode())
|
||||
|
||||
|
||||
def test_make_app_name_cpp_special_chars_escaped() -> None:
|
||||
"""Test special characters are properly escaped in C++ string."""
|
||||
cpp_expr, _, _ = make_app_name_cpp('my "device"', "buf", "-", add_mac_suffix=False)
|
||||
# cpp_string_escape uses octal escapes for quotes
|
||||
assert '"' not in cpp_expr[1:-1] # no unescaped quotes inside the outer quotes
|
||||
|
||||
@@ -23,6 +23,7 @@ from esphome.core.entity_helpers import (
|
||||
_setup_entity_impl,
|
||||
entity_duplicate_validator,
|
||||
get_base_entity_object_id,
|
||||
register_icon,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
@@ -909,6 +910,22 @@ def test_register_string_overflow() -> None:
|
||||
_register_string("overflow", category, 3, "test")
|
||||
|
||||
|
||||
def test_register_icon_max_length() -> None:
|
||||
"""Test register_icon rejects icons exceeding 63 characters."""
|
||||
# 63 chars should succeed
|
||||
max_icon = "mdi:" + "a" * 59 # 63 total
|
||||
idx = register_icon(max_icon)
|
||||
assert idx > 0
|
||||
|
||||
# 64 chars should fail
|
||||
too_long = "mdi:" + "a" * 60 # 64 total
|
||||
with pytest.raises(ValueError, match="Icon string too long"):
|
||||
register_icon(too_long)
|
||||
|
||||
# Empty string returns 0
|
||||
assert register_icon("") == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_setup_entity_with_entity_category(
|
||||
setup_test_environment: list[str],
|
||||
|
||||
@@ -148,6 +148,18 @@ def test_icon__invalid():
|
||||
config_validation.icon("foo")
|
||||
|
||||
|
||||
def test_icon__max_length():
|
||||
"""Test that icons exceeding 63 characters are rejected."""
|
||||
# Exactly 63 chars should pass
|
||||
max_icon = "mdi:" + "a" * 59 # 63 chars total
|
||||
assert config_validation.icon(max_icon) == max_icon
|
||||
|
||||
# 64 chars should fail
|
||||
too_long = "mdi:" + "a" * 60 # 64 chars total
|
||||
with pytest.raises(Invalid, match="Icon string is too long"):
|
||||
config_validation.icon(too_long)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ("True", "YES", "on", "enAblE", True))
|
||||
def test_boolean__valid_true(value):
|
||||
assert config_validation.boolean(value) is True
|
||||
|
||||
Reference in New Issue
Block a user