mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Yamaha device setup enhancement with unique id based on serial (#120764)
* fix server unavailale at HA startup Fixes #111108 Remove receiver zone confusion for mediaplayer instances fix uniq id based on serial where avaialble get serial suppiled by discovery for config entries. * Fix linter errors * ruff format * Enhance debug to find setup code path for tests * Enhance debug to find setup code path for tests * Fix formatting * Revered uid chanages as not needed yet and cuases other issues * Revert "Fix formatting" This reverts commit f3324868d25261a1466233eeb804f526a0023ca1. * Fix formatting * Refector tests to cope with changes to plaform init to get serial numbers * Update test patch * Update test formatting * remove all fixes revert code to only make clear we deal with zones and improve debuging
This commit is contained in:
parent
c486baccaa
commit
84486bad78
@ -1,6 +1,7 @@
|
||||
"""Constants for the Yamaha component."""
|
||||
|
||||
DOMAIN = "yamaha"
|
||||
KNOWN_ZONES = "known_zones"
|
||||
CURSOR_TYPE_DOWN = "down"
|
||||
CURSOR_TYPE_LEFT = "left"
|
||||
CURSOR_TYPE_RETURN = "return"
|
||||
|
@ -29,6 +29,8 @@ from .const import (
|
||||
CURSOR_TYPE_RIGHT,
|
||||
CURSOR_TYPE_SELECT,
|
||||
CURSOR_TYPE_UP,
|
||||
DOMAIN,
|
||||
KNOWN_ZONES,
|
||||
SERVICE_ENABLE_OUTPUT,
|
||||
SERVICE_MENU_CURSOR,
|
||||
SERVICE_SELECT_SCENE,
|
||||
@ -55,7 +57,6 @@ CURSOR_TYPE_MAP = {
|
||||
CURSOR_TYPE_SELECT: rxv.RXV.menu_sel.__name__,
|
||||
CURSOR_TYPE_UP: rxv.RXV.menu_up.__name__,
|
||||
}
|
||||
DATA_YAMAHA = "yamaha_known_receivers"
|
||||
DEFAULT_NAME = "Yamaha Receiver"
|
||||
|
||||
SUPPORT_YAMAHA = (
|
||||
@ -99,6 +100,7 @@ class YamahaConfigInfo:
|
||||
self.zone_ignore = config.get(CONF_ZONE_IGNORE)
|
||||
self.zone_names = config.get(CONF_ZONE_NAMES)
|
||||
self.from_discovery = False
|
||||
_LOGGER.debug("Discovery Info: %s", discovery_info)
|
||||
if discovery_info is not None:
|
||||
self.name = discovery_info.get("name")
|
||||
self.model = discovery_info.get("model_name")
|
||||
@ -109,23 +111,26 @@ class YamahaConfigInfo:
|
||||
|
||||
|
||||
def _discovery(config_info):
|
||||
"""Discover receivers from configuration in the network."""
|
||||
"""Discover list of zone controllers from configuration in the network."""
|
||||
if config_info.from_discovery:
|
||||
receivers = rxv.RXV(
|
||||
_LOGGER.debug("Discovery Zones")
|
||||
zones = rxv.RXV(
|
||||
config_info.ctrl_url,
|
||||
model_name=config_info.model,
|
||||
friendly_name=config_info.name,
|
||||
unit_desc_url=config_info.desc_url,
|
||||
).zone_controllers()
|
||||
_LOGGER.debug("Receivers: %s", receivers)
|
||||
elif config_info.host is None:
|
||||
receivers = []
|
||||
_LOGGER.debug("Config No Host Supplied Zones")
|
||||
zones = []
|
||||
for recv in rxv.find():
|
||||
receivers.extend(recv.zone_controllers())
|
||||
zones.extend(recv.zone_controllers())
|
||||
else:
|
||||
receivers = rxv.RXV(config_info.ctrl_url, config_info.name).zone_controllers()
|
||||
_LOGGER.debug("Config Zones Fallback")
|
||||
zones = rxv.RXV(config_info.ctrl_url, config_info.name).zone_controllers()
|
||||
|
||||
return receivers
|
||||
_LOGGER.debug("Returned _discover zones: %s", zones)
|
||||
return zones
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
@ -138,21 +143,24 @@ async def async_setup_platform(
|
||||
# Keep track of configured receivers so that we don't end up
|
||||
# discovering a receiver dynamically that we have static config
|
||||
# for. Map each device from its zone_id .
|
||||
known_zones = hass.data.setdefault(DATA_YAMAHA, set())
|
||||
known_zones = hass.data.setdefault(DOMAIN, {KNOWN_ZONES: set()})[KNOWN_ZONES]
|
||||
_LOGGER.debug("Known receiver zones: %s", known_zones)
|
||||
|
||||
# Get the Infos for configuration from config (YAML) or Discovery
|
||||
config_info = YamahaConfigInfo(config=config, discovery_info=discovery_info)
|
||||
# Async check if the Receivers are there in the network
|
||||
receivers = await hass.async_add_executor_job(_discovery, config_info)
|
||||
zone_ctrls = await hass.async_add_executor_job(_discovery, config_info)
|
||||
|
||||
entities = []
|
||||
for receiver in receivers:
|
||||
if config_info.zone_ignore and receiver.zone in config_info.zone_ignore:
|
||||
for zctrl in zone_ctrls:
|
||||
_LOGGER.info("Receiver zone: %s", zctrl.zone)
|
||||
if config_info.zone_ignore and zctrl.zone in config_info.zone_ignore:
|
||||
_LOGGER.debug("Ignore receiver zone: %s %s", config_info.name, zctrl.zone)
|
||||
continue
|
||||
|
||||
entity = YamahaDevice(
|
||||
entity = YamahaDeviceZone(
|
||||
config_info.name,
|
||||
receiver,
|
||||
zctrl,
|
||||
config_info.source_ignore,
|
||||
config_info.source_names,
|
||||
config_info.zone_names,
|
||||
@ -163,7 +171,9 @@ async def async_setup_platform(
|
||||
known_zones.add(entity.zone_id)
|
||||
entities.append(entity)
|
||||
else:
|
||||
_LOGGER.debug("Ignoring duplicate receiver: %s", config_info.name)
|
||||
_LOGGER.debug(
|
||||
"Ignoring duplicate zone: %s %s", config_info.name, zctrl.zone
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@ -184,16 +194,16 @@ async def async_setup_platform(
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_MENU_CURSOR,
|
||||
{vol.Required(ATTR_CURSOR): vol.In(CURSOR_TYPE_MAP)},
|
||||
YamahaDevice.menu_cursor.__name__,
|
||||
YamahaDeviceZone.menu_cursor.__name__,
|
||||
)
|
||||
|
||||
|
||||
class YamahaDevice(MediaPlayerEntity):
|
||||
"""Representation of a Yamaha device."""
|
||||
class YamahaDeviceZone(MediaPlayerEntity):
|
||||
"""Representation of a Yamaha device zone."""
|
||||
|
||||
def __init__(self, name, receiver, source_ignore, source_names, zone_names):
|
||||
def __init__(self, name, zctrl, source_ignore, source_names, zone_names):
|
||||
"""Initialize the Yamaha Receiver."""
|
||||
self.receiver = receiver
|
||||
self.zctrl = zctrl
|
||||
self._attr_is_volume_muted = False
|
||||
self._attr_volume_level = 0
|
||||
self._attr_state = MediaPlayerState.OFF
|
||||
@ -205,24 +215,38 @@ class YamahaDevice(MediaPlayerEntity):
|
||||
self._is_playback_supported = False
|
||||
self._play_status = None
|
||||
self._name = name
|
||||
self._zone = receiver.zone
|
||||
if self.receiver.serial_number is not None:
|
||||
self._zone = zctrl.zone
|
||||
if self.zctrl.serial_number is not None:
|
||||
# Since not all receivers will have a serial number and set a unique id
|
||||
# the default name of the integration may not be changed
|
||||
# to avoid a breaking change.
|
||||
self._attr_unique_id = f"{self.receiver.serial_number}_{self._zone}"
|
||||
# Prefix as MusicCast could have used this
|
||||
self._attr_unique_id = f"{self.zctrl.serial_number}_{self._zone}"
|
||||
_LOGGER.debug(
|
||||
"Receiver zone: %s zone %s uid %s",
|
||||
self._name,
|
||||
self._zone,
|
||||
self._attr_unique_id,
|
||||
)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Receiver zone: %s zone %s no uid %s",
|
||||
self._name,
|
||||
self._zone,
|
||||
self._attr_unique_id,
|
||||
)
|
||||
|
||||
def update(self) -> None:
|
||||
"""Get the latest details from the device."""
|
||||
try:
|
||||
self._play_status = self.receiver.play_status()
|
||||
self._play_status = self.zctrl.play_status()
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.info("Receiver is offline: %s", self._name)
|
||||
self._attr_available = False
|
||||
return
|
||||
|
||||
self._attr_available = True
|
||||
if self.receiver.on:
|
||||
if self.zctrl.on:
|
||||
if self._play_status is None:
|
||||
self._attr_state = MediaPlayerState.ON
|
||||
elif self._play_status.playing:
|
||||
@ -232,21 +256,21 @@ class YamahaDevice(MediaPlayerEntity):
|
||||
else:
|
||||
self._attr_state = MediaPlayerState.OFF
|
||||
|
||||
self._attr_is_volume_muted = self.receiver.mute
|
||||
self._attr_volume_level = (self.receiver.volume / 100) + 1
|
||||
self._attr_is_volume_muted = self.zctrl.mute
|
||||
self._attr_volume_level = (self.zctrl.volume / 100) + 1
|
||||
|
||||
if self.source_list is None:
|
||||
self.build_source_list()
|
||||
|
||||
current_source = self.receiver.input
|
||||
current_source = self.zctrl.input
|
||||
self._attr_source = self._source_names.get(current_source, current_source)
|
||||
self._playback_support = self.receiver.get_playback_support()
|
||||
self._is_playback_supported = self.receiver.is_playback_supported(
|
||||
self._playback_support = self.zctrl.get_playback_support()
|
||||
self._is_playback_supported = self.zctrl.is_playback_supported(
|
||||
self._attr_source
|
||||
)
|
||||
surround_programs = self.receiver.surround_programs()
|
||||
surround_programs = self.zctrl.surround_programs()
|
||||
if surround_programs:
|
||||
self._attr_sound_mode = self.receiver.surround_program
|
||||
self._attr_sound_mode = self.zctrl.surround_program
|
||||
self._attr_sound_mode_list = surround_programs
|
||||
else:
|
||||
self._attr_sound_mode = None
|
||||
@ -260,10 +284,15 @@ class YamahaDevice(MediaPlayerEntity):
|
||||
|
||||
self._attr_source_list = sorted(
|
||||
self._source_names.get(source, source)
|
||||
for source in self.receiver.inputs()
|
||||
for source in self.zctrl.inputs()
|
||||
if source not in self._source_ignore
|
||||
)
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique ID for this media_player."""
|
||||
return self._attr_unique_id or ""
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
@ -277,7 +306,7 @@ class YamahaDevice(MediaPlayerEntity):
|
||||
@property
|
||||
def zone_id(self):
|
||||
"""Return a zone_id to ensure 1 media player per zone."""
|
||||
return f"{self.receiver.ctrl_url}:{self._zone}"
|
||||
return f"{self.zctrl.ctrl_url}:{self._zone}"
|
||||
|
||||
@property
|
||||
def supported_features(self) -> MediaPlayerEntityFeature:
|
||||
@ -301,42 +330,42 @@ class YamahaDevice(MediaPlayerEntity):
|
||||
|
||||
def turn_off(self) -> None:
|
||||
"""Turn off media player."""
|
||||
self.receiver.on = False
|
||||
self.zctrl.on = False
|
||||
|
||||
def set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
receiver_vol = 100 - (volume * 100)
|
||||
negative_receiver_vol = -receiver_vol
|
||||
self.receiver.volume = negative_receiver_vol
|
||||
zone_vol = 100 - (volume * 100)
|
||||
negative_zone_vol = -zone_vol
|
||||
self.zctrl.volume = negative_zone_vol
|
||||
|
||||
def mute_volume(self, mute: bool) -> None:
|
||||
"""Mute (true) or unmute (false) media player."""
|
||||
self.receiver.mute = mute
|
||||
self.zctrl.mute = mute
|
||||
|
||||
def turn_on(self) -> None:
|
||||
"""Turn the media player on."""
|
||||
self.receiver.on = True
|
||||
self._attr_volume_level = (self.receiver.volume / 100) + 1
|
||||
self.zctrl.on = True
|
||||
self._attr_volume_level = (self.zctrl.volume / 100) + 1
|
||||
|
||||
def media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
self._call_playback_function(self.receiver.play, "play")
|
||||
self._call_playback_function(self.zctrl.play, "play")
|
||||
|
||||
def media_pause(self) -> None:
|
||||
"""Send pause command."""
|
||||
self._call_playback_function(self.receiver.pause, "pause")
|
||||
self._call_playback_function(self.zctrl.pause, "pause")
|
||||
|
||||
def media_stop(self) -> None:
|
||||
"""Send stop command."""
|
||||
self._call_playback_function(self.receiver.stop, "stop")
|
||||
self._call_playback_function(self.zctrl.stop, "stop")
|
||||
|
||||
def media_previous_track(self) -> None:
|
||||
"""Send previous track command."""
|
||||
self._call_playback_function(self.receiver.previous, "previous track")
|
||||
self._call_playback_function(self.zctrl.previous, "previous track")
|
||||
|
||||
def media_next_track(self) -> None:
|
||||
"""Send next track command."""
|
||||
self._call_playback_function(self.receiver.next, "next track")
|
||||
self._call_playback_function(self.zctrl.next, "next track")
|
||||
|
||||
def _call_playback_function(self, function, function_text):
|
||||
try:
|
||||
@ -346,7 +375,7 @@ class YamahaDevice(MediaPlayerEntity):
|
||||
|
||||
def select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
self.receiver.input = self._reverse_mapping.get(source, source)
|
||||
self.zctrl.input = self._reverse_mapping.get(source, source)
|
||||
|
||||
def play_media(
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
@ -370,26 +399,26 @@ class YamahaDevice(MediaPlayerEntity):
|
||||
menu must be fetched by the receiver from the vtuner service.
|
||||
"""
|
||||
if media_type == "NET RADIO":
|
||||
self.receiver.net_radio(media_id)
|
||||
self.zctrl.net_radio(media_id)
|
||||
|
||||
def enable_output(self, port, enabled):
|
||||
"""Enable or disable an output port.."""
|
||||
self.receiver.enable_output(port, enabled)
|
||||
self.zctrl.enable_output(port, enabled)
|
||||
|
||||
def menu_cursor(self, cursor):
|
||||
"""Press a menu cursor button."""
|
||||
getattr(self.receiver, CURSOR_TYPE_MAP[cursor])()
|
||||
getattr(self.zctrl, CURSOR_TYPE_MAP[cursor])()
|
||||
|
||||
def set_scene(self, scene):
|
||||
"""Set the current scene."""
|
||||
try:
|
||||
self.receiver.scene = scene
|
||||
self.zctrl.scene = scene
|
||||
except AssertionError:
|
||||
_LOGGER.warning("Scene '%s' does not exist!", scene)
|
||||
|
||||
def select_sound_mode(self, sound_mode: str) -> None:
|
||||
"""Set Sound Mode for Receiver.."""
|
||||
self.receiver.surround_program = sound_mode
|
||||
self.zctrl.surround_program = sound_mode
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
|
@ -46,7 +46,10 @@ def main_zone_fixture():
|
||||
def device_fixture(main_zone):
|
||||
"""Mock the yamaha device."""
|
||||
device = FakeYamahaDevice("http://receiver", "Receiver", zones=[main_zone])
|
||||
with patch("rxv.RXV", return_value=device):
|
||||
with (
|
||||
patch("rxv.RXV", return_value=device),
|
||||
patch("rxv.find", return_value=[device]),
|
||||
):
|
||||
yield device
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user