22import json
33
44import logging
5+ import time
56from typing import Any
7+ from homeassistant import util
68
79from homeassistant .components .mqtt .models import ReceiveMessage
810from homeassistant .helpers import device_registry as dr
1315from homeassistant .components .mqtt .subscription import (
1416 async_prepare_subscribe_topics ,
1517 async_subscribe_topics ,
18+ async_unsubscribe_topics ,
1619)
1720from homeassistant .config_entries import ConfigEntry
1821
1922from homeassistant .components .media_player import (
23+ MediaPlayerDeviceClass ,
2024 MediaPlayerEntity ,
2125 MediaPlayerEntityFeature ,
2226)
2327
24- from homeassistant .components .media_player .const import MEDIA_TYPE_MUSIC , MediaType
28+ from homeassistant .components .media_player .const import MEDIA_TYPE_MUSIC
2529
2630from homeassistant .components .media_player .browse_media import (
31+ BrowseMedia ,
2732 async_process_play_media_url ,
2833)
2934
4954 | MediaPlayerEntityFeature .VOLUME_STEP
5055 | MediaPlayerEntityFeature .PLAY
5156 | MediaPlayerEntityFeature .PLAY_MEDIA
57+ | MediaPlayerEntityFeature .SEEK
58+ | MediaPlayerEntityFeature .BROWSE_MEDIA
5259)
5360
5461
5562async def async_setup_entry (
5663 hass : HomeAssistant , entry : ConfigEntry , async_add_entities : AddEntitiesCallback
5764) -> bool :
58-
59- sub_state = hass .data [DOMAIN ][entry .entry_id ]["mqtt" ]
60-
6165 device_registry = dr .async_get (hass )
6266 device = device_registry .async_get_device (identifiers = {(DOMAIN , entry .unique_id )})
6367
6468 if device is None :
6569 return False
6670
67- entity = HassAgentMediaPlayerDevice (
68- entry .unique_id ,
69- device ,
70- f"hass.agent/media_player/{ device .name } /cmd" ,
71+ async_add_entities (
72+ [HassAgentMediaPlayerDevice (entry .unique_id , entry .entry_id , device )]
7173 )
7274
73- def updated (message : ReceiveMessage ):
74- payload = json .loads (message .payload )
75- entity .apply_payload (payload )
76-
77- sub_state = async_prepare_subscribe_topics (
78- hass ,
79- sub_state ,
80- {
81- f"{ entry .unique_id } -state" : {
82- "topic" : f"hass.agent/media_player/{ device .name } /state" ,
83- "msg_callback" : updated ,
84- "qos" : 0 ,
85- }
86- },
87- )
88-
89- await async_subscribe_topics (hass , sub_state )
90-
91- hass .data [DOMAIN ][entry .entry_id ]["mqtt" ] = sub_state
92-
93- async_add_entities ([entity ])
94-
9575 return True
9676
9777
98- async def async_unload_entry (hass : HomeAssistant , entry : ConfigEntry ):
99- print ("unload!" )
100-
101-
10278class HassAgentMediaPlayerDevice (MediaPlayerEntity ):
10379 """HASS.Agent MediaPlayer Device"""
10480
10581 @callback
106- def apply_payload (self , payload ):
82+ def update_thumbnail (self , message : ReceiveMessage ):
83+ self .hass .data [DOMAIN ][self ._entry_id ]["thumbnail" ] = message .payload
84+
85+ self ._attr_media_image_url = (
86+ f"/api/hass_agent/{ self .entity_id } /thumbnail.png?time={ time .time ()} "
87+ )
88+
89+ @property
90+ def media_image_local (self ) -> str | None :
91+ return self ._attr_media_image_url
92+
93+ @callback
94+ def updated (self , message : ReceiveMessage ):
95+ """Updates the media player with new data from MQTT"""
96+ payload = json .loads (message .payload )
97+
10798 self ._state = payload ["state" ].lower ()
108- self ._playing = payload ["title" ]
10999 self ._volume_level = payload ["volume" ]
110100 self ._muted = payload ["muted" ]
111101 self ._available = True
112102
103+ if self ._state != "off" :
104+ self ._attr_media_album_artist = payload ["albumartist" ]
105+ self ._attr_media_album_name = payload ["albumtitle" ]
106+ self ._attr_media_artist = payload ["artist" ]
107+ self ._attr_media_title = payload ["title" ]
108+
109+ self ._attr_media_duration = payload ["duration" ]
110+ self ._attr_media_position = payload ["currentposition" ]
111+
112+ self ._attr_media_position_updated_at = util .dt .utcnow ()
113+
114+ self ._last_updated = time .time ()
115+
116+ # self.media_image_url
117+
113118 self .async_write_ha_state ()
114119
115- def __init__ (self , unique_id , device : dr .DeviceEntry , command_topic ):
120+ async def async_added_to_hass (self ) -> None :
121+ self ._listeners = async_prepare_subscribe_topics (
122+ self .hass ,
123+ self ._listeners ,
124+ {
125+ f"{ self ._attr_unique_id } -state" : {
126+ "topic" : f"hass.agent/media_player/{ self ._attr_device_info ['name' ]} /state" ,
127+ "msg_callback" : self .updated ,
128+ "qos" : 0 ,
129+ },
130+ f"{ self ._attr_unique_id } -thumbnail" : {
131+ "topic" : f"hass.agent/media_player/{ self ._attr_device_info ['name' ]} /thumbnail" ,
132+ "msg_callback" : self .update_thumbnail ,
133+ "qos" : 0 ,
134+ "encoding" : None ,
135+ },
136+ },
137+ )
138+
139+ await async_subscribe_topics (self .hass , self ._listeners )
140+
141+ async def async_will_remove_from_hass (self ) -> None :
142+ if self ._listeners is not None :
143+ async_unsubscribe_topics (self .hass , self ._listeners )
144+
145+ def __init__ (self , unique_id , entry_id , device : dr .DeviceEntry ):
116146 """Initialize"""
147+ self ._entry_id = entry_id
117148 self ._name = device .name
118149 self ._attr_device_info = {
119150 "identifiers" : device .identifiers ,
@@ -122,14 +153,17 @@ def __init__(self, unique_id, device: dr.DeviceEntry, command_topic):
122153 "model" : device .model ,
123154 "sw_version" : device .sw_version ,
124155 }
125- self ._command_topic = command_topic
126- self ._attr_unique_id = f"hass.agent- { unique_id } "
156+ self ._command_topic = f"hass.agent/media_player/ { device . name } /cmd"
157+ self ._attr_unique_id = f"media_player_ { unique_id } "
127158 self ._available = False
128159 self ._muted = False
129160 self ._volume_level = 0
130161 self ._playing = ""
131162 self ._state = ""
132163
164+ self ._listeners = {}
165+ self ._last_updated = 0
166+
133167 async def _send_command (self , command , data = None ):
134168 """Send a command"""
135169 _logger .debug ("Sending command: %s" , command )
@@ -160,12 +194,14 @@ def state(self):
160194 @property
161195 def available (self ):
162196 """Return if we're available"""
163- return self ._available
164197
165- @property
166- def media_title (self ):
167- """Return the title of current playing media"""
168- return self ._playing
198+ diff = round (time .time () - self ._last_updated )
199+ return diff < 5
200+
201+ # @property
202+ # def media_title(self):
203+ # """Return the title of current playing media"""
204+ # return self._playing
169205
170206 @property
171207 def volume_level (self ):
@@ -177,12 +213,6 @@ def is_volume_muted(self):
177213 """Return if volume is currently muted"""
178214 return self ._muted
179215
180- @property
181- def media_duration (self ):
182- """Return the duration of the current playing media in seconds"""
183- """ NOT IMPLEMENTED """
184- return 0
185-
186216 @property
187217 def supported_features (self ):
188218 """Flag media player features that are supported"""
@@ -191,21 +221,24 @@ def supported_features(self):
191221 @property
192222 def device_class (self ):
193223 """Announce ourselve as a speaker"""
194- return "DEVICE_CLASS_SPEAKER"
224+ return MediaPlayerDeviceClass . SPEAKER
195225
196226 @property
197227 def media_content_type (self ):
198228 """Content type of current playing media"""
199229 return MEDIA_TYPE_MUSIC
200230
231+ async def async_media_seek (self , position : float ) -> None :
232+ self ._attr_media_position = position
233+ self ._attr_media_position_updated_at = util .dt .utcnow ()
234+ await self ._send_command ("seek" , position )
235+
201236 async def async_volume_up (self ):
202237 """Volume up the media player"""
203- super ().async_volume_up ()
204238 await self ._send_command ("volumeup" )
205239
206240 async def async_volume_down (self ):
207241 """Volume down media player"""
208- super ().async_volume_down ()
209242 await self ._send_command ("volumedown" )
210243
211244 async def async_mute_volume (self , mute ):
@@ -235,11 +268,22 @@ async def async_media_previous_track(self):
235268 """Send previous track command"""
236269 await self ._send_command ("previous" )
237270
238- async def async_play_media (
239- self , media_type : MediaType | str , media_id : str , ** kwargs : Any
240- ):
271+ async def async_browse_media (
272+ self , media_content_type : str | None = None , media_content_id : str | None = None
273+ ) -> BrowseMedia :
274+ """Implement the websocket media browsing helper."""
275+ # If your media player has no own media sources to browse, route all browse commands
276+ # to the media source integration.
277+ return await media_source .async_browse_media (
278+ self .hass ,
279+ media_content_id ,
280+ # This allows filtering content. In this case it will only show audio sources.
281+ content_filter = lambda item : item .media_content_type .startswith ("audio/" ),
282+ )
283+
284+ async def async_play_media (self , media_type : str , media_id : str , ** kwargs : Any ):
241285 """Play media source"""
242- if media_type != MEDIA_TYPE_MUSIC :
286+ if not media_type . startswith ( "audio/" ) :
243287 _logger .error (
244288 "Invalid media type %r. Only %s is supported!" ,
245289 media_type ,
0 commit comments