1 """Support for Yamaha Receivers."""
3 from __future__
import annotations
11 import voluptuous
as vol
14 PLATFORM_SCHEMA
as MEDIA_PLAYER_PLATFORM_SCHEMA,
16 MediaPlayerEntityFeature,
37 SERVICE_ENABLE_OUTPUT,
42 _LOGGER = logging.getLogger(__name__)
44 ATTR_CURSOR =
"cursor"
45 ATTR_ENABLED =
"enabled"
50 CONF_SOURCE_IGNORE =
"source_ignore"
51 CONF_SOURCE_NAMES =
"source_names"
52 CONF_ZONE_IGNORE =
"zone_ignore"
53 CONF_ZONE_NAMES =
"zone_names"
56 CURSOR_TYPE_DOWN: rxv.RXV.menu_down.__name__,
57 CURSOR_TYPE_LEFT: rxv.RXV.menu_left.__name__,
58 CURSOR_TYPE_RETURN: rxv.RXV.menu_return.__name__,
59 CURSOR_TYPE_RIGHT: rxv.RXV.menu_right.__name__,
60 CURSOR_TYPE_SELECT: rxv.RXV.menu_sel.__name__,
61 CURSOR_TYPE_UP: rxv.RXV.menu_up.__name__,
63 DEFAULT_NAME =
"Yamaha Receiver"
66 MediaPlayerEntityFeature.VOLUME_SET
67 | MediaPlayerEntityFeature.VOLUME_MUTE
68 | MediaPlayerEntityFeature.TURN_ON
69 | MediaPlayerEntityFeature.TURN_OFF
70 | MediaPlayerEntityFeature.SELECT_SOURCE
71 | MediaPlayerEntityFeature.PLAY
72 | MediaPlayerEntityFeature.SELECT_SOUND_MODE
75 PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
77 vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
78 vol.Optional(CONF_HOST): cv.string,
79 vol.Optional(CONF_SOURCE_IGNORE, default=[]): vol.All(
80 cv.ensure_list, [cv.string]
82 vol.Optional(CONF_ZONE_IGNORE, default=[]): vol.All(
83 cv.ensure_list, [cv.string]
85 vol.Optional(CONF_SOURCE_NAMES, default={}): {cv.string: cv.string},
86 vol.Optional(CONF_ZONE_NAMES, default={}): {cv.string: cv.string},
92 """Configuration Info for Yamaha Receivers."""
95 self, config: ConfigType, discovery_info: DiscoveryInfoType |
None
97 """Initialize the Configuration Info for Yamaha Receiver."""
98 self.
namename = config.get(CONF_NAME)
99 self.
hosthost = config.get(CONF_HOST)
100 self.
ctrl_urlctrl_url: str |
None = f
"http://{self.host}:80/YamahaRemoteControl/ctrl"
106 _LOGGER.debug(
"Discovery Info: %s", discovery_info)
107 if discovery_info
is not None:
108 self.
namename = discovery_info.get(
"name")
109 self.
modelmodel = discovery_info.get(
"model_name")
110 self.
ctrl_urlctrl_url = discovery_info.get(
"control_url")
111 self.
desc_urldesc_url = discovery_info.get(
"description_url")
117 """Discover list of zone controllers from configuration in the network."""
118 if config_info.from_discovery:
119 _LOGGER.debug(
"Discovery Zones")
121 config_info.ctrl_url,
122 model_name=config_info.model,
123 friendly_name=config_info.name,
124 unit_desc_url=config_info.desc_url,
126 elif config_info.host
is None:
127 _LOGGER.debug(
"Config No Host Supplied Zones")
129 for recv
in rxv.find(DISCOVER_TIMEOUT):
130 zones.extend(recv.zone_controllers())
132 _LOGGER.debug(
"Config Zones")
133 zones = rxv.RXV(config_info.ctrl_url, config_info.name).zone_controllers()
135 _LOGGER.debug(
"Returned _discover zones: %s", zones)
142 async_add_entities: AddEntitiesCallback,
143 discovery_info: DiscoveryInfoType |
None =
None,
145 """Set up the Yamaha platform."""
149 known_zones = hass.data.setdefault(DOMAIN, {KNOWN_ZONES: set()})[KNOWN_ZONES]
150 _LOGGER.debug(
"Known receiver zones: %s", known_zones)
153 config_info =
YamahaConfigInfo(config=config, discovery_info=discovery_info)
156 zone_ctrls = await hass.async_add_executor_job(_discovery, config_info)
157 except requests.exceptions.ConnectionError
as ex:
158 raise PlatformNotReady(f
"Issue while connecting to {config_info.name}")
from ex
161 for zctrl
in zone_ctrls:
162 _LOGGER.debug(
"Receiver zone: %s serial %s", zctrl.zone, zctrl.serial_number)
163 if config_info.zone_ignore
and zctrl.zone
in config_info.zone_ignore:
164 _LOGGER.debug(
"Ignore receiver zone: %s %s", config_info.name, zctrl.zone)
167 assert config_info.name
171 config_info.source_ignore,
172 config_info.source_names,
173 config_info.zone_names,
177 if entity.zone_id
not in known_zones:
178 known_zones.add(entity.zone_id)
179 entities.append(entity)
182 "Ignoring duplicate zone: %s %s", config_info.name, zctrl.zone
188 platform = entity_platform.async_get_current_platform()
189 platform.async_register_entity_service(
190 SERVICE_SELECT_SCENE,
191 {vol.Required(ATTR_SCENE): cv.string},
195 platform.async_register_entity_service(
196 SERVICE_ENABLE_OUTPUT,
197 {vol.Required(ATTR_ENABLED): cv.boolean, vol.Required(ATTR_PORT): cv.string},
201 platform.async_register_entity_service(
203 {vol.Required(ATTR_CURSOR): vol.In(CURSOR_TYPE_MAP)},
204 YamahaDeviceZone.menu_cursor.__name__,
209 """Representation of a Yamaha device zone."""
211 _reverse_mapping: dict[str, str]
217 source_ignore: list[str] |
None,
218 source_names: dict[str, str] |
None,
219 zone_names: dict[str, str] |
None,
221 """Initialize the Yamaha Receiver."""
226 self._source_ignore: list[str] = source_ignore
or []
227 self._source_names: dict[str, str] = source_names
or {}
228 self._zone_names: dict[str, str] = zone_names
or {}
234 if self.
zctrlzctrl.serial_number
is not None:
241 """Get the latest details from the device."""
244 except requests.exceptions.ConnectionError:
245 _LOGGER.debug(
"Receiver is offline: %s", self.
_name_name)
250 if self.
zctrlzctrl.on:
254 self.
_attr_state_attr_state = MediaPlayerState.PLAYING
256 self.
_attr_state_attr_state = MediaPlayerState.IDLE
266 current_source = self.
zctrlzctrl.input
267 self.
_attr_source_attr_source = self._source_names.
get(current_source, current_source)
272 surround_programs = self.
zctrlzctrl.surround_programs()
273 if surround_programs:
281 """Build the source list."""
283 alias: source
for source, alias
in self._source_names.items()
287 self._source_names.
get(source, source)
288 for source
in self.
zctrlzctrl.inputs()
289 if source
not in self._source_ignore
294 """Return the name of the device."""
295 name = self.
_name_name
296 zone_name = self._zone_names.
get(self.
_zone_zone, self.
_zone_zone)
297 if zone_name !=
"Main_Zone":
299 name += f
" {zone_name.replace('_', ' ')}"
304 """Return a zone_id to ensure 1 media player per zone."""
305 return f
"{self.zctrl.ctrl_url}:{self._zone}"
309 """Flag media player features that are supported."""
310 supported_features = SUPPORT_YAMAHA
315 MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PLAY_MEDIA
317 "pause": MediaPlayerEntityFeature.PAUSE,
318 "stop": MediaPlayerEntityFeature.STOP,
319 "skip_f": MediaPlayerEntityFeature.NEXT_TRACK,
320 "skip_r": MediaPlayerEntityFeature.PREVIOUS_TRACK,
322 for attr, feature
in mapping.items():
323 if getattr(supports, attr,
False):
324 supported_features |= feature
325 return supported_features
328 """Turn off media player."""
329 self.
zctrlzctrl.on =
False
332 """Set volume level, range 0..1."""
333 zone_vol = 100 - (volume * 100)
334 negative_zone_vol = -zone_vol
335 self.
zctrlzctrl.volume = negative_zone_vol
338 """Mute (true) or unmute (false) media player."""
339 self.
zctrlzctrl.mute = mute
342 """Turn the media player on."""
343 self.
zctrlzctrl.on =
True
347 """Send play command."""
351 """Send pause command."""
355 """Send stop command."""
359 """Send previous track command."""
363 """Send next track command."""
369 except rxv.exceptions.ResponseException:
370 _LOGGER.warning(
"Failed to execute %s on %s", function_text, self.
_name_name)
373 """Select input source."""
377 self, media_type: MediaType | str, media_id: str, **kwargs: Any
379 """Play media from an ID.
381 This exposes a pass through for various input sources in the
382 Yamaha to direct play certain kinds of media. media_type is
383 treated as the input type that we are setting, and media id is
385 For the NET RADIO mediatype the format for ``media_id`` is a
386 "path" in your vtuner hierarchy. For instance:
387 ``Bookmarks>Internet>Radio Paradise``. The separators are
388 ``>`` and the parts of this are navigated by name behind the
389 scenes. There is a looping construct built into the yamaha
390 library to do this with a fallback timeout if the vtuner
391 service is unresponsive.
392 NOTE: this might take a while, because the only API interface
393 for setting the net radio station emulates button pressing and
394 navigating through the net radio menu hierarchy. And each sub
395 menu must be fetched by the receiver from the vtuner service.
397 if media_type ==
"NET RADIO":
398 self.
zctrlzctrl.net_radio(media_id)
401 """Enable or disable an output port.."""
405 """Press a menu cursor button."""
406 getattr(self.
zctrlzctrl, CURSOR_TYPE_MAP[cursor])()
409 """Set the current scene."""
411 self.
zctrlzctrl.scene = scene
412 except AssertionError:
413 _LOGGER.warning(
"Scene '%s' does not exist!", scene)
416 """Set Sound Mode for Receiver.."""
417 self.
zctrlzctrl.surround_program = sound_mode
421 """Artist of current playing media."""
428 """Album of current playing media."""
435 """Content type of current playing media."""
438 return MediaType.MUSIC
443 """Artist of current playing media."""
451 return f
"{station}: {song}"
453 return song
or station
web.Response get(self, web.Request request, str config_key)