From aaed14d0180b69af0c93b872688b38cd3ad9ed50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 23 Sep 2026 23:22:20 +0100 Subject: [PATCH] [media_player] Use register_apply_action for the media player actions (#19514) --- esphome/components/media_player/__init__.py | 96 +++++++------------ esphome/components/media_player/automation.h | 70 -------------- tests/components/media_player/common.yaml | 22 +++-- .../media_player/test.esp32-idf.yaml | 9 ++ 4 files changed, 58 insertions(+), 139 deletions(-) create mode 100644 tests/components/media_player/test.esp32-idf.yaml diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index aa1e88dca9..ed3eaf4853 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -152,7 +152,7 @@ CONF_ON_PAUSE = "on_pause" CONF_ON_ANNOUNCEMENT = "on_announcement" CONF_MEDIA_URL = "media_url" -# Command actions that all share the same schema and codegen handler +# Command actions that all share the same schema and only differ in the command sent _COMMAND_ACTIONS = [ "play", "pause", @@ -201,16 +201,7 @@ _STATE_CONDITIONS = [ "muted", ] -# Special action classes with custom schemas/handlers -PlayMediaAction = media_player_ns.class_( - "PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer) -) -EnqueueMediaAction = media_player_ns.class_( - "EnqueueMediaAction", automation.Action, cg.Parented.template(MediaPlayer) -) -VolumeSetAction = media_player_ns.class_( - "VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer) -) +MediaPlayerCommand = media_player_ns.enum("MediaPlayerCommand", is_class=True) _CALLBACK_AUTOMATIONS = ( @@ -329,59 +320,50 @@ _MEDIA_URL_ACTION_SCHEMA = cv.maybe_simple_value( ) -async def _media_action_handler(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - media_url = await cg.templatable(config[CONF_MEDIA_URL], args, cg.std_string) - announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_) - cg.add(var.set_media_url(media_url)) - cg.add(var.set_announcement(announcement)) - return var +_ANNOUNCEMENT_FIELD = automation.ApplyField( + CONF_ANNOUNCEMENT, "set_announcement", cg.bool_ +) +_MEDIA_URL_FIELD = automation.ApplyField(CONF_MEDIA_URL, "set_media_url", cg.std_string) -automation.register_action( +def _set_command(command_name: str) -> automation.ApplyCall: + command = getattr( + MediaPlayerCommand, f"MEDIA_PLAYER_COMMAND_{command_name.upper()}" + ) + return automation.ApplyCall(f"set_command({command})") + + +automation.register_apply_action( "media_player.play_media", - PlayMediaAction, _MEDIA_URL_ACTION_SCHEMA, - synchronous=True, -)(_media_action_handler) + _MEDIA_URL_FIELD, + _ANNOUNCEMENT_FIELD, + call="make_call", +) -automation.register_action( +automation.register_apply_action( "media_player.enqueue", - EnqueueMediaAction, _MEDIA_URL_ACTION_SCHEMA, - synchronous=True, -)(_media_action_handler) + _set_command("enqueue"), + _MEDIA_URL_FIELD, + _ANNOUNCEMENT_FIELD, + call="make_call", +) + +for _action_name in _COMMAND_ACTIONS: + automation.register_apply_action( + f"media_player.{_action_name}", + MEDIA_PLAYER_ACTION_SCHEMA, + _set_command(_action_name), + _ANNOUNCEMENT_FIELD, + call="make_call", + ) def _snake_to_camel(name): return "".join(word.capitalize() for word in name.split("_")) -def _register_command_actions(): - async def handler(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_) - cg.add(var.set_announcement(announcement)) - return var - - for action_name in _COMMAND_ACTIONS: - class_name = f"{_snake_to_camel(action_name)}Action" - action_class = media_player_ns.class_( - class_name, automation.Action, cg.Parented.template(MediaPlayer) - ) - automation.register_action( - f"media_player.{action_name}", - action_class, - MEDIA_PLAYER_ACTION_SCHEMA, - synchronous=True, - )(handler) - - -_register_command_actions() - - def _register_state_conditions(): async def handler(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) @@ -401,9 +383,8 @@ def _register_state_conditions(): _register_state_conditions() -@automation.register_action( +automation.register_apply_action( "media_player.volume_set", - VolumeSetAction, cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(MediaPlayer), @@ -411,14 +392,9 @@ _register_state_conditions() }, key=CONF_VOLUME, ), - synchronous=True, + automation.ApplyField(CONF_VOLUME, "set_volume", cg.float_), + call="make_call", ) -async def media_player_volume_set_action(config, action_id, template_arg, args): - var = cg.new_Pvariable(action_id, template_arg) - await cg.register_parented(var, config[CONF_ID]) - volume = await cg.templatable(config[CONF_VOLUME], args, cg.float_) - cg.add(var.set_volume(volume)) - return var @coroutine_with_priority(CoroPriority.CORE) diff --git a/esphome/components/media_player/automation.h b/esphome/components/media_player/automation.h index 899acfefdf..648e9eb29f 100644 --- a/esphome/components/media_player/automation.h +++ b/esphome/components/media_player/automation.h @@ -5,76 +5,6 @@ namespace esphome::media_player { -template -class MediaPlayerCommandAction final : public Action, public Parented { - public: - TEMPLATABLE_VALUE(bool, announcement); - void play(const Ts &...x) override { - this->parent_->make_call().set_command(Command).set_announcement(this->announcement_.value(x...)).perform(); - } -}; - -template -using PlayAction = MediaPlayerCommandAction; -template -using PauseAction = MediaPlayerCommandAction; -template -using StopAction = MediaPlayerCommandAction; -template -using ToggleAction = MediaPlayerCommandAction; -template -using VolumeUpAction = MediaPlayerCommandAction; -template -using VolumeDownAction = MediaPlayerCommandAction; -template -using TurnOnAction = MediaPlayerCommandAction; -template -using TurnOffAction = MediaPlayerCommandAction; -template -using NextAction = MediaPlayerCommandAction; -template -using PreviousAction = MediaPlayerCommandAction; -template -using MuteAction = MediaPlayerCommandAction; -template -using UnmuteAction = MediaPlayerCommandAction; -template -using RepeatOffAction = MediaPlayerCommandAction; -template -using RepeatOneAction = MediaPlayerCommandAction; -template -using RepeatAllAction = MediaPlayerCommandAction; -template -using ShuffleAction = MediaPlayerCommandAction; -template -using UnshuffleAction = MediaPlayerCommandAction; -template -using GroupJoinAction = MediaPlayerCommandAction; -template -using ClearPlaylistAction = MediaPlayerCommandAction; - -template -class MediaPlayerMediaAction final : public Action, public Parented { - TEMPLATABLE_VALUE(std::string, media_url) - TEMPLATABLE_VALUE(bool, announcement) - void play(const Ts &...x) override { - auto call = this->parent_->make_call(); - if constexpr (Command != MediaPlayerCommand::MEDIA_PLAYER_COMMAND_PLAY) - call.set_command(Command); - call.set_media_url(this->media_url_.value(x...)).set_announcement(this->announcement_.value(x...)).perform(); - } -}; - -template -using PlayMediaAction = MediaPlayerMediaAction; -template -using EnqueueMediaAction = MediaPlayerMediaAction; - -template class VolumeSetAction final : public Action, public Parented { - TEMPLATABLE_VALUE(float, volume) - void play(const Ts &...x) override { this->parent_->make_call().set_volume(this->volume_.value(x...)).perform(); } -}; - /// Callback forwarder that triggers an Automation<> on any state change. /// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_. struct StateAnyForwarder { diff --git a/tests/components/media_player/common.yaml b/tests/components/media_player/common.yaml index 88d04d0ff0..dd6773864d 100644 --- a/tests/components/media_player/common.yaml +++ b/tests/components/media_player/common.yaml @@ -1,28 +1,30 @@ -i2s_audio: - i2s_lrclk_pin: 13 - i2s_bclk_pin: 14 - i2s_mclk_pin: 15 - speaker: - platform: i2s_audio id: test_speaker - i2s_dout_pin: 18 + i2s_audio_id: i2s_audio_bus + i2s_dout_pin: ${i2s_dout_pin} dac_type: external media_player: - platform: speaker + id: test_media_player name: None - speaker: test_speaker + announcement_pipeline: + speaker: test_speaker + format: NONE on_state: - media_player.play: - media_player.play_media: http://localhost/media.mp3 - - media_player.play_media: !lambda 'return "http://localhost/media.mp3";' + - media_player.play_media: !lambda |- + return id(test_media_player).is_muted() ? "http://localhost/muted.mp3" : "http://localhost/media.mp3"; on_idle: - media_player.pause: on_play: - media_player.stop: - media_player.stop: announcement: true + - media_player.stop: + announcement: !lambda 'return id(test_media_player).is_muted();' on_announcement: - media_player.play: on_turn_on: @@ -61,8 +63,10 @@ media_player: - media_player.volume_up: - media_player.volume_down: - media_player.volume_set: 50% + - media_player.volume_set: !lambda 'return id(test_media_player).volume / 2;' - media_player.enqueue: http://localhost/media.mp3 - - media_player.enqueue: !lambda 'return "http://localhost/media.mp3";' + - media_player.enqueue: !lambda |- + return id(test_media_player).is_muted() ? "http://localhost/muted.mp3" : "http://localhost/media.mp3"; - media_player.enqueue: media_url: http://localhost/media.mp3 announcement: true diff --git a/tests/components/media_player/test.esp32-idf.yaml b/tests/components/media_player/test.esp32-idf.yaml new file mode 100644 index 0000000000..3e55bfb742 --- /dev/null +++ b/tests/components/media_player/test.esp32-idf.yaml @@ -0,0 +1,9 @@ +substitutions: + i2s_dout_pin: GPIO18 + +packages: + i2s_audio: !include ../../test_build_components/common/i2s_audio/esp32-idf.yaml + media_player: !include common.yaml + +wifi: + ap: