Files
esphome-lsm6ds/esphome/components/router/speaker/__init__.py
T

124 lines
4.3 KiB
Python

from esphome import automation, core
import esphome.codegen as cg
from esphome.components import audio, speaker
import esphome.config_validation as cv
from esphome.const import (
CONF_BITS_PER_SAMPLE,
CONF_ID,
CONF_NUM_CHANNELS,
CONF_OUTPUT_SPEAKER,
CONF_SAMPLE_RATE,
)
from esphome.core import ID
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType, TemplateArgsType
CODEOWNERS = ["@kahrendt"]
CONF_OUTPUT_SPEAKERS = "output_speakers"
CONF_TARGET_SPEAKER = "target_speaker"
router_ns = cg.esphome_ns.namespace("router")
Router = router_ns.class_("Router", cg.Component, speaker.Speaker)
SwitchOutputAction = router_ns.class_("SwitchOutputAction", automation.Action)
SpeakerPtr = speaker.Speaker.operator("ptr")
def _set_stream_limits(config: ConfigType) -> ConfigType:
# Lock the router's stream limits to the user-declared format. Limits are set
# at CONFIG_SCHEMA time so they're visible to other components' FINAL_VALIDATE
# (which has no guaranteed ordering vs. ours).
audio.set_stream_limits(
min_bits_per_sample=config[CONF_BITS_PER_SAMPLE],
max_bits_per_sample=config[CONF_BITS_PER_SAMPLE],
min_channels=config[CONF_NUM_CHANNELS],
max_channels=config[CONF_NUM_CHANNELS],
min_sample_rate=config[CONF_SAMPLE_RATE],
max_sample_rate=config[CONF_SAMPLE_RATE],
)(config)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(Router),
cv.Required(CONF_OUTPUT_SPEAKERS): cv.All(
cv.ensure_list(cv.use_id(speaker.Speaker)),
cv.Length(min=2, max=8),
),
# All outputs must agree on a single format so the producer can keep
# streaming through a switch without reconfiguring. These are required
# rather than inherited because downstream components (e.g. mixer)
# read them from the router's declaration during FINAL_VALIDATE,
# which can't depend on our FINAL_VALIDATE running first.
cv.Required(CONF_BITS_PER_SAMPLE): cv.int_range(8, 32),
cv.Required(CONF_NUM_CHANNELS): cv.int_range(1, 2),
cv.Required(CONF_SAMPLE_RATE): cv.int_range(8000, 96000),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on_esp32,
_set_stream_limits,
)
def _final_validate(config: ConfigType) -> ConfigType:
# Validate every configured output speaker can accept the router's format.
# Switching to an output that can't reproduce the format the producer is
# already sending would otherwise fail silently at runtime.
for spk_id in config[CONF_OUTPUT_SPEAKERS]:
proxy = {**config, CONF_OUTPUT_SPEAKER: spk_id}
audio.final_validate_audio_schema(
"router",
audio_device=CONF_OUTPUT_SPEAKER,
bits_per_sample=config[CONF_BITS_PER_SAMPLE],
channels=config[CONF_NUM_CHANNELS],
sample_rate=config[CONF_SAMPLE_RATE],
)(proxy)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# The first configured output is the default active output on boot.
speakers = config[CONF_OUTPUT_SPEAKERS]
cg.add(var.set_output_count(len(speakers)))
for spk_id in speakers:
spk = await cg.get_variable(spk_id)
cg.add(var.add_output(spk))
@automation.register_action(
"router.speaker.switch_output",
SwitchOutputAction,
cv.Schema(
{
cv.GenerateID(CONF_ID): cv.use_id(Router),
cv.Required(CONF_TARGET_SPEAKER): cv.templatable(
cv.use_id(speaker.Speaker)
),
}
),
synchronous=True,
)
async def switch_output_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
parent = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, parent)
target = config[CONF_TARGET_SPEAKER]
if not isinstance(target, core.Lambda):
target = await cg.get_variable(target)
template_ = await cg.templatable(target, args, SpeakerPtr)
cg.add(var.set_target(template_))
return var