[speaker][i2s_audio][es8156] Allow a list of audio DACs on one speaker (#21260)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrtoy-meandClaude Opus 5.5 authored and GitHub committed 2026-10-26 10:43:03 -04:00
1 parent 26ba2cc234
commit ae127f7db8
9 files changed
+285 -28

No files matched your search

@@ -0,0 +1,82 @@
"""Tests for the es8156 bits_per_sample check on the speakers that use it."""
from __future__ import annotations
import pytest
from esphome.components.es8156.audio_dac import FINAL_VALIDATE_SCHEMA
from esphome.config import Config
import esphome.config_validation as cv
from esphome.const import (
CONF_AUDIO_DAC,
CONF_BITS_PER_SAMPLE,
CONF_ID,
PlatformFramework,
)
from esphome.core import ID
from tests.component_tests.types import SetCoreConfigCallable
ES8156_ID = "es"
def _dac_id(name: str) -> ID:
return ID(name, is_declaration=False, type="audio_dac::AudioDac")
def _full_config(audio_dac: ID | list[ID], bits_per_sample: int) -> Config:
"""A full config with one speaker using the given audio_dac value."""
full = Config()
full["speaker"] = [
{CONF_AUDIO_DAC: audio_dac, CONF_BITS_PER_SAMPLE: bits_per_sample}
]
return full
def _final_validate() -> None:
FINAL_VALIDATE_SCHEMA({CONF_ID: ID(ES8156_ID, is_declaration=True)})
def test_single_audio_dac_over_24_bits_is_rejected(
set_core_config: SetCoreConfigCallable,
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF, full_config=_full_config(_dac_id(ES8156_ID), 32)
)
with pytest.raises(cv.Invalid, match="ES8156 does not support more than 24 bits"):
_final_validate()
def test_audio_dac_list_over_24_bits_is_rejected(
set_core_config: SetCoreConfigCallable,
) -> None:
"""The check also finds the es8156 inside a list of audio DACs."""
set_core_config(
PlatformFramework.ESP32_IDF,
full_config=_full_config([_dac_id("other"), _dac_id(ES8156_ID)], 32),
)
with pytest.raises(cv.Invalid, match="ES8156 does not support more than 24 bits"):
_final_validate()
def test_audio_dac_list_at_16_bits_passes(
set_core_config: SetCoreConfigCallable,
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF,
full_config=_full_config([_dac_id("other"), _dac_id(ES8156_ID)], 16),
)
_final_validate()
def test_other_audio_dac_over_24_bits_passes(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A speaker that does not use this es8156 is not checked."""
set_core_config(
PlatformFramework.ESP32_IDF, full_config=_full_config(_dac_id("other"), 32)
)
_final_validate()
@@ -0,0 +1,57 @@
"""Tests for the speaker audio_dac option."""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome.components.speaker import SPEAKER_SCHEMA
import esphome.config_validation as cv
from esphome.const import CONF_AUDIO_DAC, PlatformFramework
from tests.component_tests.types import SetCoreConfigCallable
def test_speaker_empty_audio_dac_uses_the_only_dac(
generate_main: Callable[[str | Path], str],
) -> None:
"""An empty audio_dac: still picks the only audio DAC in the config."""
main_cpp = generate_main(
"tests/component_tests/speaker/test_speaker_audio_dac_empty.yaml"
)
assert "speaker_audio_dacs[] PROGMEM = {only_dac};" in main_cpp
assert "speaker_empty_audio_dac->set_audio_dacs(speaker_audio_dacs, 1);" in main_cpp
def test_speaker_audio_dac_list_single_and_absent(
generate_main: Callable[[str | Path], str],
) -> None:
"""A list feeds every DAC, a single id one DAC, and no audio_dac sets nothing."""
main_cpp = generate_main(
"tests/component_tests/speaker/test_speaker_audio_dac_forms.yaml"
)
assert "speaker_audio_dacs[] PROGMEM = {dac_a, dac_b};" in main_cpp
assert "speaker_list->set_audio_dacs(speaker_audio_dacs, 2);" in main_cpp
assert "speaker_audio_dacs_2[] PROGMEM = {dac_a};" in main_cpp
assert "source_single->set_audio_dacs(speaker_audio_dacs_2, 1);" in main_cpp
assert "source_absent->set_audio_dac" not in main_cpp
@pytest.mark.parametrize(
("audio_dac", "message"),
[
(["dac_a", "dac_a"], "Each audio_dac can only be listed once"),
([], "length of value must be at least 1"),
],
)
def test_speaker_audio_dac_list_rejections(
set_core_config: SetCoreConfigCallable, audio_dac: list[str], message: str
) -> None:
"""A repeated DAC and an empty list are rejected with the list's own message, not use_id's."""
set_core_config(PlatformFramework.ESP32_IDF)
with pytest.raises(cv.Invalid, match=message):
SPEAKER_SCHEMA({CONF_AUDIO_DAC: audio_dac})
@@ -0,0 +1,26 @@
esphome:
name: test
esp32:
board: esp32dev
i2c:
id: i2c_bus
sda: 21
scl: 22
i2s_audio:
- id: i2s_audio_bus
i2s_lrclk_pin: 25
i2s_bclk_pin: 26
audio_dac:
- platform: aic3204
id: only_dac
speaker:
- platform: i2s_audio
id: speaker_empty_audio_dac
dac_type: external
i2s_dout_pin: 27
audio_dac:
@@ -0,0 +1,35 @@
esphome:
name: test
esp32:
board: esp32dev
i2c:
id: i2c_bus
sda: 21
scl: 22
i2s_audio:
- id: i2s_audio_bus
i2s_lrclk_pin: 25
i2s_bclk_pin: 26
audio_dac:
- platform: aic3204
id: dac_a
- platform: aic3204
id: dac_b
address: 0x19
speaker:
- platform: i2s_audio
id: speaker_list
dac_type: external
i2s_dout_pin: 27
audio_dac: [dac_a, dac_b]
- platform: mixer
output_speaker: speaker_list
source_speakers:
- id: source_single
audio_dac: dac_a
- id: source_absent