Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """Support for LG TV running on NetCast 3 or 4."""
2 
3 from __future__ import annotations
4 
5 from datetime import datetime
6 from typing import TYPE_CHECKING, Any
7 
8 from pylgnetcast import LG_COMMAND, LgNetCastClient, LgNetCastError
9 from requests import RequestException
10 
12  MediaPlayerDeviceClass,
13  MediaPlayerEntity,
14  MediaPlayerEntityFeature,
15  MediaPlayerState,
16  MediaType,
17 )
18 from homeassistant.config_entries import ConfigEntry
19 from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_MODEL, CONF_NAME
20 from homeassistant.core import HomeAssistant
21 from homeassistant.helpers.device_registry import DeviceInfo
22 from homeassistant.helpers.entity_platform import AddEntitiesCallback
23 from homeassistant.helpers.trigger import PluggableAction
24 
25 from .const import ATTR_MANUFACTURER, DOMAIN
26 from .triggers.turn_on import async_get_turn_on_trigger
27 
28 DEFAULT_NAME = "LG TV Remote"
29 
30 CONF_ON_ACTION = "turn_on_action"
31 
32 SUPPORT_LGTV = (
33  MediaPlayerEntityFeature.PAUSE
34  | MediaPlayerEntityFeature.VOLUME_STEP
35  | MediaPlayerEntityFeature.VOLUME_SET
36  | MediaPlayerEntityFeature.VOLUME_MUTE
37  | MediaPlayerEntityFeature.PREVIOUS_TRACK
38  | MediaPlayerEntityFeature.NEXT_TRACK
39  | MediaPlayerEntityFeature.TURN_OFF
40  | MediaPlayerEntityFeature.SELECT_SOURCE
41  | MediaPlayerEntityFeature.PLAY
42  | MediaPlayerEntityFeature.PLAY_MEDIA
43  | MediaPlayerEntityFeature.STOP
44 )
45 
46 
48  hass: HomeAssistant,
49  config_entry: ConfigEntry,
50  async_add_entities: AddEntitiesCallback,
51 ) -> None:
52  """Set up a LG Netcast Media Player from a config_entry."""
53 
54  host = config_entry.data[CONF_HOST]
55  access_token = config_entry.data[CONF_ACCESS_TOKEN]
56  unique_id = config_entry.unique_id
57  name = config_entry.data.get(CONF_NAME, DEFAULT_NAME)
58  model = config_entry.data[CONF_MODEL]
59 
60  client = LgNetCastClient(host, access_token)
61 
62  hass.data[DOMAIN][config_entry.entry_id] = client
63 
64  async_add_entities([LgTVDevice(client, name, model, unique_id=unique_id)])
65 
66 
68  """Representation of a LG TV."""
69 
70  _attr_assumed_state = True
71  _attr_device_class = MediaPlayerDeviceClass.TV
72  _attr_media_content_type = MediaType.CHANNEL
73  _attr_has_entity_name = True
74  _attr_name = None
75 
76  def __init__(self, client, name, model, unique_id):
77  """Initialize the LG TV device."""
78  self._client_client = client
79  self._muted_muted = False
80  self._turn_on_turn_on = PluggableAction(self.async_write_ha_stateasync_write_ha_state)
81  self._volume_volume = 0
82  self._channel_id_channel_id = None
83  self._channel_name_channel_name = ""
84  self._program_name_program_name = ""
85  self._sources_sources = {}
86  self._source_names_source_names = []
87  self._attr_unique_id_attr_unique_id = unique_id
88  self._attr_device_info_attr_device_info = DeviceInfo(
89  identifiers={(DOMAIN, unique_id)},
90  manufacturer=ATTR_MANUFACTURER,
91  name=name,
92  model=model,
93  )
94 
95  async def async_added_to_hass(self) -> None:
96  """Connect and subscribe to dispatcher signals and state updates."""
97  await super().async_added_to_hass()
98 
99  entry = self.registry_entryregistry_entry
100 
101  if TYPE_CHECKING:
102  assert entry is not None and entry.device_id is not None
103 
104  self.async_on_removeasync_on_remove(
105  self._turn_on_turn_on.async_register(
106  self.hasshass, async_get_turn_on_trigger(entry.device_id)
107  )
108  )
109 
110  def send_command(self, command):
111  """Send remote control commands to the TV."""
112 
113  try:
114  with self._client_client as client:
115  client.send_command(command)
116  except (LgNetCastError, RequestException):
117  self._attr_state_attr_state = MediaPlayerState.OFF
118 
119  def update(self) -> None:
120  """Retrieve the latest data from the LG TV."""
121 
122  try:
123  with self._client_client as client:
124  self._attr_state_attr_state = MediaPlayerState.ON
125 
126  self.__update_volume__update_volume()
127 
128  channel_info = client.query_data("cur_channel")
129  if channel_info:
130  channel_info = channel_info[0]
131  channel_id = channel_info.find("major")
132  self._channel_name_channel_name = channel_info.find("chname").text
133  self._program_name_program_name = channel_info.find("progName").text
134  if channel_id is not None:
135  self._channel_id_channel_id = int(channel_id.text)
136  if self._channel_name_channel_name is None:
137  self._channel_name_channel_name = channel_info.find("inputSourceName").text
138  if self._program_name_program_name is None:
139  self._program_name_program_name = channel_info.find("labelName").text
140 
141  channel_list = client.query_data("channel_list")
142  if channel_list:
143  channel_names = []
144  for channel in channel_list:
145  channel_name = channel.find("chname")
146  if channel_name is not None:
147  channel_names.append(str(channel_name.text))
148  self._sources_sources = dict(zip(channel_names, channel_list, strict=False))
149  # sort source names by the major channel number
150  source_tuples = [
151  (k, source.find("major").text)
152  for k, source in self._sources_sources.items()
153  ]
154  sorted_sources = sorted(
155  source_tuples, key=lambda channel: int(channel[1])
156  )
157  self._source_names_source_names = [n for n, k in sorted_sources]
158  except (LgNetCastError, RequestException):
159  self._attr_state_attr_state = MediaPlayerState.OFF
160 
161  def __update_volume(self):
162  volume_info = self._client_client.get_volume()
163  if volume_info:
164  (volume, muted) = volume_info
165  self._volume_volume = volume
166  self._muted_muted = muted
167 
168  @property
169  def is_volume_muted(self):
170  """Boolean if volume is currently muted."""
171  return self._muted_muted
172 
173  @property
174  def volume_level(self):
175  """Volume level of the media player (0..1)."""
176  return self._volume_volume / 100.0
177 
178  @property
179  def source(self):
180  """Return the current input source."""
181  return self._channel_name_channel_name
182 
183  @property
184  def source_list(self):
185  """List of available input sources."""
186  return self._source_names_source_names
187 
188  @property
189  def media_content_id(self):
190  """Content id of current playing media."""
191  return self._channel_id_channel_id
192 
193  @property
194  def media_channel(self):
195  """Channel currently playing."""
196  return self._channel_name_channel_name
197 
198  @property
199  def media_title(self):
200  """Title of current playing media."""
201  return self._program_name_program_name
202 
203  @property
204  def supported_features(self) -> MediaPlayerEntityFeature:
205  """Flag media player features that are supported."""
206  if self._turn_on_turn_on:
207  return SUPPORT_LGTV | MediaPlayerEntityFeature.TURN_ON
208  return SUPPORT_LGTV
209 
210  @property
211  def media_image_url(self):
212  """URL for obtaining a screen capture."""
213  return (
214  f"{self._client.url}data?target=screen_image&_={datetime.now().timestamp()}"
215  )
216 
217  def turn_off(self) -> None:
218  """Turn off media player."""
219  self.send_commandsend_command(LG_COMMAND.POWER)
220 
221  async def async_turn_on(self) -> None:
222  """Turn on the media player."""
223  await self._turn_on_turn_on.async_run(self.hasshass, self._context_context)
224 
225  def volume_up(self) -> None:
226  """Volume up the media player."""
227  self.send_commandsend_command(LG_COMMAND.VOLUME_UP)
228 
229  def volume_down(self) -> None:
230  """Volume down media player."""
231  self.send_commandsend_command(LG_COMMAND.VOLUME_DOWN)
232 
233  def set_volume_level(self, volume: float) -> None:
234  """Set volume level, range 0..1."""
235  self._client_client.set_volume(float(volume * 100))
236 
237  def mute_volume(self, mute: bool) -> None:
238  """Send mute command."""
239  self.send_commandsend_command(LG_COMMAND.MUTE_TOGGLE)
240 
241  def select_source(self, source: str) -> None:
242  """Select input source."""
243  self._client_client.change_channel(self._sources_sources[source])
244 
245  def media_play(self) -> None:
246  """Send play command."""
247  self.send_commandsend_command(LG_COMMAND.PLAY)
248 
249  def media_pause(self) -> None:
250  """Send media pause command to media player."""
251  self.send_commandsend_command(LG_COMMAND.PAUSE)
252 
253  def media_stop(self) -> None:
254  """Send media stop command to media player."""
255  self.send_commandsend_command(LG_COMMAND.STOP)
256 
257  def media_next_track(self) -> None:
258  """Send next track command."""
259  self.send_commandsend_command(LG_COMMAND.FAST_FORWARD)
260 
261  def media_previous_track(self) -> None:
262  """Send the previous track command."""
263  self.send_commandsend_command(LG_COMMAND.REWIND)
264 
266  self, media_type: MediaType | str, media_id: str, **kwargs: Any
267  ) -> None:
268  """Tune to channel."""
269  if media_type != MediaType.CHANNEL:
270  raise ValueError(f"Invalid media type: {media_type}")
271 
272  for name, channel in self._sources_sources.items():
273  channel_id = channel.find("major")
274  if channel_id is not None and int(channel_id.text) == int(media_id):
275  self.select_sourceselect_sourceselect_source(name)
276  return
277 
278  raise ValueError(f"Invalid media id: {media_id}")
None play_media(self, MediaType|str media_type, str media_id, **Any kwargs)
def __init__(self, client, name, model, unique_id)
Definition: media_player.py:76
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
None async_register(HomeAssistant hass, system_health.SystemHealthRegistration register)
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: media_player.py:51
dict[str, str] async_get_turn_on_trigger(str device_id)
Definition: turn_on.py:39