[media_player] Add more commands to support Sendspin (#12258)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
This commit is contained in:
Kevin Ahrendt
2026-02-18 21:51:01 -06:00
committed by GitHub
parent 3c227eeca4
commit 264c8faedd
5 changed files with 232 additions and 168 deletions

View File

@@ -35,86 +35,73 @@ MEDIA_PLAYER_FORMAT_PURPOSE_ENUM = {
"announcement": MediaPlayerFormatPurpose.PURPOSE_ANNOUNCEMENT,
}
PlayAction = media_player_ns.class_(
"PlayAction", automation.Action, cg.Parented.template(MediaPlayer)
)
PlayMediaAction = media_player_ns.class_(
"PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer)
)
ToggleAction = media_player_ns.class_(
"ToggleAction", automation.Action, cg.Parented.template(MediaPlayer)
)
PauseAction = media_player_ns.class_(
"PauseAction", automation.Action, cg.Parented.template(MediaPlayer)
)
StopAction = media_player_ns.class_(
"StopAction", automation.Action, cg.Parented.template(MediaPlayer)
)
VolumeUpAction = media_player_ns.class_(
"VolumeUpAction", automation.Action, cg.Parented.template(MediaPlayer)
)
VolumeDownAction = media_player_ns.class_(
"VolumeDownAction", automation.Action, cg.Parented.template(MediaPlayer)
)
VolumeSetAction = media_player_ns.class_(
"VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer)
)
TurnOnAction = media_player_ns.class_(
"TurnOnAction", automation.Action, cg.Parented.template(MediaPlayer)
)
TurnOffAction = media_player_ns.class_(
"TurnOffAction", automation.Action, cg.Parented.template(MediaPlayer)
)
# Local config key constants
CONF_ANNOUNCEMENT = "announcement"
CONF_ON_PLAY = "on_play"
CONF_ON_PAUSE = "on_pause"
CONF_ON_ANNOUNCEMENT = "on_announcement"
CONF_MEDIA_URL = "media_url"
StateTrigger = media_player_ns.class_("StateTrigger", automation.Trigger.template())
IdleTrigger = media_player_ns.class_("IdleTrigger", automation.Trigger.template())
PlayTrigger = media_player_ns.class_("PlayTrigger", automation.Trigger.template())
PauseTrigger = media_player_ns.class_("PauseTrigger", automation.Trigger.template())
AnnoucementTrigger = media_player_ns.class_(
"AnnouncementTrigger", automation.Trigger.template()
# Command actions that all share the same schema and codegen handler
_COMMAND_ACTIONS = [
"play",
"pause",
"stop",
"toggle",
"volume_up",
"volume_down",
"turn_on",
"turn_off",
"next",
"previous",
"mute",
"unmute",
"repeat_off",
"repeat_one",
"repeat_all",
"shuffle",
"unshuffle",
"group_join",
"clear_playlist",
]
# State triggers: (config_key, C++ class name)
_STATE_TRIGGERS = [
(CONF_ON_STATE, "StateTrigger"),
(CONF_ON_IDLE, "IdleTrigger"),
(CONF_ON_PLAY, "PlayTrigger"),
(CONF_ON_PAUSE, "PauseTrigger"),
(CONF_ON_ANNOUNCEMENT, "AnnouncementTrigger"),
(CONF_ON_TURN_ON, "OnTrigger"),
(CONF_ON_TURN_OFF, "OffTrigger"),
]
# State conditions that all share the same schema and codegen handler
_STATE_CONDITIONS = [
"idle",
"paused",
"playing",
"announcing",
"on",
"off",
"muted",
]
# Special action classes with custom schemas/handlers
PlayMediaAction = media_player_ns.class_(
"PlayMediaAction", automation.Action, cg.Parented.template(MediaPlayer)
)
OnTrigger = media_player_ns.class_("OnTrigger", automation.Trigger.template())
OffTrigger = media_player_ns.class_("OffTrigger", automation.Trigger.template())
IsIdleCondition = media_player_ns.class_("IsIdleCondition", automation.Condition)
IsPausedCondition = media_player_ns.class_("IsPausedCondition", automation.Condition)
IsPlayingCondition = media_player_ns.class_("IsPlayingCondition", automation.Condition)
IsAnnouncingCondition = media_player_ns.class_(
"IsAnnouncingCondition", automation.Condition
VolumeSetAction = media_player_ns.class_(
"VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer)
)
IsOnCondition = media_player_ns.class_("IsOnCondition", automation.Condition)
IsOffCondition = media_player_ns.class_("IsOffCondition", automation.Condition)
async def setup_media_player_core_(var, config):
await setup_entity(var, config, "media_player")
for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_IDLE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_PLAY, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_PAUSE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_ANNOUNCEMENT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_TURN_ON, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_TURN_OFF, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
for conf_key, _ in _STATE_TRIGGERS:
for conf in config.get(conf_key, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
async def register_media_player(var, config):
@@ -133,41 +120,14 @@ async def new_media_player(config, *args):
_MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
{
cv.Optional(CONF_ON_STATE): automation.validate_automation(
cv.Optional(conf_key): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
media_player_ns.class_(class_name, automation.Trigger.template())
),
}
),
cv.Optional(CONF_ON_IDLE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger),
}
),
cv.Optional(CONF_ON_PLAY): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PlayTrigger),
}
),
cv.Optional(CONF_ON_PAUSE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger),
}
),
cv.Optional(CONF_ON_ANNOUNCEMENT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(AnnoucementTrigger),
}
),
cv.Optional(CONF_ON_TURN_ON): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnTrigger),
}
),
cv.Optional(CONF_ON_TURN_OFF): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OffTrigger),
}
),
)
for conf_key, class_name in _STATE_TRIGGERS
}
)
@@ -228,56 +188,48 @@ async def media_player_play_media_action(config, action_id, template_arg, args):
return var
@automation.register_action("media_player.play", PlayAction, MEDIA_PLAYER_ACTION_SCHEMA)
@automation.register_action(
"media_player.toggle", ToggleAction, MEDIA_PLAYER_ACTION_SCHEMA
)
@automation.register_action(
"media_player.pause", PauseAction, MEDIA_PLAYER_ACTION_SCHEMA
)
@automation.register_action("media_player.stop", StopAction, MEDIA_PLAYER_ACTION_SCHEMA)
@automation.register_action(
"media_player.volume_up", VolumeUpAction, MEDIA_PLAYER_ACTION_SCHEMA
)
@automation.register_action(
"media_player.volume_down", VolumeDownAction, MEDIA_PLAYER_ACTION_SCHEMA
)
@automation.register_action(
"media_player.turn_on", TurnOnAction, MEDIA_PLAYER_ACTION_SCHEMA
)
@automation.register_action(
"media_player.turn_off", TurnOffAction, MEDIA_PLAYER_ACTION_SCHEMA
)
async def media_player_action(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
def _snake_to_camel(name):
return "".join(word.capitalize() for word in name.split("_"))
@automation.register_condition(
"media_player.is_idle", IsIdleCondition, MEDIA_PLAYER_CONDITION_SCHEMA
)
@automation.register_condition(
"media_player.is_paused", IsPausedCondition, MEDIA_PLAYER_CONDITION_SCHEMA
)
@automation.register_condition(
"media_player.is_playing", IsPlayingCondition, MEDIA_PLAYER_CONDITION_SCHEMA
)
@automation.register_condition(
"media_player.is_announcing", IsAnnouncingCondition, MEDIA_PLAYER_CONDITION_SCHEMA
)
@automation.register_condition(
"media_player.is_on", IsOnCondition, MEDIA_PLAYER_CONDITION_SCHEMA
)
@automation.register_condition(
"media_player.is_off", IsOffCondition, MEDIA_PLAYER_CONDITION_SCHEMA
)
async def media_player_condition(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
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
)(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)
await cg.register_parented(var, config[CONF_ID])
return var
for condition_name in _STATE_CONDITIONS:
class_name = f"Is{_snake_to_camel(condition_name)}Condition"
condition_class = media_player_ns.class_(class_name, automation.Condition)
automation.register_condition(
f"media_player.is_{condition_name}",
condition_class,
MEDIA_PLAYER_CONDITION_SCHEMA,
)(handler)
_register_state_conditions()
@automation.register_action(

View File

@@ -32,6 +32,28 @@ template<typename... Ts>
using TurnOnAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_TURN_ON, Ts...>;
template<typename... Ts>
using TurnOffAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_TURN_OFF, Ts...>;
template<typename... Ts>
using NextAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_NEXT, Ts...>;
template<typename... Ts>
using PreviousAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_PREVIOUS, Ts...>;
template<typename... Ts>
using MuteAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_MUTE, Ts...>;
template<typename... Ts>
using UnmuteAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_UNMUTE, Ts...>;
template<typename... Ts>
using RepeatOffAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF, Ts...>;
template<typename... Ts>
using RepeatOneAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE, Ts...>;
template<typename... Ts>
using RepeatAllAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ALL, Ts...>;
template<typename... Ts>
using ShuffleAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_SHUFFLE, Ts...>;
template<typename... Ts>
using UnshuffleAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_UNSHUFFLE, Ts...>;
template<typename... Ts>
using GroupJoinAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_GROUP_JOIN, Ts...>;
template<typename... Ts>
using ClearPlaylistAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST, Ts...>;
template<typename... Ts> class PlayMediaAction : public Action<Ts...>, public Parented<MediaPlayer> {
TEMPLATABLE_VALUE(std::string, media_url)
@@ -105,5 +127,10 @@ template<typename... Ts> class IsOffCondition : public Condition<Ts...>, public
bool check(const Ts &...x) override { return this->parent_->state == MediaPlayerState::MEDIA_PLAYER_STATE_OFF; }
};
template<typename... Ts> class IsMutedCondition : public Condition<Ts...>, public Parented<MediaPlayer> {
public:
bool check(const Ts &...x) override { return this->parent_->is_muted(); }
};
} // namespace media_player
} // namespace esphome

