124 lines
4.3 KiB
Python
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
|