from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import ( CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, CONF_ON_IDLE, CONF_ON_STATE, CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_VOLUME, ) from esphome.core import CORE from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.coroutine import CoroPriority, coroutine_with_priority from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@jesserockz"] IS_PLATFORM_COMPONENT = True media_player_ns = cg.esphome_ns.namespace("media_player") MediaPlayer = media_player_ns.class_("MediaPlayer") MediaPlayerSupportedFormat = media_player_ns.struct("MediaPlayerSupportedFormat") MediaPlayerFormatPurpose = media_player_ns.enum( "MediaPlayerFormatPurpose", is_class=True ) MEDIA_PLAYER_FORMAT_PURPOSE_ENUM = { "default": MediaPlayerFormatPurpose.PURPOSE_DEFAULT, "announcement": MediaPlayerFormatPurpose.PURPOSE_ANNOUNCEMENT, } # 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" # 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", ] StateAnyForwarder = media_player_ns.class_("StateAnyForwarder") StateEnterForwarder = media_player_ns.class_("StateEnterForwarder") MediaPlayerState = media_player_ns.enum("MediaPlayerState") # State triggers: (config_key, state enum or None for any-state) _STATE_TRIGGERS = ( (CONF_ON_STATE, None), (CONF_ON_IDLE, MediaPlayerState.MEDIA_PLAYER_STATE_IDLE), (CONF_ON_PLAY, MediaPlayerState.MEDIA_PLAYER_STATE_PLAYING), (CONF_ON_PAUSE, MediaPlayerState.MEDIA_PLAYER_STATE_PAUSED), (CONF_ON_ANNOUNCEMENT, MediaPlayerState.MEDIA_PLAYER_STATE_ANNOUNCING), (CONF_ON_TURN_ON, MediaPlayerState.MEDIA_PLAYER_STATE_ON), (CONF_ON_TURN_OFF, MediaPlayerState.MEDIA_PLAYER_STATE_OFF), ) # 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) ) EnqueueMediaAction = media_player_ns.class_( "EnqueueMediaAction", automation.Action, cg.Parented.template(MediaPlayer) ) VolumeSetAction = media_player_ns.class_( "VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer) ) _CALLBACK_AUTOMATIONS = ( automation.CallbackAutomation( CONF_ON_STATE, "add_on_state_callback", forwarder=StateAnyForwarder ), automation.CallbackAutomation( CONF_ON_IDLE, "add_on_state_callback", forwarder=StateEnterForwarder.template( MediaPlayerState.MEDIA_PLAYER_STATE_IDLE ), ), automation.CallbackAutomation( CONF_ON_PLAY, "add_on_state_callback", forwarder=StateEnterForwarder.template( MediaPlayerState.MEDIA_PLAYER_STATE_PLAYING ), ), automation.CallbackAutomation( CONF_ON_PAUSE, "add_on_state_callback", forwarder=StateEnterForwarder.template( MediaPlayerState.MEDIA_PLAYER_STATE_PAUSED ), ), automation.CallbackAutomation( CONF_ON_ANNOUNCEMENT, "add_on_state_callback", forwarder=StateEnterForwarder.template( MediaPlayerState.MEDIA_PLAYER_STATE_ANNOUNCING ), ), automation.CallbackAutomation( CONF_ON_TURN_ON, "add_on_state_callback", forwarder=StateEnterForwarder.template(MediaPlayerState.MEDIA_PLAYER_STATE_ON), ), automation.CallbackAutomation( CONF_ON_TURN_OFF, "add_on_state_callback", forwarder=StateEnterForwarder.template(MediaPlayerState.MEDIA_PLAYER_STATE_OFF), ), ) @setup_entity("media_player") async def setup_media_player_core_(var, config): await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) async def register_media_player(var, config): if not CORE.has_id(config[CONF_ID]): var = cg.Pvariable(config[CONF_ID], var) cg.add(cg.App.register_media_player(var)) CORE.register_platform_component("media_player", var) await setup_media_player_core_(var, config) async def new_media_player(config, *args): var = cg.new_Pvariable(config[CONF_ID], *args) await register_media_player(var, config) return var _MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( { cv.Optional(conf_key): automation.validate_automation({}) for conf_key, _ in _STATE_TRIGGERS } ) _MEDIA_PLAYER_SCHEMA.add_extra(entity_duplicate_validator("media_player")) def media_player_schema( class_: MockObjClass, *, entity_category: str = cv.UNDEFINED, icon: str = cv.UNDEFINED, ) -> cv.Schema: schema = {cv.GenerateID(CONF_ID): cv.declare_id(class_)} for key, default, validator in [ (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), (CONF_ICON, icon, cv.icon), ]: if default is not cv.UNDEFINED: schema[cv.Optional(key, default=default)] = validator return _MEDIA_PLAYER_SCHEMA.extend(schema) MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id( cv.Schema( { cv.GenerateID(): cv.use_id(MediaPlayer), cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean), } ) ) MEDIA_PLAYER_CONDITION_SCHEMA = automation.maybe_simple_id( {cv.GenerateID(): cv.use_id(MediaPlayer)} ) _MEDIA_URL_ACTION_SCHEMA = cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(MediaPlayer), cv.Required(CONF_MEDIA_URL): cv.templatable(cv.url), cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean), }, key=CONF_MEDIA_URL, ) 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 automation.register_action( "media_player.play_media", PlayMediaAction, _MEDIA_URL_ACTION_SCHEMA, synchronous=True, )(_media_action_handler) automation.register_action( "media_player.enqueue", EnqueueMediaAction, _MEDIA_URL_ACTION_SCHEMA, synchronous=True, )(_media_action_handler) 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) 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( "media_player.volume_set", VolumeSetAction, cv.maybe_simple_value( { cv.GenerateID(): cv.use_id(MediaPlayer), cv.Required(CONF_VOLUME): cv.templatable(cv.percentage), }, key=CONF_VOLUME, ), synchronous=True, ) 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) async def to_code(config): cg.add_global(media_player_ns.using) cg.add_define("USE_MEDIA_PLAYER")