1 """Support for interfacing with the XBMC/Kodi JSON-RPC API."""
3 from __future__
import annotations
5 from collections.abc
import Awaitable, Callable, Coroutine
6 from datetime
import timedelta
7 from functools
import wraps
10 from typing
import Any, Concatenate
12 from jsonrpc_base.jsonrpc
import ProtocolError, TransportError
13 from pykodi
import CannotConnectError
14 import voluptuous
as vol
18 PLATFORM_SCHEMA
as MEDIA_PLAYER_PLATFORM_SCHEMA,
22 MediaPlayerEntityFeature,
25 async_process_play_media_url,
40 EVENT_HOMEASSISTANT_STARTED,
44 config_validation
as cv,
45 device_registry
as dr,
55 from .browse_media
import (
59 media_source_content_filter,
74 _LOGGER = logging.getLogger(__name__)
76 EVENT_KODI_CALL_METHOD_RESULT =
"kodi_call_method_result"
78 CONF_TCP_PORT =
"tcp_port"
79 CONF_TURN_ON_ACTION =
"turn_on_action"
80 CONF_TURN_OFF_ACTION =
"turn_off_action"
81 CONF_ENABLE_WEBSOCKET =
"enable_websocket"
83 DEPRECATED_TURN_OFF_ACTIONS = {
85 "quit":
"Application.Quit",
86 "hibernate":
"System.Hibernate",
87 "suspend":
"System.Suspend",
88 "reboot":
"System.Reboot",
89 "shutdown":
"System.Shutdown",
96 "music": MediaType.MUSIC,
97 "artist": MediaType.MUSIC,
98 "album": MediaType.MUSIC,
99 "song": MediaType.MUSIC,
100 "video": MediaType.VIDEO,
101 "set": MediaType.PLAYLIST,
102 "musicvideo": MediaType.VIDEO,
103 "movie": MediaType.MOVIE,
104 "tvshow": MediaType.TVSHOW,
105 "season": MediaType.TVSHOW,
106 "episode": MediaType.TVSHOW,
108 "channel": MediaType.CHANNEL,
110 "audio": MediaType.MUSIC,
113 MAP_KODI_MEDIA_TYPES: dict[MediaType | str, str] = {
114 MediaType.MOVIE:
"movieid",
115 MediaType.EPISODE:
"episodeid",
116 MediaType.SEASON:
"seasonid",
117 MediaType.TVSHOW:
"tvshowid",
121 PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
123 vol.Required(CONF_HOST): cv.string,
124 vol.Optional(CONF_NAME): cv.string,
125 vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
126 vol.Optional(CONF_TCP_PORT, default=DEFAULT_WS_PORT): cv.port,
127 vol.Optional(CONF_PROXY_SSL, default=DEFAULT_SSL): cv.boolean,
128 vol.Optional(CONF_TURN_ON_ACTION): cv.SCRIPT_SCHEMA,
129 vol.Optional(CONF_TURN_OFF_ACTION): vol.Any(
130 cv.SCRIPT_SCHEMA, vol.In(DEPRECATED_TURN_OFF_ACTIONS)
132 vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
133 vol.Inclusive(CONF_USERNAME,
"auth"): cv.string,
134 vol.Inclusive(CONF_PASSWORD,
"auth"): cv.string,
135 vol.Optional(CONF_ENABLE_WEBSOCKET, default=
True): cv.boolean,
140 SERVICE_ADD_MEDIA =
"add_to_playlist"
141 SERVICE_CALL_METHOD =
"call_method"
143 ATTR_MEDIA_TYPE =
"media_type"
144 ATTR_MEDIA_NAME =
"media_name"
145 ATTR_MEDIA_ARTIST_NAME =
"artist_name"
146 ATTR_MEDIA_ID =
"media_id"
147 ATTR_METHOD =
"method"
150 KODI_ADD_MEDIA_SCHEMA: VolDictType = {
151 vol.Required(ATTR_MEDIA_TYPE): cv.string,
152 vol.Optional(ATTR_MEDIA_ID): cv.string,
153 vol.Optional(ATTR_MEDIA_NAME): cv.string,
154 vol.Optional(ATTR_MEDIA_ARTIST_NAME): cv.string,
157 KODI_CALL_METHOD_SCHEMA = cv.make_entity_service_schema(
158 {vol.Required(ATTR_METHOD): cv.string}, extra=vol.ALLOW_EXTRA
162 def find_matching_config_entries_for_host(hass, host):
163 """Search existing config entries for one matching the host."""
164 for entry
in hass.config_entries.async_entries(DOMAIN):
165 if entry.data[CONF_HOST] == host:
173 async_add_entities: AddEntitiesCallback,
174 discovery_info: DiscoveryInfoType |
None =
None,
176 """Set up the Kodi platform."""
181 host = config[CONF_HOST]
182 if find_matching_config_entries_for_host(hass, host):
185 websocket = config.get(CONF_ENABLE_WEBSOCKET)
186 ws_port = config.get(CONF_TCP_PORT)
if websocket
else None
189 CONF_NAME: config.get(CONF_NAME, host),
191 CONF_PORT: config.get(CONF_PORT),
192 CONF_WS_PORT: ws_port,
193 CONF_USERNAME: config.get(CONF_USERNAME),
194 CONF_PASSWORD: config.get(CONF_PASSWORD),
195 CONF_SSL: config.get(CONF_PROXY_SSL),
196 CONF_TIMEOUT: config.get(CONF_TIMEOUT),
199 hass.async_create_task(
200 hass.config_entries.flow.async_init(
201 DOMAIN, context={
"source": SOURCE_IMPORT}, data=entry_data
208 config_entry: ConfigEntry,
209 async_add_entities: AddEntitiesCallback,
211 """Set up the Kodi media player platform."""
212 platform = entity_platform.async_get_current_platform()
213 platform.async_register_entity_service(
214 SERVICE_ADD_MEDIA, KODI_ADD_MEDIA_SCHEMA,
"async_add_media_to_playlist"
216 platform.async_register_entity_service(
217 SERVICE_CALL_METHOD, KODI_CALL_METHOD_SCHEMA,
"async_call_method"
220 data = hass.data[DOMAIN][config_entry.entry_id]
221 connection = data[DATA_CONNECTION]
222 kodi = data[DATA_KODI]
223 name = config_entry.data[CONF_NAME]
224 if (uid := config_entry.unique_id)
is None:
225 uid = config_entry.entry_id
227 entity = KodiEntity(connection, kodi, name, uid)
231 def cmd[_KodiEntityT: KodiEntity, **_P](
232 func: Callable[Concatenate[_KodiEntityT, _P], Awaitable[Any]],
233 ) -> Callable[Concatenate[_KodiEntityT, _P], Coroutine[Any, Any,
None]]:
234 """Catch command exceptions."""
237 async
def wrapper(obj: _KodiEntityT, *args: _P.args, **kwargs: _P.kwargs) ->
None:
238 """Wrap all command methods."""
240 await func(obj, *args, **kwargs)
241 except (TransportError, ProtocolError)
as exc:
243 if obj.state == MediaPlayerState.OFF:
244 log_function = _LOGGER.debug
246 log_function = _LOGGER.error
248 "Error calling %s on entity %s: %r",
258 """Representation of a XBMC/Kodi device."""
260 _attr_has_entity_name =
True
262 _attr_translation_key =
"media_player"
263 _attr_supported_features = (
264 MediaPlayerEntityFeature.BROWSE_MEDIA
265 | MediaPlayerEntityFeature.NEXT_TRACK
266 | MediaPlayerEntityFeature.PAUSE
267 | MediaPlayerEntityFeature.PLAY
268 | MediaPlayerEntityFeature.PLAY_MEDIA
269 | MediaPlayerEntityFeature.PREVIOUS_TRACK
270 | MediaPlayerEntityFeature.SEEK
271 | MediaPlayerEntityFeature.SHUFFLE_SET
272 | MediaPlayerEntityFeature.STOP
273 | MediaPlayerEntityFeature.TURN_OFF
274 | MediaPlayerEntityFeature.TURN_ON
275 | MediaPlayerEntityFeature.VOLUME_MUTE
276 | MediaPlayerEntityFeature.VOLUME_SET
277 | MediaPlayerEntityFeature.VOLUME_STEP
280 def __init__(self, connection, kodi, name, uid):
281 """Initialize the Kodi entity."""
282 self._connection = connection
284 self._attr_unique_id = uid
285 self._device_id =
None
287 self._properties = {}
289 self._app_properties = {}
290 self._media_position_updated_at =
None
291 self._media_position =
None
292 self._connect_error =
False
295 identifiers={(DOMAIN, uid)},
300 def _reset_state(self, players=None):
301 self._players = players
302 self._properties = {}
304 self._app_properties = {}
305 self._media_position_updated_at =
None
306 self._media_position =
None
309 def _kodi_is_off(self):
310 return self._players
is None
313 def _no_active_players(self):
314 return not self._players
317 def async_on_speed_event(self, sender, data):
318 """Handle player changes between playing and paused."""
319 self._properties[
"speed"] = data[
"player"][
"speed"]
321 if not hasattr(data[
"item"],
"id"):
326 force_refresh = data[
"item"][
"id"] != self._item.
get(
"id")
328 self.async_schedule_update_ha_state(force_refresh)
331 def async_on_stop(self, sender, data):
332 """Handle the stop of the player playback."""
334 if self._kodi_is_off:
337 self._reset_state([])
338 self.async_write_ha_state()
341 def async_on_volume_changed(self, sender, data):
342 """Handle the volume changes."""
343 self._app_properties[
"volume"] = data[
"volume"]
344 self._app_properties[
"muted"] = data[
"muted"]
345 self.async_write_ha_state()
348 def async_on_key_press(self, sender, data):
349 """Handle a incoming key press notification."""
350 self.hass.bus.async_fire(
351 f
"{DOMAIN}_keypress",
353 CONF_TYPE:
"keypress",
354 CONF_DEVICE_ID: self._device_id,
355 ATTR_ENTITY_ID: self.entity_id,
361 async
def async_on_quit(self, sender, data):
362 """Reset the player state on quit action."""
363 await self._clear_connection()
365 async
def _clear_connection(self, close=True):
367 self.async_write_ha_state()
369 await self._connection.close()
372 def state(self) -> MediaPlayerState:
373 """Return the state of the device."""
374 if self._kodi_is_off:
375 return MediaPlayerState.OFF
377 if self._no_active_players:
378 return MediaPlayerState.IDLE
380 if self._properties[
"speed"] == 0:
381 return MediaPlayerState.PAUSED
383 return MediaPlayerState.PLAYING
386 """Connect the websocket if needed."""
387 if not self._connection.can_subscribe:
390 if self._connection.connected:
391 await self._on_ws_connected()
393 async
def start_watchdog(event=None):
394 """Start websocket watchdog."""
395 await self._async_connect_websocket_if_disconnected()
396 self.async_on_remove(
399 self._async_connect_websocket_if_disconnected,
400 WEBSOCKET_WATCHDOG_INTERVAL,
406 if self.hass.state
is CoreState.running:
407 await start_watchdog()
409 self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_watchdog)
411 async
def _on_ws_connected(self):
412 """Call after ws is connected."""
413 self._connect_error =
False
414 self._register_ws_callbacks()
416 version = (await self._kodi.get_application_properties([
"version"]))[
"version"]
417 sw_version = f
"{version['major']}.{version['minor']}"
418 dev_reg = dr.async_get(self.hass)
419 device = dev_reg.async_get_device(identifiers={(DOMAIN, self.unique_id)})
420 dev_reg.async_update_device(device.id, sw_version=sw_version)
421 self._device_id = device.id
423 self.async_schedule_update_ha_state(
True)
425 async
def _async_ws_connect(self):
426 """Connect to Kodi via websocket protocol."""
428 await self._connection.connect()
429 await self._on_ws_connected()
430 except (TransportError, CannotConnectError):
431 if not self._connect_error:
432 self._connect_error =
True
433 _LOGGER.warning(
"Unable to connect to Kodi via websocket")
434 await self._clear_connection(
False)
436 self._connect_error =
False
438 async
def _ping(self):
440 await self._kodi.ping()
441 except (TransportError, CannotConnectError):
442 if not self._connect_error:
443 self._connect_error =
True
444 _LOGGER.warning(
"Unable to ping Kodi via websocket")
445 await self._clear_connection()
447 self._connect_error =
False
449 async
def _async_connect_websocket_if_disconnected(self, *_):
450 """Reconnect the websocket if it fails."""
451 if not self._connection.connected:
452 await self._async_ws_connect()
457 def _register_ws_callbacks(self):
458 self._connection.server.Player.OnPause = self.async_on_speed_event
459 self._connection.server.Player.OnPlay = self.async_on_speed_event
460 self._connection.server.Player.OnAVStart = self.async_on_speed_event
461 self._connection.server.Player.OnAVChange = self.async_on_speed_event
462 self._connection.server.Player.OnResume = self.async_on_speed_event
463 self._connection.server.Player.OnSpeedChanged = self.async_on_speed_event
464 self._connection.server.Player.OnSeek = self.async_on_speed_event
465 self._connection.server.Player.OnStop = self.async_on_stop
466 self._connection.server.Application.OnVolumeChanged = (
467 self.async_on_volume_changed
469 self._connection.server.Other.OnKeyPress = self.async_on_key_press
470 self._connection.server.System.OnQuit = self.async_on_quit
471 self._connection.server.System.OnRestart = self.async_on_quit
472 self._connection.server.System.OnSleep = self.async_on_quit
476 """Retrieve latest state."""
477 if not self._connection.connected:
482 self._players = await self._kodi.get_players()
483 except (TransportError, ProtocolError):
484 if not self._connection.can_subscribe:
489 if self._kodi_is_off:
494 self._app_properties = await self._kodi.get_application_properties(
498 self._properties = await self._kodi.get_player_properties(
499 self._players[0], [
"time",
"totaltime",
"speed",
"live"]
502 position = self._properties[
"time"]
503 if self._media_position != position:
504 self._media_position_updated_at = dt_util.utcnow()
505 self._media_position = position
507 self._item = await self._kodi.get_playing_item_properties(
524 self._reset_state([])
528 """Return True if entity has to be polled for state."""
529 return not self._connection.can_subscribe
532 def volume_level(self) -> float | None:
533 """Volume level of the media player (0..1)."""
534 if "volume" in self._app_properties:
535 return int(self._app_properties[
"volume"]) / 100.0
539 def is_volume_muted(self):
540 """Boolean if volume is currently muted."""
541 return self._app_properties.
get(
"muted")
544 def media_content_id(self):
545 """Content ID of current playing media."""
546 return self._item.
get(
"uniqueid",
None)
549 def media_content_type(self):
550 """Content type of current playing media.
552 If the media type cannot be detected, the player type is used.
554 item_type = MEDIA_TYPES.get(self._item.
get(
"type"))
555 if (item_type
is None or item_type ==
"channel")
and self._players:
556 return MEDIA_TYPES.get(self._players[0][
"type"])
560 def media_duration(self):
561 """Duration of current playing media in seconds."""
562 if self._properties.
get(
"live"):
565 if (total_time := self._properties.
get(
"totaltime"))
is None:
569 total_time[
"hours"] * 3600
570 + total_time[
"minutes"] * 60
571 + total_time[
"seconds"]
575 def media_position(self):
576 """Position of current playing media in seconds."""
577 if (time := self._properties.
get(
"time"))
is None:
580 return time[
"hours"] * 3600 + time[
"minutes"] * 60 + time[
"seconds"]
583 def media_position_updated_at(self):
584 """Last valid time of media position."""
585 return self._media_position_updated_at
588 def media_image_url(self):
589 """Image url of current playing media."""
590 if (thumbnail := self._item.
get(
"thumbnail"))
is None:
593 return self._kodi.thumbnail_url(thumbnail)
596 def media_title(self):
597 """Title of current playing media."""
600 return item.get(
"title")
or item.get(
"label")
or item.get(
"file")
603 def media_series_title(self):
604 """Title of series of current playing media, TV show only."""
605 return self._item.
get(
"showtitle")
608 def media_season(self):
609 """Season of current playing media, TV show only."""
610 return self._item.
get(
"season")
613 def media_episode(self):
614 """Episode of current playing media, TV show only."""
615 return self._item.
get(
"episode")
618 def media_album_name(self):
619 """Album name of current playing media, music track only."""
620 return self._item.
get(
"album")
623 def media_artist(self):
624 """Artist of current playing media, music track only."""
625 if artists := self._item.
get(
"artist"):
631 def media_album_artist(self):
632 """Album artist of current playing media, music track only."""
633 if artists := self._item.
get(
"albumartist"):
640 """Return the state attributes."""
641 state_attr: dict[str, str |
None] = {}
642 if self.state == MediaPlayerState.OFF:
645 state_attr[
"dynamic_range"] =
"sdr"
646 if (video_details := self._item.
get(
"streamdetails", {}).
get(
"video"))
and (
647 hdr_type := video_details[0].
get(
"hdrtype")
649 state_attr[
"dynamic_range"] = hdr_type
654 """Turn the media player on."""
655 _LOGGER.debug(
"Firing event to turn on device")
656 self.hass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_id})
659 """Turn the media player off."""
660 _LOGGER.debug(
"Firing event to turn off device")
661 self.hass.bus.async_fire(EVENT_TURN_OFF, {ATTR_ENTITY_ID: self.entity_id})
664 async
def async_volume_up(self) -> None:
665 """Volume up the media player."""
666 await self._kodi.volume_up()
669 async
def async_volume_down(self) -> None:
670 """Volume down the media player."""
671 await self._kodi.volume_down()
674 async
def async_set_volume_level(self, volume: float) ->
None:
675 """Set volume level, range 0..1."""
676 await self._kodi.set_volume_level(
int(volume * 100))
679 async
def async_mute_volume(self, mute: bool) ->
None:
680 """Mute (true) or unmute (false) media player."""
681 await self._kodi.mute(mute)
684 async
def async_media_play_pause(self) -> None:
685 """Pause media on media player."""
686 await self._kodi.play_pause()
689 async
def async_media_play(self) -> None:
691 await self._kodi.play()
694 async
def async_media_pause(self) -> None:
695 """Pause the media player."""
696 await self._kodi.pause()
699 async
def async_media_stop(self) -> None:
700 """Stop the media player."""
701 await self._kodi.stop()
704 async
def async_media_next_track(self) -> None:
705 """Send next track command."""
706 await self._kodi.next_track()
709 async
def async_media_previous_track(self) -> None:
710 """Send next track command."""
711 await self._kodi.previous_track()
714 async
def async_media_seek(self, position: float) ->
None:
715 """Send seek command."""
716 await self._kodi.media_seek(position)
720 self, media_type: MediaType | str, media_id: str, **kwargs: Any
722 """Send the play_media command to the media player."""
723 if media_source.is_media_source_id(media_id):
724 media_type = MediaType.URL
725 play_item = await media_source.async_resolve_media(
726 self.hass, media_id, self.entity_id
728 media_id = play_item.url
730 media_type_lower = media_type.lower()
732 if media_type_lower == MediaType.CHANNEL:
733 await self._kodi.play_channel(
int(media_id))
734 elif media_type_lower == MediaType.PLAYLIST:
735 await self._kodi.play_playlist(
int(media_id))
736 elif media_type_lower ==
"file":
737 await self._kodi.play_file(media_id)
738 elif media_type_lower ==
"directory":
739 await self._kodi.play_directory(media_id)
740 elif media_type_lower
in [
745 await self.async_clear_playlist()
746 await self.async_add_to_playlist(media_type_lower, media_id)
747 await self._kodi.play_playlist(0)
748 elif media_type_lower
in [
754 await self._kodi.play_item(
755 {MAP_KODI_MEDIA_TYPES[media_type_lower]:
int(media_id)}
760 await self._kodi.play_file(media_id)
763 async
def async_set_shuffle(self, shuffle: bool) ->
None:
764 """Set shuffle mode, for the first player."""
765 if self._no_active_players:
766 raise RuntimeError(
"Error: No active player.")
767 await self._kodi.set_shuffle(shuffle)
769 async
def async_call_method(self, method, **kwargs):
770 """Run Kodi JSONRPC API method with params."""
771 _LOGGER.debug(
"Run API method %s, kwargs=%s", method, kwargs)
774 result = await self._kodi.call_method(method, **kwargs)
776 except ProtocolError
as exc:
777 result = exc.args[2][
"error"]
779 "Run API method %s.%s(%s) error: %s",
785 except TransportError:
788 "TransportError trying to run API method %s.%s(%s)",
794 if isinstance(result, dict):
796 "entity_id": self.entity_id,
798 "result_ok": result_ok,
799 "input": {
"method": method,
"params": kwargs},
801 _LOGGER.debug(
"EVENT kodi_call_method_result: %s", event_data)
802 self.hass.bus.async_fire(
803 EVENT_KODI_CALL_METHOD_RESULT, event_data=event_data
807 async
def async_clear_playlist(self) -> None:
808 """Clear default playlist (i.e. playlistid=0)."""
809 await self._kodi.clear_playlist()
811 async
def async_add_to_playlist(self, media_type, media_id):
812 """Add media item to default playlist (i.e. playlistid=0)."""
813 if media_type == MediaType.ARTIST:
814 await self._kodi.add_artist_to_playlist(
int(media_id))
815 elif media_type == MediaType.ALBUM:
816 await self._kodi.add_album_to_playlist(
int(media_id))
817 elif media_type == MediaType.TRACK:
818 await self._kodi.add_song_to_playlist(
int(media_id))
820 async
def async_add_media_to_playlist(
821 self, media_type, media_id=None, media_name="ALL", artist_name=""
823 """Add a media to default playlist.
825 First the media type must be selected, then
826 the media can be specified in terms of id or
827 name and optionally artist name.
828 All the albums of an artist can be added with
831 if media_type ==
"SONG":
833 media_id = await self._async_find_song(media_name, artist_name)
835 await self._kodi.add_song_to_playlist(
int(media_id))
837 elif media_type ==
"ALBUM":
839 if media_name ==
"ALL":
840 await self._async_add_all_albums(artist_name)
843 media_id = await self._async_find_album(media_name, artist_name)
845 await self._kodi.add_album_to_playlist(
int(media_id))
848 raise RuntimeError(
"Unrecognized media type.")
851 _LOGGER.warning(
"No media detected for Playlist.Add")
853 async
def _async_add_all_albums(self, artist_name):
854 """Add all albums of an artist to default playlist (i.e. playlistid=0).
856 The artist is specified in terms of name.
858 artist_id = await self._async_find_artist(artist_name)
860 albums = await self._kodi.get_albums(artist_id)
862 for alb
in albums[
"albums"]:
863 await self._kodi.add_album_to_playlist(
int(alb[
"albumid"]))
865 async
def _async_find_artist(self, artist_name):
866 """Find artist by name."""
867 artists = await self._kodi.get_artists()
869 out = self._find(artist_name, [a[
"artist"]
for a
in artists[
"artists"]])
870 return artists[
"artists"][out[0][0]][
"artistid"]
872 _LOGGER.warning(
"No artists were found: %s", artist_name)
875 async
def _async_find_song(self, song_name, artist_name=""):
876 """Find song by name and optionally artist name."""
878 if artist_name !=
"":
879 artist_id = await self._async_find_artist(artist_name)
881 songs = await self._kodi.get_songs(artist_id)
882 if songs[
"limits"][
"total"] == 0:
885 out = self._find(song_name, [a[
"label"]
for a
in songs[
"songs"]])
886 return songs[
"songs"][out[0][0]][
"songid"]
888 async
def _async_find_album(self, album_name, artist_name=""):
889 """Find album by name and optionally artist name."""
891 if artist_name !=
"":
892 artist_id = await self._async_find_artist(artist_name)
894 albums = await self._kodi.get_albums(artist_id)
896 out = self._find(album_name, [a[
"label"]
for a
in albums[
"albums"]])
897 return albums[
"albums"][out[0][0]][
"albumid"]
900 "No albums were found with artist: %s, album: %s",
907 def _find(key_word, words):
908 key_word = key_word.split(
" ")
909 patt = [re.compile(f
"(^| ){k}( |$)", re.IGNORECASE)
for k
in key_word]
911 out = [[i, 0]
for i
in range(len(words))]
912 for i
in range(len(words)):
913 mtc = [p.search(words[i])
for p
in patt]
914 rate = [m
is not None for m
in mtc].count(
True)
917 return sorted(out, key=
lambda out: out[1], reverse=
True)
921 media_content_type: MediaType | str |
None =
None,
922 media_content_id: str |
None =
None,
924 """Implement the websocket media browsing helper."""
927 async
def _get_thumbnail_url(
934 return self._kodi.thumbnail_url(thumbnail_url)
936 return self.get_browse_image_url(
942 if media_content_type
in [
None,
"library"]:
945 if media_content_id
and media_source.is_media_source_id(media_content_id):
946 return await media_source.async_browse_media(
947 self.hass, media_content_id, content_filter=media_source_content_filter
951 "search_type": media_content_type,
952 "search_id": media_content_id,
958 f
"Media not found: {media_content_type} / {media_content_id}"
962 async
def async_get_browse_image(
964 media_content_type: MediaType | str,
965 media_content_id: str,
966 media_image_id: str |
None =
None,
967 ) -> tuple[bytes |
None, str |
None]:
968 """Get media image from kodi server."""
971 self._kodi, media_content_id, media_content_type
973 except (ProtocolError, TransportError):
977 return await self._async_fetch_image(image_url)
None __init__(self, _AOSmithCoordinatorT coordinator, str junction_id)
None async_added_to_hass(self)
web.Response get(self, web.Request request, str config_key)
None async_turn_on(self, **Any kwargs)
None async_turn_off(self, **Any kwargs)
dict[str, bool] extra_state_attributes(self)
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
BrowseMedia|None async_browse_media(HomeAssistant hass, MediaType|str media_content_type, str media_content_id, str cast_type)
bool async_play_media(HomeAssistant hass, str cast_entity_id, Chromecast chromecast, MediaType|str media_type, str media_id)
def _find(list[dict[str, Any]] regions, region_id)
bool state(HomeAssistant hass, str|State|None entity, Any req_state, timedelta|None for_period=None, str|None attribute=None, TemplateVarsType variables=None)
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)
bool is_internal_request(HomeAssistant hass)