From 84486bad788e26aad507b1992f76cae5a1084af0 Mon Sep 17 00:00:00 2001 From: "Phill (pssc)" Date: Fri, 26 Jul 2024 22:36:34 +0100 Subject: [PATCH] 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 --- homeassistant/components/yamaha/const.py | 1 + .../components/yamaha/media_player.py | 133 +++++++++++------- tests/components/yamaha/test_media_player.py | 5 +- 3 files changed, 86 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/yamaha/const.py b/homeassistant/components/yamaha/const.py index c0f4e34dd50..492babe9657 100644 --- a/homeassistant/components/yamaha/const.py +++ b/homeassistant/components/yamaha/const.py @@ -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" diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 1be7cb03e17..48dbcfffc97 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -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): diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index 02246e69269..66d0a42f256 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -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