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:
Phill (pssc) 2024-07-26 22:36:34 +01:00 committed by GitHub
parent c486baccaa
commit 84486bad78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 86 additions and 53 deletions

View File

@ -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"

View File

@ -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):

View File

@ -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