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.""" """Constants for the Yamaha component."""
DOMAIN = "yamaha" DOMAIN = "yamaha"
KNOWN_ZONES = "known_zones"
CURSOR_TYPE_DOWN = "down" CURSOR_TYPE_DOWN = "down"
CURSOR_TYPE_LEFT = "left" CURSOR_TYPE_LEFT = "left"
CURSOR_TYPE_RETURN = "return" CURSOR_TYPE_RETURN = "return"

View File

@ -29,6 +29,8 @@ from .const import (
CURSOR_TYPE_RIGHT, CURSOR_TYPE_RIGHT,
CURSOR_TYPE_SELECT, CURSOR_TYPE_SELECT,
CURSOR_TYPE_UP, CURSOR_TYPE_UP,
DOMAIN,
KNOWN_ZONES,
SERVICE_ENABLE_OUTPUT, SERVICE_ENABLE_OUTPUT,
SERVICE_MENU_CURSOR, SERVICE_MENU_CURSOR,
SERVICE_SELECT_SCENE, SERVICE_SELECT_SCENE,
@ -55,7 +57,6 @@ CURSOR_TYPE_MAP = {
CURSOR_TYPE_SELECT: rxv.RXV.menu_sel.__name__, CURSOR_TYPE_SELECT: rxv.RXV.menu_sel.__name__,
CURSOR_TYPE_UP: rxv.RXV.menu_up.__name__, CURSOR_TYPE_UP: rxv.RXV.menu_up.__name__,
} }
DATA_YAMAHA = "yamaha_known_receivers"
DEFAULT_NAME = "Yamaha Receiver" DEFAULT_NAME = "Yamaha Receiver"
SUPPORT_YAMAHA = ( SUPPORT_YAMAHA = (
@ -99,6 +100,7 @@ class YamahaConfigInfo:
self.zone_ignore = config.get(CONF_ZONE_IGNORE) self.zone_ignore = config.get(CONF_ZONE_IGNORE)
self.zone_names = config.get(CONF_ZONE_NAMES) self.zone_names = config.get(CONF_ZONE_NAMES)
self.from_discovery = False self.from_discovery = False
_LOGGER.debug("Discovery Info: %s", discovery_info)
if discovery_info is not None: if discovery_info is not None:
self.name = discovery_info.get("name") self.name = discovery_info.get("name")
self.model = discovery_info.get("model_name") self.model = discovery_info.get("model_name")
@ -109,23 +111,26 @@ class YamahaConfigInfo:
def _discovery(config_info): 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: if config_info.from_discovery:
receivers = rxv.RXV( _LOGGER.debug("Discovery Zones")
zones = rxv.RXV(
config_info.ctrl_url, config_info.ctrl_url,
model_name=config_info.model, model_name=config_info.model,
friendly_name=config_info.name, friendly_name=config_info.name,
unit_desc_url=config_info.desc_url, unit_desc_url=config_info.desc_url,
).zone_controllers() ).zone_controllers()
_LOGGER.debug("Receivers: %s", receivers)
elif config_info.host is None: elif config_info.host is None:
receivers = [] _LOGGER.debug("Config No Host Supplied Zones")
zones = []
for recv in rxv.find(): for recv in rxv.find():
receivers.extend(recv.zone_controllers()) zones.extend(recv.zone_controllers())
else: 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( 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 # Keep track of configured receivers so that we don't end up
# discovering a receiver dynamically that we have static config # discovering a receiver dynamically that we have static config
# for. Map each device from its zone_id . # 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 # Get the Infos for configuration from config (YAML) or Discovery
config_info = YamahaConfigInfo(config=config, discovery_info=discovery_info) config_info = YamahaConfigInfo(config=config, discovery_info=discovery_info)
# Async check if the Receivers are there in the network # 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 = [] entities = []
for receiver in receivers: for zctrl in zone_ctrls:
if config_info.zone_ignore and receiver.zone in config_info.zone_ignore: _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 continue
entity = YamahaDevice( entity = YamahaDeviceZone(
config_info.name, config_info.name,
receiver, zctrl,
config_info.source_ignore, config_info.source_ignore,
config_info.source_names, config_info.source_names,
config_info.zone_names, config_info.zone_names,
@ -163,7 +171,9 @@ async def async_setup_platform(
known_zones.add(entity.zone_id) known_zones.add(entity.zone_id)
entities.append(entity) entities.append(entity)
else: 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) async_add_entities(entities)
@ -184,16 +194,16 @@ async def async_setup_platform(
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_MENU_CURSOR, SERVICE_MENU_CURSOR,
{vol.Required(ATTR_CURSOR): vol.In(CURSOR_TYPE_MAP)}, {vol.Required(ATTR_CURSOR): vol.In(CURSOR_TYPE_MAP)},
YamahaDevice.menu_cursor.__name__, YamahaDeviceZone.menu_cursor.__name__,
) )
class YamahaDevice(MediaPlayerEntity): class YamahaDeviceZone(MediaPlayerEntity):
"""Representation of a Yamaha device.""" """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.""" """Initialize the Yamaha Receiver."""
self.receiver = receiver self.zctrl = zctrl
self._attr_is_volume_muted = False self._attr_is_volume_muted = False
self._attr_volume_level = 0 self._attr_volume_level = 0
self._attr_state = MediaPlayerState.OFF self._attr_state = MediaPlayerState.OFF
@ -205,24 +215,38 @@ class YamahaDevice(MediaPlayerEntity):
self._is_playback_supported = False self._is_playback_supported = False
self._play_status = None self._play_status = None
self._name = name self._name = name
self._zone = receiver.zone self._zone = zctrl.zone
if self.receiver.serial_number is not None: if self.zctrl.serial_number is not None:
# Since not all receivers will have a serial number and set a unique id # Since not all receivers will have a serial number and set a unique id
# the default name of the integration may not be changed # the default name of the integration may not be changed
# to avoid a breaking change. # 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: def update(self) -> None:
"""Get the latest details from the device.""" """Get the latest details from the device."""
try: try:
self._play_status = self.receiver.play_status() self._play_status = self.zctrl.play_status()
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
_LOGGER.info("Receiver is offline: %s", self._name) _LOGGER.info("Receiver is offline: %s", self._name)
self._attr_available = False self._attr_available = False
return return
self._attr_available = True self._attr_available = True
if self.receiver.on: if self.zctrl.on:
if self._play_status is None: if self._play_status is None:
self._attr_state = MediaPlayerState.ON self._attr_state = MediaPlayerState.ON
elif self._play_status.playing: elif self._play_status.playing:
@ -232,21 +256,21 @@ class YamahaDevice(MediaPlayerEntity):
else: else:
self._attr_state = MediaPlayerState.OFF self._attr_state = MediaPlayerState.OFF
self._attr_is_volume_muted = self.receiver.mute self._attr_is_volume_muted = self.zctrl.mute
self._attr_volume_level = (self.receiver.volume / 100) + 1 self._attr_volume_level = (self.zctrl.volume / 100) + 1
if self.source_list is None: if self.source_list is None:
self.build_source_list() 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._attr_source = self._source_names.get(current_source, current_source)
self._playback_support = self.receiver.get_playback_support() self._playback_support = self.zctrl.get_playback_support()
self._is_playback_supported = self.receiver.is_playback_supported( self._is_playback_supported = self.zctrl.is_playback_supported(
self._attr_source self._attr_source
) )
surround_programs = self.receiver.surround_programs() surround_programs = self.zctrl.surround_programs()
if 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 self._attr_sound_mode_list = surround_programs
else: else:
self._attr_sound_mode = None self._attr_sound_mode = None
@ -260,10 +284,15 @@ class YamahaDevice(MediaPlayerEntity):
self._attr_source_list = sorted( self._attr_source_list = sorted(
self._source_names.get(source, source) 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 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 @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
@ -277,7 +306,7 @@ class YamahaDevice(MediaPlayerEntity):
@property @property
def zone_id(self): def zone_id(self):
"""Return a zone_id to ensure 1 media player per zone.""" """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 @property
def supported_features(self) -> MediaPlayerEntityFeature: def supported_features(self) -> MediaPlayerEntityFeature:
@ -301,42 +330,42 @@ class YamahaDevice(MediaPlayerEntity):
def turn_off(self) -> None: def turn_off(self) -> None:
"""Turn off media player.""" """Turn off media player."""
self.receiver.on = False self.zctrl.on = False
def set_volume_level(self, volume: float) -> None: def set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1.""" """Set volume level, range 0..1."""
receiver_vol = 100 - (volume * 100) zone_vol = 100 - (volume * 100)
negative_receiver_vol = -receiver_vol negative_zone_vol = -zone_vol
self.receiver.volume = negative_receiver_vol self.zctrl.volume = negative_zone_vol
def mute_volume(self, mute: bool) -> None: def mute_volume(self, mute: bool) -> None:
"""Mute (true) or unmute (false) media player.""" """Mute (true) or unmute (false) media player."""
self.receiver.mute = mute self.zctrl.mute = mute
def turn_on(self) -> None: def turn_on(self) -> None:
"""Turn the media player on.""" """Turn the media player on."""
self.receiver.on = True self.zctrl.on = True
self._attr_volume_level = (self.receiver.volume / 100) + 1 self._attr_volume_level = (self.zctrl.volume / 100) + 1
def media_play(self) -> None: def media_play(self) -> None:
"""Send play command.""" """Send play command."""
self._call_playback_function(self.receiver.play, "play") self._call_playback_function(self.zctrl.play, "play")
def media_pause(self) -> None: def media_pause(self) -> None:
"""Send pause command.""" """Send pause command."""
self._call_playback_function(self.receiver.pause, "pause") self._call_playback_function(self.zctrl.pause, "pause")
def media_stop(self) -> None: def media_stop(self) -> None:
"""Send stop command.""" """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: def media_previous_track(self) -> None:
"""Send previous track command.""" """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: def media_next_track(self) -> None:
"""Send next track command.""" """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): def _call_playback_function(self, function, function_text):
try: try:
@ -346,7 +375,7 @@ class YamahaDevice(MediaPlayerEntity):
def select_source(self, source: str) -> None: def select_source(self, source: str) -> None:
"""Select input source.""" """Select input source."""
self.receiver.input = self._reverse_mapping.get(source, source) self.zctrl.input = self._reverse_mapping.get(source, source)
def play_media( def play_media(
self, media_type: MediaType | str, media_id: str, **kwargs: Any 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. menu must be fetched by the receiver from the vtuner service.
""" """
if media_type == "NET RADIO": if media_type == "NET RADIO":
self.receiver.net_radio(media_id) self.zctrl.net_radio(media_id)
def enable_output(self, port, enabled): def enable_output(self, port, enabled):
"""Enable or disable an output port..""" """Enable or disable an output port.."""
self.receiver.enable_output(port, enabled) self.zctrl.enable_output(port, enabled)
def menu_cursor(self, cursor): def menu_cursor(self, cursor):
"""Press a menu cursor button.""" """Press a menu cursor button."""
getattr(self.receiver, CURSOR_TYPE_MAP[cursor])() getattr(self.zctrl, CURSOR_TYPE_MAP[cursor])()
def set_scene(self, scene): def set_scene(self, scene):
"""Set the current scene.""" """Set the current scene."""
try: try:
self.receiver.scene = scene self.zctrl.scene = scene
except AssertionError: except AssertionError:
_LOGGER.warning("Scene '%s' does not exist!", scene) _LOGGER.warning("Scene '%s' does not exist!", scene)
def select_sound_mode(self, sound_mode: str) -> None: def select_sound_mode(self, sound_mode: str) -> None:
"""Set Sound Mode for Receiver..""" """Set Sound Mode for Receiver.."""
self.receiver.surround_program = sound_mode self.zctrl.surround_program = sound_mode
@property @property
def media_artist(self): def media_artist(self):

View File

@ -46,7 +46,10 @@ def main_zone_fixture():
def device_fixture(main_zone): def device_fixture(main_zone):
"""Mock the yamaha device.""" """Mock the yamaha device."""
device = FakeYamahaDevice("http://receiver", "Receiver", zones=[main_zone]) 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 yield device