Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """Support for Denon Network Receivers."""
2 
3 from __future__ import annotations
4 
5 import logging
6 import telnetlib # pylint: disable=deprecated-module
7 
8 import voluptuous as vol
9 
11  PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA,
12  MediaPlayerEntity,
13  MediaPlayerEntityFeature,
14  MediaPlayerState,
15 )
16 from homeassistant.const import CONF_HOST, CONF_NAME
17 from homeassistant.core import HomeAssistant
19 from homeassistant.helpers.entity_platform import AddEntitiesCallback
20 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
21 
22 _LOGGER = logging.getLogger(__name__)
23 
24 DEFAULT_NAME = "Music station"
25 
26 SUPPORT_DENON = (
27  MediaPlayerEntityFeature.VOLUME_SET
28  | MediaPlayerEntityFeature.VOLUME_MUTE
29  | MediaPlayerEntityFeature.TURN_ON
30  | MediaPlayerEntityFeature.TURN_OFF
31  | MediaPlayerEntityFeature.SELECT_SOURCE
32 )
33 SUPPORT_MEDIA_MODES = (
34  MediaPlayerEntityFeature.PAUSE
35  | MediaPlayerEntityFeature.STOP
36  | MediaPlayerEntityFeature.PREVIOUS_TRACK
37  | MediaPlayerEntityFeature.NEXT_TRACK
38  | MediaPlayerEntityFeature.PLAY
39 )
40 
41 PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
42  {
43  vol.Required(CONF_HOST): cv.string,
44  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
45  }
46 )
47 
48 NORMAL_INPUTS = {
49  "Cd": "CD",
50  "Dvd": "DVD",
51  "Blue ray": "BD",
52  "TV": "TV",
53  "Satellite / Cable": "SAT/CBL",
54  "Game": "GAME",
55  "Game2": "GAME2",
56  "Video Aux": "V.AUX",
57  "Dock": "DOCK",
58 }
59 
60 MEDIA_MODES = {
61  "Tuner": "TUNER",
62  "Media server": "SERVER",
63  "Ipod dock": "IPOD",
64  "Net/USB": "NET/USB",
65  "Rapsody": "RHAPSODY",
66  "Napster": "NAPSTER",
67  "Pandora": "PANDORA",
68  "LastFM": "LASTFM",
69  "Flickr": "FLICKR",
70  "Favorites": "FAVORITES",
71  "Internet Radio": "IRADIO",
72  "USB/IPOD": "USB/IPOD",
73 }
74 
75 # Sub-modes of 'NET/USB'
76 # {'USB': 'USB', 'iPod Direct': 'IPD', 'Internet Radio': 'IRP',
77 # 'Favorites': 'FVP'}
78 
79 
81  hass: HomeAssistant,
82  config: ConfigType,
83  add_entities: AddEntitiesCallback,
84  discovery_info: DiscoveryInfoType | None = None,
85 ) -> None:
86  """Set up the Denon platform."""
87  denon = DenonDevice(config[CONF_NAME], config[CONF_HOST])
88 
89  if denon.do_update():
90  add_entities([denon])
91 
92 
94  """Representation of a Denon device."""
95 
96  def __init__(self, name, host):
97  """Initialize the Denon device."""
98  self._name_name = name
99  self._host_host = host
100  self._pwstate_pwstate = "PWSTANDBY"
101  self._volume_volume = 0
102  # Initial value 60dB, changed if we get a MVMAX
103  self._volume_max_volume_max = 60
104  self._source_list_source_list = NORMAL_INPUTS.copy()
105  self._source_list_source_list.update(MEDIA_MODES)
106  self._muted_muted = False
107  self._mediasource_mediasource = ""
108  self._mediainfo_mediainfo = ""
109 
110  self._should_setup_sources_should_setup_sources = True
111 
112  def _setup_sources(self, telnet):
113  # NSFRN - Network name
114  nsfrn = self.telnet_requesttelnet_request(telnet, "NSFRN ?").removeprefix("NSFRN ")
115  if nsfrn:
116  self._name_name = nsfrn
117 
118  # SSFUN - Configured sources with (optional) names
119  self._source_list_source_list = {}
120  for line in self.telnet_requesttelnet_request(telnet, "SSFUN ?", all_lines=True):
121  ssfun = line.removeprefix("SSFUN").split(" ", 1)
122 
123  source = ssfun[0]
124  if len(ssfun) == 2 and ssfun[1]:
125  configured_name = ssfun[1]
126  else:
127  # No name configured, reusing the source name
128  configured_name = source
129 
130  self._source_list_source_list[configured_name] = source
131 
132  # SSSOD - Deleted sources
133  for line in self.telnet_requesttelnet_request(telnet, "SSSOD ?", all_lines=True):
134  source, status = line.removeprefix("SSSOD").split(" ", 1)
135  if status == "DEL":
136  for pretty_name, name in self._source_list_source_list.items():
137  if source == name:
138  del self._source_list_source_list[pretty_name]
139  break
140 
141  @classmethod
142  def telnet_request(cls, telnet, command, all_lines=False):
143  """Execute `command` and return the response."""
144  _LOGGER.debug("Sending: %s", command)
145  telnet.write(command.encode("ASCII") + b"\r")
146  lines = []
147  while True:
148  line = telnet.read_until(b"\r", timeout=0.2)
149  if not line:
150  break
151  lines.append(line.decode("ASCII").strip())
152  _LOGGER.debug("Received: %s", line)
153 
154  if all_lines:
155  return lines
156  return lines[0] if lines else ""
157 
158  def telnet_command(self, command):
159  """Establish a telnet connection and sends `command`."""
160  telnet = telnetlib.Telnet(self._host_host)
161  _LOGGER.debug("Sending: %s", command)
162  telnet.write(command.encode("ASCII") + b"\r")
163  telnet.read_very_eager() # skip response
164  telnet.close()
165 
166  def update(self) -> None:
167  """Get the latest details from the device."""
168  self.do_updatedo_update()
169 
170  def do_update(self) -> bool:
171  """Get the latest details from the device, as boolean."""
172  try:
173  telnet = telnetlib.Telnet(self._host_host)
174  except OSError:
175  return False
176 
177  if self._should_setup_sources_should_setup_sources:
178  self._setup_sources_setup_sources(telnet)
179  self._should_setup_sources_should_setup_sources = False
180 
181  self._pwstate_pwstate = self.telnet_requesttelnet_request(telnet, "PW?")
182  for line in self.telnet_requesttelnet_request(telnet, "MV?", all_lines=True):
183  if line.startswith("MVMAX "):
184  # only grab two digit max, don't care about any half digit
185  self._volume_max_volume_max = int(line[len("MVMAX ") : len("MVMAX XX")])
186  continue
187  if line.startswith("MV"):
188  self._volume_volume = int(line.removeprefix("MV"))
189  self._muted_muted = self.telnet_requesttelnet_request(telnet, "MU?") == "MUON"
190  self._mediasource_mediasource = self.telnet_requesttelnet_request(telnet, "SI?").removeprefix("SI")
191 
192  if self._mediasource_mediasource in MEDIA_MODES.values():
193  self._mediainfo_mediainfo = ""
194  answer_codes = [
195  "NSE0",
196  "NSE1X",
197  "NSE2X",
198  "NSE3X",
199  "NSE4",
200  "NSE5",
201  "NSE6",
202  "NSE7",
203  "NSE8",
204  ]
205  for line in self.telnet_requesttelnet_request(telnet, "NSE", all_lines=True):
206  self._mediainfo_mediainfo += f"{line.removeprefix(answer_codes.pop(0))}\n"
207  else:
208  self._mediainfo_mediainfo = self.sourcesourcesource
209 
210  telnet.close()
211  return True
212 
213  @property
214  def name(self):
215  """Return the name of the device."""
216  return self._name_name
217 
218  @property
219  def state(self) -> MediaPlayerState | None:
220  """Return the state of the device."""
221  if self._pwstate_pwstate == "PWSTANDBY":
222  return MediaPlayerState.OFF
223  if self._pwstate_pwstate == "PWON":
224  return MediaPlayerState.ON
225 
226  return None
227 
228  @property
229  def volume_level(self):
230  """Volume level of the media player (0..1)."""
231  return self._volume_volume / self._volume_max_volume_max
232 
233  @property
234  def is_volume_muted(self):
235  """Return boolean if volume is currently muted."""
236  return self._muted_muted
237 
238  @property
239  def source_list(self):
240  """Return the list of available input sources."""
241  return sorted(self._source_list_source_list)
242 
243  @property
244  def media_title(self):
245  """Return the current media info."""
246  return self._mediainfo_mediainfo
247 
248  @property
249  def supported_features(self) -> MediaPlayerEntityFeature:
250  """Flag media player features that are supported."""
251  if self._mediasource_mediasource in MEDIA_MODES.values():
252  return SUPPORT_DENON | SUPPORT_MEDIA_MODES
253  return SUPPORT_DENON
254 
255  @property
256  def source(self) -> str | None:
257  """Return the current input source."""
258  for pretty_name, name in self._source_list_source_list.items():
259  if self._mediasource_mediasource == name:
260  return pretty_name
261  return None
262 
263  def turn_off(self) -> None:
264  """Turn off media player."""
265  self.telnet_commandtelnet_command("PWSTANDBY")
266 
267  def volume_up(self) -> None:
268  """Volume up media player."""
269  self.telnet_commandtelnet_command("MVUP")
270 
271  def volume_down(self) -> None:
272  """Volume down media player."""
273  self.telnet_commandtelnet_command("MVDOWN")
274 
275  def set_volume_level(self, volume: float) -> None:
276  """Set volume level, range 0..1."""
277  self.telnet_commandtelnet_command(f"MV{round(volume * self._volume_max):02}")
278 
279  def mute_volume(self, mute: bool) -> None:
280  """Mute (true) or unmute (false) media player."""
281  mute_status = "ON" if mute else "OFF"
282  self.telnet_commandtelnet_command(f"MU{mute_status})")
283 
284  def media_play(self) -> None:
285  """Play media player."""
286  self.telnet_commandtelnet_command("NS9A")
287 
288  def media_pause(self) -> None:
289  """Pause media player."""
290  self.telnet_commandtelnet_command("NS9B")
291 
292  def media_stop(self) -> None:
293  """Pause media player."""
294  self.telnet_commandtelnet_command("NS9C")
295 
296  def media_next_track(self) -> None:
297  """Send the next track command."""
298  self.telnet_commandtelnet_command("NS9D")
299 
300  def media_previous_track(self) -> None:
301  """Send the previous track command."""
302  self.telnet_commandtelnet_command("NS9E")
303 
304  def turn_on(self) -> None:
305  """Turn the media player on."""
306  self.telnet_commandtelnet_command("PWON")
307 
308  def select_source(self, source: str) -> None:
309  """Select input source."""
310  self.telnet_commandtelnet_command(f"SI{self._source_list.get(source)}")
def telnet_request(cls, telnet, command, all_lines=False)
None add_entities(AsusWrtRouter router, AddEntitiesCallback async_add_entities, set[str] tracked)
None setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: media_player.py:85