2727from ..const import ( # noqa: TID252
2828 CC_CONVERSATION_ENDED_EVENT ,
2929 CYCLE_VIEWS ,
30+ CONF_MUSIC_MODE_AUTO ,
31+ CONF_MUSIC_MODE_TIMEOUT ,
3032 DEVICES ,
3133 DOMAIN ,
3234 ESPHOME_DOMAIN ,
@@ -94,16 +96,21 @@ def __init__(self, hass: HomeAssistant, config: VAConfigEntry) -> None:
9496 """Initialise."""
9597 self .hass = hass
9698 self .config = config
97- self .mic_integration = get_config_entry_by_entity_id (
98- hass , config .runtime_data .core .mic_device or ""
99- ).domain
99+ self .mic_integration = None
100100 self .music_player_entity = config .runtime_data .core .musicplayer_device
101101 self .music_player_volume : float = 0.0
102102 self .is_ducked : bool = False
103103 self .ducking_task : asyncio .Task | None = None
104104
105+ if mic_device := get_config_entry_by_entity_id (
106+ hass , config .runtime_data .core .mic_device
107+ ):
108+ self .mic_integration = mic_device .domain
109+
105110 def register_listeners (self ) -> None :
106111 """Register the state change listener for assist/mic status entities."""
112+ if not self .mic_integration :
113+ return
107114
108115 if self .mic_integration == HASSMIC_DOMAIN :
109116 assist_entity_id = get_hassmic_pipeline_status_entity_id (
@@ -306,14 +313,20 @@ async def do_overlay_event(self, state: str) -> None:
306313 elif state in ["start" , "intent-processing" ]:
307314 state = AssistSatelliteState .PROCESSING
308315
316+ assist_prompt = (
317+ self .config .runtime_data .dashboard .display_settings .assist_prompt
318+ if not self .config .runtime_data .runtime_config_overrides .assist_prompt
319+ else self .config .runtime_data .runtime_config_overrides .assist_prompt
320+ )
321+
309322 async_dispatcher_send (
310323 self .hass ,
311324 f"{ DOMAIN } _{ self .config .entry_id } _event" ,
312325 VAEvent (
313326 VAEventType .ASSIST_LISTENING ,
314327 {
315328 "state" : state ,
316- "style" : self . config . runtime_data . dashboard . display_settings . assist_prompt ,
329+ "style" : assist_prompt ,
317330 },
318331 ),
319332 )
@@ -412,9 +425,7 @@ def on_mode_state_change(self, new_mode: str) -> None:
412425 if new_mode == VAMode .NORMAL :
413426 # Add navigate to default view
414427 if self .navigation_manager :
415- self .navigation_manager .browser_navigate (
416- self .config .runtime_data .dashboard .home
417- )
428+ self .navigation_manager .navigate_home ()
418429 elif new_mode == VAMode .MUSIC :
419430 # Add navigate to music view
420431 if self .navigation_manager :
@@ -441,6 +452,11 @@ def __init__(self, hass: HomeAssistant, config: VAConfigEntry) -> None:
441452 self .config = config
442453 self .entity_id : str | None = None
443454
455+ # Music mode auto-switching configuration
456+ self .music_mode_auto = config .options .get (CONF_MUSIC_MODE_AUTO , False )
457+ self .music_mode_timeout = config .options .get (CONF_MUSIC_MODE_TIMEOUT , 300 )
458+ self .music_timeout_task : asyncio .Task | None = None
459+
444460 def register_listeners (self ) -> None :
445461 """Register the state change listener for entities."""
446462 # Add microphone mute switch listener
@@ -469,6 +485,25 @@ def register_listeners(self) -> None:
469485 )
470486 )
471487
488+ # Add music player state listener for auto mode switching
489+ if self ._should_monitor_music_player ():
490+ musicplayer_device = self .config .runtime_data .core .musicplayer_device
491+
492+ # Check initial state, enter music mode if already playing
493+ if musicplayer_state := self .hass .states .get (musicplayer_device ):
494+ if musicplayer_state .state == MediaPlayerState .PLAYING :
495+ if self ._is_music_content (musicplayer_state ):
496+ self ._handle_music_started ()
497+
498+ # Add listener for future state changes
499+ self .config .async_on_unload (
500+ async_track_state_change_event (
501+ self .hass ,
502+ musicplayer_device ,
503+ self ._async_on_musicplayer_device_state_change ,
504+ )
505+ )
506+
472507 def _add_entity_state_listener (
473508 self , entity_id : str , listener : Callable [[Event [EventStateChangedData ]], None ]
474509 ) -> None :
@@ -675,6 +710,181 @@ def _async_cc_on_conversation_ended_handler(self, event: Event):
675710 event .data ["device_id" ],
676711 )
677712
713+ def _should_monitor_music_player (self ) -> bool :
714+ """Check if music player monitoring should be enabled."""
715+ musicplayer = self .config .runtime_data .core .musicplayer_device
716+
717+ if not musicplayer :
718+ return False
719+
720+ # Only monitor if at least one feature is enabled
721+ if not self .music_mode_auto and self .music_mode_timeout <= 0 :
722+ return False
723+
724+ return True
725+
726+ def _is_music_content (self , state_obj : State ) -> bool :
727+ """Check if the media content type is an audio entertainment type."""
728+ media_content_type = state_obj .attributes .get ("media_content_type" )
729+
730+ allowed_types = (
731+ "music" ,
732+ "podcast" ,
733+ "episode" ,
734+ "track" ,
735+ "album" ,
736+ "playlist" ,
737+ "artist" ,
738+ "composer" ,
739+ "contributing_artist" ,
740+ "channel" ,
741+ "channels" ,
742+ )
743+
744+ return media_content_type in allowed_types
745+
746+ @callback
747+ def _async_on_musicplayer_device_state_change (
748+ self , event : Event [EventStateChangedData ]
749+ ) -> None :
750+ """Handle music player state changes for auto mode switching."""
751+ if not self ._validate_event (event ):
752+ return
753+
754+ new_state_obj = event .data .get ("new_state" )
755+ old_state_obj = event .data .get ("old_state" )
756+
757+ new_state = new_state_obj .state
758+ old_state = old_state_obj .state if old_state_obj else None
759+
760+ if old_state == new_state :
761+ return
762+
763+ # Music started playing
764+ if new_state == MediaPlayerState .PLAYING :
765+ if not self ._is_music_content (new_state_obj ):
766+ return
767+
768+ self ._handle_music_started ()
769+ # Music stopped/paused
770+ elif new_state in (
771+ MediaPlayerState .IDLE ,
772+ MediaPlayerState .PAUSED ,
773+ MediaPlayerState .OFF ,
774+ ):
775+ self ._handle_music_stopped ()
776+
777+ def _handle_music_started (self ) -> None :
778+ """Handle music playback started - transition to music mode."""
779+ # Only auto-enter if feature is enabled
780+ if not self .music_mode_auto :
781+ return
782+
783+ current_mode = self ._get_current_mode ()
784+
785+ # Don't override these modes
786+ if current_mode in (VAMode .HOLD , VAMode .GAME ):
787+ return
788+
789+ _LOGGER .info (
790+ "Music playback started on %s, switching to music mode" ,
791+ self .config .runtime_data .core .name ,
792+ )
793+
794+ # Cancel any pending timeout task
795+ self ._cancel_music_timeout_task ()
796+
797+ # Update mode to music
798+ self ._set_mode (VAMode .MUSIC )
799+
800+ def _handle_music_stopped (self ) -> None :
801+ """Handle music playback stopped - schedule transition to default mode."""
802+ current_mode = self ._get_current_mode ()
803+
804+ if current_mode != VAMode .MUSIC :
805+ return
806+
807+ if self .music_mode_timeout <= 0 :
808+ return
809+
810+ default_mode = self ._get_default_mode ()
811+ _LOGGER .info (
812+ "Music playback stopped on %s, scheduling return to default mode '%s' in %d seconds" ,
813+ self .config .runtime_data .core .name ,
814+ default_mode ,
815+ self .music_mode_timeout ,
816+ )
817+
818+ # Cancel any existing timeout task
819+ self ._cancel_music_timeout_task ()
820+
821+ # Schedule new timeout task
822+ self .music_timeout_task = self .config .async_create_background_task (
823+ self .hass ,
824+ self ._music_mode_timeout_handler (),
825+ name = f"Music Mode Timeout - { self .config .runtime_data .core .name } " ,
826+ )
827+
828+ async def _music_mode_timeout_handler (self ) -> None :
829+ """Handle music mode timeout - transition back to default mode."""
830+ try :
831+ # Wait for timeout duration
832+ await asyncio .sleep (min (self .music_mode_timeout , 3600 ))
833+
834+ # Verify mode is still music before transitioning
835+ current_mode = self ._get_current_mode ()
836+ if current_mode != VAMode .MUSIC :
837+ return
838+
839+ default_mode = self ._get_default_mode ()
840+ _LOGGER .info (
841+ "Music mode timeout expired for %s, returning to default mode '%s'" ,
842+ self .config .runtime_data .core .name ,
843+ default_mode ,
844+ )
845+
846+ # Update mode to default
847+ self ._set_mode (default_mode )
848+
849+ except asyncio .CancelledError :
850+ raise
851+
852+ def _cancel_music_timeout_task (self ) -> None :
853+ """Cancel any existing music mode timeout task."""
854+ if self .music_timeout_task and not self .music_timeout_task .done ():
855+ self .music_timeout_task .cancel ()
856+ self .music_timeout_task = None
857+
858+ def _get_current_mode (self ) -> str :
859+ """Get the current mode from the sensor entity."""
860+ sensor_entity = get_sensor_entity_from_instance (
861+ self .hass , self .config .entry_id
862+ )
863+ if sensor_entity and (state := self .hass .states .get (sensor_entity )):
864+ return state .attributes .get ("mode" , VAMode .NORMAL )
865+ return VAMode .NORMAL
866+
867+ def _get_default_mode (self ) -> str :
868+ """Get the configured default mode from config options."""
869+ return self .config .options .get ("mode" , VAMode .NORMAL )
870+
871+ def _set_mode (self , mode : str ) -> None :
872+ """Set the mode using the view_assist.set_state service."""
873+ sensor_entity = get_sensor_entity_from_instance (
874+ self .hass , self .config .entry_id
875+ )
876+ if sensor_entity :
877+ self .hass .async_create_task (
878+ self .hass .services .async_call (
879+ DOMAIN ,
880+ "set_state" ,
881+ {
882+ "entity_id" : sensor_entity ,
883+ "mode" : mode ,
884+ },
885+ )
886+ )
887+
678888 def _update_sensor_entity (self , updates : dict ) -> None :
679889 """Update sensor entity attributes."""
680890 self .config .runtime_data .extra_data .update (updates )
0 commit comments