Skip to content

Commit a347ecb

Browse files
committed
Fixes #1 & #2 and adds media player thumbnail + seeking + more metadata
1 parent 091eed1 commit a347ecb

File tree

4 files changed

+157
-59
lines changed

4 files changed

+157
-59
lines changed

custom_components/hass_agent/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import logging
55
import requests
6+
from .views import MediaPlayerThumbnailView
67
from homeassistant.helpers import device_registry as dr
78
from homeassistant.components.mqtt.models import ReceiveMessage
89
from homeassistant.components.mqtt.subscription import (
@@ -104,7 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
104105
{
105106
"internal_mqtt": {},
106107
"apis": {},
107-
"mqtt": {},
108+
"thumbnail": None,
108109
"loaded": {"media_player": False, "notifications": False},
109110
},
110111
)
@@ -114,6 +115,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
114115
url = entry.data.get(CONF_URL, None)
115116

116117
if url is not None:
118+
117119
def get_device_info():
118120
return requests.get(f"{url}/info", timeout=10)
119121

@@ -201,3 +203,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
201203
hass.data[DOMAIN].pop(entry.entry_id)
202204

203205
return unload_ok
206+
207+
208+
async def async_setup(hass: HomeAssistant, config) -> bool:
209+
hass.http.register_view(MediaPlayerThumbnailView(hass))
210+
return True

custom_components/hass_agent/media_player.py

Lines changed: 101 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import json
33

44
import logging
5+
import time
56
from typing import Any
7+
from homeassistant import util
68

79
from homeassistant.components.mqtt.models import ReceiveMessage
810
from homeassistant.helpers import device_registry as dr
@@ -13,17 +15,20 @@
1315
from homeassistant.components.mqtt.subscription import (
1416
async_prepare_subscribe_topics,
1517
async_subscribe_topics,
18+
async_unsubscribe_topics,
1619
)
1720
from homeassistant.config_entries import ConfigEntry
1821

1922
from 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

2630
from homeassistant.components.media_player.browse_media import (
31+
BrowseMedia,
2732
async_process_play_media_url,
2833
)
2934

@@ -49,71 +54,97 @@
4954
| MediaPlayerEntityFeature.VOLUME_STEP
5055
| MediaPlayerEntityFeature.PLAY
5156
| MediaPlayerEntityFeature.PLAY_MEDIA
57+
| MediaPlayerEntityFeature.SEEK
58+
| MediaPlayerEntityFeature.BROWSE_MEDIA
5259
)
5360

5461

5562
async 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-
10278
class 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,

custom_components/hass_agent/notify.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ async def async_send_message(self, message: str, **kwargs: Any):
9595

9696
_logger.debug("Sending notification")
9797

98-
if entry.data[CONF_URL] is None:
98+
url = entry.data.get(CONF_URL, None)
99+
100+
if url is None:
99101
await mqtt.async_publish(
100102
self.hass,
101103
f"hass.agent/notifications/{self._device_name}",
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from typing import Any
2+
from homeassistant.components.http.view import HomeAssistantView
3+
from aiohttp import web
4+
5+
from homeassistant.core import HomeAssistant
6+
7+
from homeassistant.helpers import entity_registry as er
8+
9+
from .const import DOMAIN
10+
11+
12+
class MediaPlayerThumbnailView(HomeAssistantView):
13+
url = "/api/hass_agent/{media_player:.*}/thumbnail.png"
14+
15+
name = "api:hass_agent:media_player_thumbnails"
16+
17+
requires_auth = False
18+
19+
def __init__(self, hass: HomeAssistant) -> None:
20+
self.hass = hass
21+
22+
async def get(
23+
self,
24+
request: web.Request,
25+
**kwargs: Any,
26+
) -> web.Response:
27+
print(kwargs)
28+
29+
media_player = kwargs["media_player"]
30+
31+
entity_registry = er.async_get(self.hass)
32+
33+
entity = entity_registry.async_get(media_player)
34+
35+
thumbnail = self.hass.data[DOMAIN][entity.config_entry_id]["thumbnail"]
36+
37+
if thumbnail is None:
38+
return web.Response(status=500)
39+
40+
return web.Response(
41+
body=thumbnail,
42+
content_type="image/png",
43+
status=200,
44+
headers={"Content-Length": f"{len(thumbnail)}"},
45+
)

0 commit comments

Comments
 (0)