View File

@@ -60,11 +60,39 @@ const char *media_player_command_to_string(MediaPlayerCommand command) {
return "TURN_ON";
case MEDIA_PLAYER_COMMAND_TURN_OFF:
return "TURN_OFF";
case MEDIA_PLAYER_COMMAND_NEXT:
return "NEXT";
case MEDIA_PLAYER_COMMAND_PREVIOUS:
return "PREVIOUS";
case MEDIA_PLAYER_COMMAND_REPEAT_ALL:
return "REPEAT_ALL";
case MEDIA_PLAYER_COMMAND_SHUFFLE:
return "SHUFFLE";
case MEDIA_PLAYER_COMMAND_UNSHUFFLE:
return "UNSHUFFLE";
case MEDIA_PLAYER_COMMAND_GROUP_JOIN:
return "GROUP_JOIN";
default:
return "UNKNOWN";
}
}
void MediaPlayerTraits::set_supports_pause(bool supports_pause) {
if (supports_pause) {
this->feature_flags_ |= MediaPlayerEntityFeature::PAUSE | MediaPlayerEntityFeature::PLAY;
} else {
this->feature_flags_ &= ~(MediaPlayerEntityFeature::PAUSE | MediaPlayerEntityFeature::PLAY);
}
}
void MediaPlayerTraits::set_supports_turn_off_on(bool supports_turn_off_on) {
if (supports_turn_off_on) {
this->feature_flags_ |= MediaPlayerEntityFeature::TURN_OFF | MediaPlayerEntityFeature::TURN_ON;
} else {
this->feature_flags_ &= ~(MediaPlayerEntityFeature::TURN_OFF | MediaPlayerEntityFeature::TURN_ON);
}
}
void MediaPlayerCall::validate_() {
if (this->media_url_.has_value()) {
if (this->command_.has_value() && this->command_.value() != MEDIA_PLAYER_COMMAND_ENQUEUE) {
@@ -125,6 +153,30 @@ MediaPlayerCall &MediaPlayerCall::set_command(const char *command) {
this->set_command(MEDIA_PLAYER_COMMAND_TURN_ON);
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TURN_OFF")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_TURN_OFF);
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("VOLUME_UP")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_VOLUME_UP);
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("VOLUME_DOWN")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_VOLUME_DOWN);
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("ENQUEUE")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_ENQUEUE);
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("REPEAT_ONE")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_REPEAT_ONE);
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("REPEAT_OFF")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_REPEAT_OFF);
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("REPEAT_ALL")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_REPEAT_ALL);
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("CLEAR_PLAYLIST")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST);
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("NEXT")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_NEXT);
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("PREVIOUS")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_PREVIOUS);
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("SHUFFLE")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_SHUFFLE);
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("UNSHUFFLE")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_UNSHUFFLE);
} else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("GROUP_JOIN")) == 0) {
this->set_command(MEDIA_PLAYER_COMMAND_GROUP_JOIN);
} else {
ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command);
}

View File

@@ -58,6 +58,12 @@ enum MediaPlayerCommand : uint8_t {
MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST = 11,
MEDIA_PLAYER_COMMAND_TURN_ON = 12,
MEDIA_PLAYER_COMMAND_TURN_OFF = 13,
MEDIA_PLAYER_COMMAND_NEXT = 14,
MEDIA_PLAYER_COMMAND_PREVIOUS = 15,
MEDIA_PLAYER_COMMAND_REPEAT_ALL = 16,
MEDIA_PLAYER_COMMAND_SHUFFLE = 17,
MEDIA_PLAYER_COMMAND_UNSHUFFLE = 18,
MEDIA_PLAYER_COMMAND_GROUP_JOIN = 19,
};
const char *media_player_command_to_string(MediaPlayerCommand command);
@@ -74,38 +80,40 @@ struct MediaPlayerSupportedFormat {
uint32_t sample_bytes;
};
// Base features always reported for all media players
static constexpr uint32_t BASE_MEDIA_PLAYER_FEATURES =
MediaPlayerEntityFeature::PLAY_MEDIA | MediaPlayerEntityFeature::BROWSE_MEDIA | MediaPlayerEntityFeature::STOP |
MediaPlayerEntityFeature::VOLUME_SET | MediaPlayerEntityFeature::VOLUME_MUTE |
MediaPlayerEntityFeature::MEDIA_ANNOUNCE;
class MediaPlayer;
class MediaPlayerTraits {
public:
MediaPlayerTraits() = default;
void set_supports_pause(bool supports_pause) { this->supports_pause_ = supports_pause; }
bool get_supports_pause() const { return this->supports_pause_; }
void set_supports_turn_off_on(bool supports_turn_off_on) { this->supports_turn_off_on_ = supports_turn_off_on; }
bool get_supports_turn_off_on() const { return this->supports_turn_off_on_; }
uint32_t get_feature_flags() const { return this->feature_flags_; }
void add_feature_flags(uint32_t feature_flags) { this->feature_flags_ |= feature_flags; }
void clear_feature_flags(uint32_t feature_flags) { this->feature_flags_ &= ~feature_flags; }
// Returns true only if all specified flags are set
bool has_feature_flags(uint32_t feature_flags) const {
return (this->feature_flags_ & feature_flags) == feature_flags;
}
std::vector<MediaPlayerSupportedFormat> &get_supported_formats() { return this->supported_formats_; }
uint32_t get_feature_flags() const {
uint32_t flags = 0;
flags |= MediaPlayerEntityFeature::PLAY_MEDIA | MediaPlayerEntityFeature::BROWSE_MEDIA |
MediaPlayerEntityFeature::STOP | MediaPlayerEntityFeature::VOLUME_SET |
MediaPlayerEntityFeature::VOLUME_MUTE | MediaPlayerEntityFeature::MEDIA_ANNOUNCE;
if (this->get_supports_pause()) {
flags |= MediaPlayerEntityFeature::PAUSE | MediaPlayerEntityFeature::PLAY;
}
if (this->get_supports_turn_off_on()) {
flags |= MediaPlayerEntityFeature::TURN_OFF | MediaPlayerEntityFeature::TURN_ON;
}
return flags;
// Legacy setters/getters are kept for backward compatibility
void set_supports_pause(bool supports_pause);
bool get_supports_pause() const { return this->has_feature_flags(MediaPlayerEntityFeature::PAUSE); }
void set_supports_turn_off_on(bool supports_turn_off_on);
bool get_supports_turn_off_on() const {
return this->has_feature_flags(MediaPlayerEntityFeature::TURN_ON | MediaPlayerEntityFeature::TURN_OFF);
}
protected:
std::vector<MediaPlayerSupportedFormat> supported_formats_{};
bool supports_pause_{false};
bool supports_turn_off_on_{false};
uint32_t feature_flags_{BASE_MEDIA_PLAYER_FEATURES};
};
class MediaPlayerCall {

View File

@@ -23,8 +23,27 @@ media_player:
- media_player.stop:
- media_player.stop:
announcement: true
on_announcement:
- media_player.play:
on_turn_on:
- media_player.play:
on_turn_off:
- media_player.stop:
on_pause:
- media_player.toggle:
- media_player.turn_on:
- media_player.turn_off:
- media_player.next:
- media_player.previous:
- media_player.mute:
- media_player.unmute:
- media_player.repeat_off:
- media_player.repeat_one:
- media_player.repeat_all:
- media_player.shuffle:
- media_player.unshuffle:
- media_player.group_join:
- media_player.clear_playlist:
- wait_until:
media_player.is_idle:
- wait_until:
@@ -33,6 +52,12 @@ media_player:
media_player.is_announcing:
- wait_until:
media_player.is_paused:
- wait_until:
media_player.is_on:
- wait_until:
media_player.is_off:
- wait_until:
media_player.is_muted:
- media_player.volume_up:
- media_player.volume_down:
- media_player.volume_set: 50%