mirror of
https://github.com/home-assistant/core.git
synced 2026-06-18 09:52:57 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cd1cb0c64 | |||
| f635ccd30b |
Generated
+2
@@ -1063,6 +1063,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/madvr/ @iloveicedgreentea
|
||||
/homeassistant/components/marantz_infrared/ @balloob
|
||||
/tests/components/marantz_infrared/ @balloob
|
||||
/homeassistant/components/marantz_rs232/ @balloob
|
||||
/tests/components/marantz_rs232/ @balloob
|
||||
/homeassistant/components/mastodon/ @fabaff @andrew-codechimp
|
||||
/tests/components/mastodon/ @fabaff @andrew-codechimp
|
||||
/homeassistant/components/matrix/ @PaarthShah
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""The Marantz RS-232 integration."""
|
||||
|
||||
from marantz_rs232 import (
|
||||
MarantzV2003Receiver,
|
||||
MarantzV2007Receiver,
|
||||
MarantzV2015Receiver,
|
||||
V2003ReceiverState,
|
||||
V2007ReceiverState,
|
||||
V2015ReceiverState,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_DEVICE, CONF_MODEL, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .config_flow import MODEL_MODERN, V2003_MODELS, V2007_MODELS
|
||||
from .const import LOGGER, MarantzReceiver, MarantzRS232ConfigEntry
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: MarantzRS232ConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Marantz RS-232 from a config entry."""
|
||||
port = entry.data[CONF_DEVICE]
|
||||
model_key = entry.data[CONF_MODEL]
|
||||
|
||||
receiver: MarantzReceiver
|
||||
if model_key == MODEL_MODERN:
|
||||
receiver = MarantzV2015Receiver(port)
|
||||
elif model_key in V2003_MODELS:
|
||||
receiver = MarantzV2003Receiver(port)
|
||||
else:
|
||||
receiver = MarantzV2007Receiver(port, model=V2007_MODELS[model_key])
|
||||
|
||||
try:
|
||||
await receiver.connect()
|
||||
await receiver.query_state()
|
||||
except (ConnectionError, OSError, TimeoutError) as err:
|
||||
LOGGER.error("Error connecting to Marantz receiver at %s: %s", port, err)
|
||||
if receiver.connected:
|
||||
await receiver.disconnect()
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
entry.runtime_data = receiver
|
||||
|
||||
@callback
|
||||
def _on_disconnect(
|
||||
state: V2015ReceiverState | V2007ReceiverState | V2003ReceiverState | None,
|
||||
) -> None:
|
||||
# Only reload if the entry is still loaded. During entry removal,
|
||||
# disconnect() fires this callback but the entry is already gone.
|
||||
if state is None and entry.state is ConfigEntryState.LOADED:
|
||||
LOGGER.warning("Marantz receiver disconnected, reloading config entry")
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(receiver.subscribe(_on_disconnect))
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: MarantzRS232ConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
await entry.runtime_data.disconnect()
|
||||
|
||||
return unload_ok
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Config flow for the Marantz RS-232 integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from marantz_rs232 import (
|
||||
MarantzV2003Receiver,
|
||||
MarantzV2007Receiver,
|
||||
MarantzV2015Receiver,
|
||||
V2007Model,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_DEVICE, CONF_MODEL
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
SerialPortSelector,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
MODEL_MODERN = "modern"
|
||||
MODEL_SR7002 = "sr7002"
|
||||
MODEL_SR8002 = "sr8002"
|
||||
MODEL_SR9300 = "sr9300"
|
||||
MODEL_SR8300 = "sr8300"
|
||||
|
||||
MODEL_NAMES: dict[str, str] = {
|
||||
MODEL_MODERN: "Modern",
|
||||
MODEL_SR7002: "SR7002",
|
||||
MODEL_SR8002: "SR8002",
|
||||
MODEL_SR9300: "SR9300",
|
||||
MODEL_SR8300: "SR8300",
|
||||
}
|
||||
|
||||
V2007_MODELS: dict[str, V2007Model] = {
|
||||
MODEL_SR7002: V2007Model.SR7002,
|
||||
MODEL_SR8002: V2007Model.SR8002,
|
||||
}
|
||||
|
||||
V2003_MODELS = frozenset({MODEL_SR9300, MODEL_SR8300})
|
||||
|
||||
|
||||
async def _async_attempt_connect(port: str, model_key: str) -> str | None:
|
||||
"""Attempt to connect to the receiver at the given port.
|
||||
|
||||
Returns None on success, error on failure.
|
||||
"""
|
||||
receiver: MarantzV2015Receiver | MarantzV2007Receiver | MarantzV2003Receiver
|
||||
if model_key == MODEL_MODERN:
|
||||
receiver = MarantzV2015Receiver(port)
|
||||
elif model_key in V2003_MODELS:
|
||||
receiver = MarantzV2003Receiver(port)
|
||||
else:
|
||||
receiver = MarantzV2007Receiver(port, model=V2007_MODELS[model_key])
|
||||
|
||||
try:
|
||||
await receiver.connect()
|
||||
except (
|
||||
# When the port contains invalid connection data
|
||||
ValueError,
|
||||
# If it is a remote port, and we cannot connect
|
||||
ConnectionError,
|
||||
OSError,
|
||||
TimeoutError,
|
||||
):
|
||||
return "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected exception")
|
||||
return "unknown"
|
||||
else:
|
||||
await receiver.disconnect()
|
||||
return None
|
||||
|
||||
|
||||
class MarantzRS232ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Marantz RS-232."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
model_key = user_input[CONF_MODEL]
|
||||
|
||||
self._async_abort_entries_match({CONF_DEVICE: user_input[CONF_DEVICE]})
|
||||
error = await _async_attempt_connect(user_input[CONF_DEVICE], model_key)
|
||||
if not error:
|
||||
return self.async_create_entry(
|
||||
title=MODEL_NAMES[model_key],
|
||||
data={
|
||||
CONF_DEVICE: user_input[CONF_DEVICE],
|
||||
CONF_MODEL: model_key,
|
||||
},
|
||||
)
|
||||
errors["base"] = error
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_MODEL, default=MODEL_MODERN): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=list(MODEL_NAMES),
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key="model",
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_DEVICE): SerialPortSelector(),
|
||||
}
|
||||
),
|
||||
user_input or {},
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Constants for the Marantz RS-232 integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from marantz_rs232 import (
|
||||
MarantzV2003Receiver,
|
||||
MarantzV2007Receiver,
|
||||
MarantzV2015Receiver,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
DOMAIN = "marantz_rs232"
|
||||
|
||||
type MarantzReceiver = (
|
||||
MarantzV2015Receiver | MarantzV2007Receiver | MarantzV2003Receiver
|
||||
)
|
||||
type MarantzRS232ConfigEntry = ConfigEntry[MarantzReceiver]
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "marantz_rs232",
|
||||
"name": "Marantz RS-232",
|
||||
"codeowners": ["@balloob"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["usb"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/marantz_rs232",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["marantz_rs232"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["marantz-rs232==2.0.0"]
|
||||
}
|
||||
@@ -0,0 +1,567 @@
|
||||
"""Media player platform for the Marantz RS-232 integration."""
|
||||
|
||||
import math
|
||||
from typing import cast
|
||||
|
||||
from marantz_rs232 import (
|
||||
V2015_MIN_VOLUME_DB,
|
||||
V2015_VOLUME_DB_RANGE,
|
||||
MarantzV2003Receiver,
|
||||
MarantzV2007Receiver,
|
||||
MarantzV2015Receiver,
|
||||
V2003MainPlayer,
|
||||
V2003MultiRoomPlayer,
|
||||
V2003ReceiverState,
|
||||
V2003Source,
|
||||
V2007MainPlayer,
|
||||
V2007MultiRoomPlayer,
|
||||
V2007ReceiverState,
|
||||
V2007Source,
|
||||
V2015InputSource,
|
||||
V2015MainPlayer,
|
||||
V2015ReceiverState,
|
||||
V2015ZonePlayer,
|
||||
)
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDeviceClass,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .config_flow import MODEL_NAMES
|
||||
from .const import DOMAIN, MarantzRS232ConfigEntry
|
||||
|
||||
V2003_MIN_VOLUME_DB = -90.0
|
||||
V2003_VOLUME_DB_RANGE = 189.0 # -90..+99
|
||||
|
||||
INPUT_SOURCE_V2015_TO_HA: dict[V2015InputSource, str] = {
|
||||
V2015InputSource.PHONO: "phono",
|
||||
V2015InputSource.CD: "cd",
|
||||
V2015InputSource.TUNER: "tuner",
|
||||
V2015InputSource.DVD: "dvd",
|
||||
V2015InputSource.BD: "bd",
|
||||
V2015InputSource.TV: "tv",
|
||||
V2015InputSource.SAT_CBL: "sat_cbl",
|
||||
V2015InputSource.SAT: "sat",
|
||||
V2015InputSource.MPLAY: "mplay",
|
||||
V2015InputSource.VCR: "vcr",
|
||||
V2015InputSource.GAME: "game",
|
||||
V2015InputSource.V_AUX: "v_aux",
|
||||
V2015InputSource.HDRADIO: "hdradio",
|
||||
V2015InputSource.SIRIUS: "sirius",
|
||||
V2015InputSource.SPOTIFY: "spotify",
|
||||
V2015InputSource.SIRIUSXM: "siriusxm",
|
||||
V2015InputSource.RHAPSODY: "rhapsody",
|
||||
V2015InputSource.PANDORA: "pandora",
|
||||
V2015InputSource.NAPSTER: "napster",
|
||||
V2015InputSource.LASTFM: "lastfm",
|
||||
V2015InputSource.FLICKR: "flickr",
|
||||
V2015InputSource.IRADIO: "iradio",
|
||||
V2015InputSource.SERVER: "server",
|
||||
V2015InputSource.FAVORITES: "favorites",
|
||||
V2015InputSource.CDR: "cdr",
|
||||
V2015InputSource.AUX1: "aux1",
|
||||
V2015InputSource.AUX2: "aux2",
|
||||
V2015InputSource.AUX3: "aux3",
|
||||
V2015InputSource.AUX4: "aux4",
|
||||
V2015InputSource.AUX5: "aux5",
|
||||
V2015InputSource.AUX6: "aux6",
|
||||
V2015InputSource.AUX7: "aux7",
|
||||
V2015InputSource.NET: "net",
|
||||
V2015InputSource.NET_USB: "net_usb",
|
||||
V2015InputSource.BT: "bt",
|
||||
V2015InputSource.M_XPORT: "m_xport",
|
||||
V2015InputSource.USB_IPOD: "usb_ipod",
|
||||
}
|
||||
|
||||
INPUT_SOURCE_V2007_TO_HA: dict[V2007Source, str] = {
|
||||
V2007Source.TV: "tv",
|
||||
V2007Source.DVD: "dvd",
|
||||
V2007Source.VCR1: "vcr1",
|
||||
V2007Source.DSS_VCR2: "dss_vcr2",
|
||||
V2007Source.AUX1: "aux1",
|
||||
V2007Source.AUX2: "aux2",
|
||||
V2007Source.CD_CDR: "cd_cdr",
|
||||
V2007Source.TAPE: "tape",
|
||||
V2007Source.TUNER1: "tuner",
|
||||
V2007Source.FM1: "fm",
|
||||
V2007Source.AM1: "am",
|
||||
V2007Source.XM1: "xm",
|
||||
}
|
||||
|
||||
INPUT_SOURCE_V2003_TO_HA: dict[V2003Source, str] = {
|
||||
V2003Source.DSS: "dss",
|
||||
V2003Source.TV: "tv",
|
||||
V2003Source.LD: "ld",
|
||||
V2003Source.DVD: "dvd",
|
||||
V2003Source.VCR1: "vcr1",
|
||||
V2003Source.VCR2_DVDR: "vcr2_dvdr",
|
||||
V2003Source.AUX1: "aux1",
|
||||
V2003Source.AUX2: "aux2",
|
||||
V2003Source.DVDR: "dvdr",
|
||||
V2003Source.CD: "cd",
|
||||
V2003Source.TAPE: "tape",
|
||||
V2003Source.CDR: "cdr",
|
||||
V2003Source.FM: "fm",
|
||||
V2003Source.AM: "am",
|
||||
V2003Source.MW: "mw",
|
||||
V2003Source.LW: "lw",
|
||||
V2003Source.TUNER: "tuner",
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MarantzRS232ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Marantz RS-232 media player."""
|
||||
receiver = config_entry.runtime_data
|
||||
|
||||
entities: list[MediaPlayerEntity]
|
||||
if isinstance(receiver, MarantzV2015Receiver):
|
||||
entities = [
|
||||
MarantzV2015MediaPlayer(receiver, receiver.main, config_entry, "main")
|
||||
]
|
||||
if receiver.zone_2.power is not None:
|
||||
entities.append(
|
||||
MarantzV2015MediaPlayer(
|
||||
receiver, receiver.zone_2, config_entry, "zone_2"
|
||||
)
|
||||
)
|
||||
if receiver.zone_3.power is not None:
|
||||
entities.append(
|
||||
MarantzV2015MediaPlayer(
|
||||
receiver, receiver.zone_3, config_entry, "zone_3"
|
||||
)
|
||||
)
|
||||
elif isinstance(receiver, MarantzV2003Receiver):
|
||||
entities = [
|
||||
MarantzV2003MediaPlayer(receiver, receiver.main, config_entry, "main")
|
||||
]
|
||||
if receiver.multi_room.power is not None:
|
||||
entities.append(
|
||||
MarantzV2003MediaPlayer(
|
||||
receiver, receiver.multi_room, config_entry, "multi_room"
|
||||
)
|
||||
)
|
||||
else:
|
||||
entities = [
|
||||
MarantzV2007MediaPlayer(receiver, receiver.main, config_entry, "main")
|
||||
]
|
||||
if receiver.multi_room_a.power is not None:
|
||||
entities.append(
|
||||
MarantzV2007MediaPlayer(
|
||||
receiver, receiver.multi_room_a, config_entry, "multi_room_a"
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class MarantzV2015MediaPlayer(MediaPlayerEntity):
|
||||
"""Representation of a modern Marantz receiver controlled over RS-232."""
|
||||
|
||||
_attr_device_class = MediaPlayerDeviceClass.RECEIVER
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "receiver"
|
||||
_attr_should_poll = False
|
||||
|
||||
_volume_min = V2015_MIN_VOLUME_DB
|
||||
_volume_range = V2015_VOLUME_DB_RANGE
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
receiver: MarantzV2015Receiver,
|
||||
player: V2015MainPlayer | V2015ZonePlayer,
|
||||
config_entry: MarantzRS232ConfigEntry,
|
||||
zone: str,
|
||||
) -> None:
|
||||
"""Initialize the media player."""
|
||||
self._receiver = receiver
|
||||
self._player = player
|
||||
self._is_main = zone == "main"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, config_entry.entry_id)},
|
||||
manufacturer="Marantz",
|
||||
model_id=MODEL_NAMES.get(config_entry.data["model"]),
|
||||
)
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_{zone}"
|
||||
|
||||
self._attr_source_list = sorted(INPUT_SOURCE_V2015_TO_HA.values())
|
||||
self._attr_supported_features = (
|
||||
MediaPlayerEntityFeature.TURN_ON
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
)
|
||||
|
||||
if zone == "main":
|
||||
self._attr_name = None
|
||||
self._attr_supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
elif zone == "zone_2":
|
||||
self._attr_name = "Zone 2"
|
||||
else:
|
||||
self._attr_name = "Zone 3"
|
||||
|
||||
self._async_update_from_player()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to receiver state updates."""
|
||||
self.async_on_remove(self._receiver.subscribe(self._async_on_state_update))
|
||||
|
||||
@callback
|
||||
def _async_on_state_update(self, state: V2015ReceiverState | None) -> None:
|
||||
if state is None:
|
||||
self._attr_available = False
|
||||
else:
|
||||
self._attr_available = True
|
||||
self._async_update_from_player()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_update_from_player(self) -> None:
|
||||
if self._player.power is None:
|
||||
self._attr_state = None
|
||||
else:
|
||||
self._attr_state = (
|
||||
MediaPlayerState.ON if self._player.power else MediaPlayerState.OFF
|
||||
)
|
||||
|
||||
source = self._player.input_source
|
||||
self._attr_source = INPUT_SOURCE_V2015_TO_HA.get(source) if source else None
|
||||
|
||||
volume_min = self._player.volume_min
|
||||
volume_max = self._player.volume_max
|
||||
if volume_min is not None:
|
||||
self._volume_min = volume_min
|
||||
if volume_max is not None and volume_max > volume_min:
|
||||
self._volume_range = volume_max - volume_min
|
||||
|
||||
volume = self._player.volume
|
||||
if volume is not None:
|
||||
self._attr_volume_level = (volume - self._volume_min) / self._volume_range
|
||||
else:
|
||||
self._attr_volume_level = None
|
||||
|
||||
if self._is_main:
|
||||
self._attr_is_volume_muted = cast(V2015MainPlayer, self._player).mute
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the receiver on."""
|
||||
await self._player.power_on()
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the receiver off."""
|
||||
await self._player.power_off()
|
||||
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
db = volume * self._volume_range + self._volume_min
|
||||
await self._player.set_volume(db)
|
||||
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up."""
|
||||
await self._player.volume_up()
|
||||
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down."""
|
||||
await self._player.volume_down()
|
||||
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute or unmute."""
|
||||
player = cast(V2015MainPlayer, self._player)
|
||||
if mute:
|
||||
await player.mute_on()
|
||||
else:
|
||||
await player.mute_off()
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
input_source = next(
|
||||
(
|
||||
input_source
|
||||
for input_source, ha_source in INPUT_SOURCE_V2015_TO_HA.items()
|
||||
if ha_source == source
|
||||
),
|
||||
None,
|
||||
)
|
||||
if input_source is None:
|
||||
raise HomeAssistantError("Invalid source")
|
||||
|
||||
await self._player.select_source(input_source)
|
||||
|
||||
|
||||
class MarantzV2007MediaPlayer(MediaPlayerEntity):
|
||||
"""Representation of a 2007-era Marantz receiver controlled over RS-232."""
|
||||
|
||||
_attr_device_class = MediaPlayerDeviceClass.RECEIVER
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "receiver"
|
||||
_attr_should_poll = False
|
||||
|
||||
_volume_min = V2015_MIN_VOLUME_DB
|
||||
_volume_range = V2015_VOLUME_DB_RANGE
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
receiver: MarantzV2007Receiver,
|
||||
player: V2007MainPlayer | V2007MultiRoomPlayer,
|
||||
config_entry: MarantzRS232ConfigEntry,
|
||||
zone: str,
|
||||
) -> None:
|
||||
"""Initialize the v2007 media player."""
|
||||
self._receiver = receiver
|
||||
self._player = player
|
||||
|
||||
if isinstance(player, V2007MainPlayer):
|
||||
self._set_volume = player.set_volume
|
||||
self._volume_up = player.volume_up
|
||||
self._volume_down = player.volume_down
|
||||
else:
|
||||
self._set_volume = player.set_line_volume
|
||||
self._volume_up = player.line_volume_up
|
||||
self._volume_down = player.line_volume_down
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, config_entry.entry_id)},
|
||||
manufacturer="Marantz",
|
||||
model_id=MODEL_NAMES.get(config_entry.data["model"]),
|
||||
)
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_{zone}"
|
||||
|
||||
self._attr_source_list = sorted(INPUT_SOURCE_V2007_TO_HA.values())
|
||||
self._attr_supported_features = (
|
||||
MediaPlayerEntityFeature.TURN_ON
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
)
|
||||
|
||||
if zone == "main":
|
||||
self._attr_name = None
|
||||
else:
|
||||
self._attr_name = "Multi Room"
|
||||
|
||||
self._async_update_from_player()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to receiver state updates."""
|
||||
self.async_on_remove(self._receiver.subscribe(self._async_on_state_update))
|
||||
|
||||
@callback
|
||||
def _async_on_state_update(self, state: V2007ReceiverState | None) -> None:
|
||||
if state is None:
|
||||
self._attr_available = False
|
||||
else:
|
||||
self._attr_available = True
|
||||
self._async_update_from_player()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_update_from_player(self) -> None:
|
||||
if self._player.power is None:
|
||||
self._attr_state = None
|
||||
else:
|
||||
self._attr_state = (
|
||||
MediaPlayerState.ON if self._player.power else MediaPlayerState.OFF
|
||||
)
|
||||
|
||||
source = self._player.input_source
|
||||
self._attr_source = INPUT_SOURCE_V2007_TO_HA.get(source) if source else None
|
||||
|
||||
if isinstance(self._player, V2007MainPlayer):
|
||||
volume = self._player.volume
|
||||
else:
|
||||
volume = self._player.line_volume
|
||||
|
||||
if volume is not None:
|
||||
self._attr_volume_level = (volume - self._volume_min) / self._volume_range
|
||||
else:
|
||||
self._attr_volume_level = None
|
||||
|
||||
self._attr_is_volume_muted = self._player.mute
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the receiver on."""
|
||||
await self._player.power_on()
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the receiver off."""
|
||||
await self._player.power_off()
|
||||
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
db = volume * self._volume_range + self._volume_min
|
||||
await self._set_volume(db)
|
||||
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up."""
|
||||
await self._volume_up()
|
||||
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down."""
|
||||
await self._volume_down()
|
||||
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute or unmute."""
|
||||
if mute:
|
||||
await self._player.mute_on()
|
||||
else:
|
||||
await self._player.mute_off()
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
v2007_source = next(
|
||||
(
|
||||
ls
|
||||
for ls, ha_source in INPUT_SOURCE_V2007_TO_HA.items()
|
||||
if ha_source == source
|
||||
),
|
||||
None,
|
||||
)
|
||||
if v2007_source is None:
|
||||
raise HomeAssistantError("Invalid source")
|
||||
|
||||
await self._player.select_source(v2007_source)
|
||||
|
||||
|
||||
class MarantzV2003MediaPlayer(MediaPlayerEntity):
|
||||
"""Representation of a 2003-era Marantz receiver controlled over RS-232."""
|
||||
|
||||
_attr_device_class = MediaPlayerDeviceClass.RECEIVER
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "receiver"
|
||||
_attr_should_poll = False
|
||||
|
||||
_volume_min = V2003_MIN_VOLUME_DB
|
||||
_volume_range = V2003_VOLUME_DB_RANGE
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
receiver: MarantzV2003Receiver,
|
||||
player: V2003MainPlayer | V2003MultiRoomPlayer,
|
||||
config_entry: MarantzRS232ConfigEntry,
|
||||
zone: str,
|
||||
) -> None:
|
||||
"""Initialize the v2003 media player."""
|
||||
self._receiver = receiver
|
||||
self._player = player
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, config_entry.entry_id)},
|
||||
manufacturer="Marantz",
|
||||
model_id=MODEL_NAMES.get(config_entry.data["model"]),
|
||||
)
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_{zone}"
|
||||
|
||||
self._attr_source_list = sorted(INPUT_SOURCE_V2003_TO_HA.values())
|
||||
if zone == "main":
|
||||
self._attr_name = None
|
||||
self._attr_supported_features = (
|
||||
MediaPlayerEntityFeature.TURN_ON
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
)
|
||||
else:
|
||||
self._attr_name = "Multi Room"
|
||||
self._attr_supported_features = (
|
||||
MediaPlayerEntityFeature.TURN_ON
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
)
|
||||
|
||||
self._async_update_from_player()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to receiver state updates."""
|
||||
self.async_on_remove(self._receiver.subscribe(self._async_on_state_update))
|
||||
|
||||
@callback
|
||||
def _async_on_state_update(self, state: V2003ReceiverState | None) -> None:
|
||||
if state is None:
|
||||
self._attr_available = False
|
||||
else:
|
||||
self._attr_available = True
|
||||
self._async_update_from_player()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _async_update_from_player(self) -> None:
|
||||
power = self._player.power
|
||||
if power is None:
|
||||
self._attr_state = None
|
||||
else:
|
||||
self._attr_state = MediaPlayerState.ON if power else MediaPlayerState.OFF
|
||||
|
||||
source = self._player.input_source
|
||||
self._attr_source = (
|
||||
INPUT_SOURCE_V2003_TO_HA.get(source) if source is not None else None
|
||||
)
|
||||
|
||||
volume = self._player.volume
|
||||
if volume is not None and volume != -math.inf:
|
||||
self._attr_volume_level = (volume - self._volume_min) / self._volume_range
|
||||
else:
|
||||
self._attr_volume_level = None
|
||||
|
||||
if isinstance(self._player, V2003MainPlayer):
|
||||
self._attr_is_volume_muted = self._player.mute
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the receiver on."""
|
||||
await self._player.power_on()
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the receiver off."""
|
||||
await self._player.power_off()
|
||||
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1. Main zone only."""
|
||||
db = round(volume * self._volume_range + self._volume_min)
|
||||
await cast(V2003MainPlayer, self._player).set_volume(db)
|
||||
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up."""
|
||||
await self._player.volume_up()
|
||||
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down."""
|
||||
await self._player.volume_down()
|
||||
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute or unmute. Main zone only."""
|
||||
player = cast(V2003MainPlayer, self._player)
|
||||
if mute:
|
||||
await player.mute_on()
|
||||
else:
|
||||
await player.mute_off()
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
v2003_source = next(
|
||||
(
|
||||
vs
|
||||
for vs, ha_source in INPUT_SOURCE_V2003_TO_HA.items()
|
||||
if ha_source == source
|
||||
),
|
||||
None,
|
||||
)
|
||||
if v2003_source is None:
|
||||
raise HomeAssistantError("Invalid source")
|
||||
|
||||
await self._player.select_source(v2003_source)
|
||||
@@ -0,0 +1,64 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup: done
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: "The integration does not create dynamic devices."
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: "The integration does not create devices that can become stale."
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"device": "[%key:common::config_flow::data::port%]",
|
||||
"model": "Receiver model"
|
||||
},
|
||||
"data_description": {
|
||||
"device": "Serial port path to connect to",
|
||||
"model": "Determines the protocol used to communicate with the receiver"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"media_player": {
|
||||
"receiver": {
|
||||
"state_attributes": {
|
||||
"source": {
|
||||
"state": {
|
||||
"am": "AM",
|
||||
"aux1": "Aux 1",
|
||||
"aux2": "Aux 2",
|
||||
"aux3": "Aux 3",
|
||||
"aux4": "Aux 4",
|
||||
"aux5": "Aux 5",
|
||||
"aux6": "Aux 6",
|
||||
"aux7": "Aux 7",
|
||||
"bd": "BD Player",
|
||||
"bt": "Bluetooth",
|
||||
"cd": "CD",
|
||||
"cd_cdr": "CD/CDR",
|
||||
"cdr": "CDR",
|
||||
"dss": "DSS",
|
||||
"dss_vcr2": "DSS/VCR 2",
|
||||
"dvd": "DVD",
|
||||
"dvdr": "DVDR",
|
||||
"favorites": "Favorites",
|
||||
"flickr": "Flickr",
|
||||
"fm": "FM",
|
||||
"game": "Game",
|
||||
"hdradio": "HD Radio",
|
||||
"iradio": "Internet Radio",
|
||||
"lastfm": "Last.fm",
|
||||
"ld": "LaserDisc",
|
||||
"lw": "LW",
|
||||
"m_xport": "M-XPort",
|
||||
"mplay": "Media Player",
|
||||
"mw": "MW",
|
||||
"napster": "Napster",
|
||||
"net": "Network",
|
||||
"net_usb": "Network/USB",
|
||||
"pandora": "Pandora",
|
||||
"phono": "Phono",
|
||||
"rhapsody": "Rhapsody",
|
||||
"sat": "Sat",
|
||||
"sat_cbl": "Satellite/Cable",
|
||||
"server": "Server",
|
||||
"sirius": "Sirius",
|
||||
"siriusxm": "SiriusXM",
|
||||
"spotify": "Spotify",
|
||||
"tape": "Tape",
|
||||
"tuner": "Tuner",
|
||||
"tv": "TV Audio",
|
||||
"usb_ipod": "USB/iPod",
|
||||
"v_aux": "V. Aux",
|
||||
"vcr": "VCR",
|
||||
"vcr1": "VCR 1",
|
||||
"vcr2_dvdr": "VCR 2/DVDR",
|
||||
"xm": "XM"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"model": {
|
||||
"options": {
|
||||
"modern": "Modern",
|
||||
"sr7002": "SR7002",
|
||||
"sr8002": "SR8002",
|
||||
"sr8300": "SR8300",
|
||||
"sr9300": "SR9300"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1
@@ -440,6 +440,7 @@ FLOWS = {
|
||||
"madvr",
|
||||
"mailgun",
|
||||
"marantz_infrared",
|
||||
"marantz_rs232",
|
||||
"mastodon",
|
||||
"matter",
|
||||
"mcp",
|
||||
|
||||
@@ -4100,6 +4100,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"marantz_rs232": {
|
||||
"name": "Marantz RS-232",
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push"
|
||||
},
|
||||
"martec": {
|
||||
"name": "Martec",
|
||||
"integration_type": "virtual",
|
||||
|
||||
Generated
+3
@@ -1527,6 +1527,9 @@ lw12==0.9.2
|
||||
# homeassistant.components.scrape
|
||||
lxml==6.1.1
|
||||
|
||||
# homeassistant.components.marantz_rs232
|
||||
marantz-rs232==2.0.0
|
||||
|
||||
# homeassistant.components.matrix
|
||||
matrix-nio==0.25.2
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Tests for the Marantz RS-232 integration."""
|
||||
|
||||
MOCK_DEVICE = "/dev/ttyUSB0"
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Test fixtures for the Marantz RS-232 integration."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from marantz_rs232 import (
|
||||
MarantzV2003Receiver,
|
||||
MarantzV2007Receiver,
|
||||
MarantzV2015Receiver,
|
||||
V2003Source,
|
||||
V2007Model,
|
||||
V2007Source,
|
||||
V2015InputSource,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.marantz_rs232.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE, CONF_MODEL
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import MOCK_DEVICE
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
def push_state(receiver: object) -> None:
|
||||
"""Notify subscribers using whichever notify API the protocol exposes."""
|
||||
# The v2003 protocol takes the state explicitly; the others read self._state.
|
||||
if hasattr(receiver, "_notify_subscribers"):
|
||||
receiver._notify_subscribers()
|
||||
else:
|
||||
receiver._notify(receiver._state if receiver._connected else None)
|
||||
|
||||
|
||||
def _install_async_mocks(receiver: object) -> object:
|
||||
"""Replace the serial I/O surface with awaitable mocks."""
|
||||
receiver._connected = True
|
||||
receiver.connect = AsyncMock()
|
||||
receiver.query_state = AsyncMock()
|
||||
receiver._send_command = AsyncMock()
|
||||
|
||||
async def _disconnect() -> None:
|
||||
receiver._connected = False
|
||||
push_state(receiver)
|
||||
|
||||
receiver.disconnect = AsyncMock(side_effect=_disconnect)
|
||||
return receiver
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_v2015_receiver() -> MarantzV2015Receiver:
|
||||
"""Create a modern (2015+) Marantz receiver test double."""
|
||||
receiver = MarantzV2015Receiver(MOCK_DEVICE)
|
||||
|
||||
main = receiver._state.main_zone
|
||||
main.power = True
|
||||
main.volume = -40.0
|
||||
main.volume_min = -80.0
|
||||
main.volume_max = 18.0
|
||||
main.mute = False
|
||||
main.input_source = V2015InputSource.CD
|
||||
|
||||
zone_2 = receiver._state.zone_2
|
||||
zone_2.power = True
|
||||
zone_2.volume = -30.0
|
||||
zone_2.input_source = V2015InputSource.TUNER
|
||||
|
||||
zone_3 = receiver._state.zone_3
|
||||
zone_3.power = False
|
||||
zone_3.input_source = V2015InputSource.NET
|
||||
|
||||
return _install_async_mocks(receiver)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_v2007_receiver() -> MarantzV2007Receiver:
|
||||
"""Create a 2007-era Marantz receiver test double."""
|
||||
receiver = MarantzV2007Receiver(MOCK_DEVICE, model=V2007Model.SR7002)
|
||||
|
||||
main = receiver._state.main
|
||||
main.power = True
|
||||
main.volume = -40.0
|
||||
main.mute = False
|
||||
main.source_audio = V2007Source.DVD.value
|
||||
|
||||
multi_room = receiver._state.multi_room_a
|
||||
multi_room.power = True
|
||||
multi_room.line_volume = -30.0
|
||||
multi_room.mute = False
|
||||
multi_room.source_audio = V2007Source.TV.value
|
||||
|
||||
return _install_async_mocks(receiver)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_v2003_receiver() -> MarantzV2003Receiver:
|
||||
"""Create a 2003-era Marantz receiver test double."""
|
||||
receiver = MarantzV2003Receiver(MOCK_DEVICE)
|
||||
|
||||
main = receiver._state.main
|
||||
main.power = True
|
||||
main.volume = -40.0
|
||||
main.mute = False
|
||||
main.audio_input = V2003Source.CD
|
||||
|
||||
multi_room = receiver._state.multi_room
|
||||
multi_room.enabled = True
|
||||
multi_room.volume = -30.0
|
||||
multi_room.audio_input = V2003Source.TUNER
|
||||
|
||||
return _install_async_mocks(receiver)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Create a mock config entry for the modern receiver."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_DEVICE: MOCK_DEVICE, CONF_MODEL: "modern"},
|
||||
title="Modern",
|
||||
entry_id="01KPBBPM6WCQ8148EFR0TCG1WW",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_usb_component(hass: HomeAssistant) -> None:
|
||||
"""Mock the USB component to prevent setup failures."""
|
||||
hass.config.components.add("usb")
|
||||
|
||||
|
||||
async def setup_integration(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
receiver: object,
|
||||
receiver_class: str,
|
||||
) -> None:
|
||||
"""Set up the integration with a mocked receiver class."""
|
||||
entry.add_to_hass(hass)
|
||||
with patch(
|
||||
f"homeassistant.components.marantz_rs232.{receiver_class}",
|
||||
return_value=receiver,
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -0,0 +1,399 @@
|
||||
# serializer version: 1
|
||||
# name: test_entities_created[media_player.modern-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'source_list': list([
|
||||
'aux1',
|
||||
'aux2',
|
||||
'aux3',
|
||||
'aux4',
|
||||
'aux5',
|
||||
'aux6',
|
||||
'aux7',
|
||||
'bd',
|
||||
'bt',
|
||||
'cd',
|
||||
'cdr',
|
||||
'dvd',
|
||||
'favorites',
|
||||
'flickr',
|
||||
'game',
|
||||
'hdradio',
|
||||
'iradio',
|
||||
'lastfm',
|
||||
'm_xport',
|
||||
'mplay',
|
||||
'napster',
|
||||
'net',
|
||||
'net_usb',
|
||||
'pandora',
|
||||
'phono',
|
||||
'rhapsody',
|
||||
'sat',
|
||||
'sat_cbl',
|
||||
'server',
|
||||
'sirius',
|
||||
'siriusxm',
|
||||
'spotify',
|
||||
'tuner',
|
||||
'tv',
|
||||
'usb_ipod',
|
||||
'v_aux',
|
||||
'vcr',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'media_player',
|
||||
'entity_category': None,
|
||||
'entity_id': 'media_player.modern',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <MediaPlayerDeviceClass.RECEIVER: 'receiver'>,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'marantz_rs232',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <MediaPlayerEntityFeature: 3468>,
|
||||
'translation_key': 'receiver',
|
||||
'unique_id': '01KPBBPM6WCQ8148EFR0TCG1WW_main',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities_created[media_player.modern-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'receiver',
|
||||
'friendly_name': 'Modern',
|
||||
'is_volume_muted': False,
|
||||
'source': 'cd',
|
||||
'source_list': list([
|
||||
'aux1',
|
||||
'aux2',
|
||||
'aux3',
|
||||
'aux4',
|
||||
'aux5',
|
||||
'aux6',
|
||||
'aux7',
|
||||
'bd',
|
||||
'bt',
|
||||
'cd',
|
||||
'cdr',
|
||||
'dvd',
|
||||
'favorites',
|
||||
'flickr',
|
||||
'game',
|
||||
'hdradio',
|
||||
'iradio',
|
||||
'lastfm',
|
||||
'm_xport',
|
||||
'mplay',
|
||||
'napster',
|
||||
'net',
|
||||
'net_usb',
|
||||
'pandora',
|
||||
'phono',
|
||||
'rhapsody',
|
||||
'sat',
|
||||
'sat_cbl',
|
||||
'server',
|
||||
'sirius',
|
||||
'siriusxm',
|
||||
'spotify',
|
||||
'tuner',
|
||||
'tv',
|
||||
'usb_ipod',
|
||||
'v_aux',
|
||||
'vcr',
|
||||
]),
|
||||
'supported_features': <MediaPlayerEntityFeature: 3468>,
|
||||
'volume_level': 0.40816326530612246,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'media_player.modern',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities_created[media_player.modern_zone_2-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'source_list': list([
|
||||
'aux1',
|
||||
'aux2',
|
||||
'aux3',
|
||||
'aux4',
|
||||
'aux5',
|
||||
'aux6',
|
||||
'aux7',
|
||||
'bd',
|
||||
'bt',
|
||||
'cd',
|
||||
'cdr',
|
||||
'dvd',
|
||||
'favorites',
|
||||
'flickr',
|
||||
'game',
|
||||
'hdradio',
|
||||
'iradio',
|
||||
'lastfm',
|
||||
'm_xport',
|
||||
'mplay',
|
||||
'napster',
|
||||
'net',
|
||||
'net_usb',
|
||||
'pandora',
|
||||
'phono',
|
||||
'rhapsody',
|
||||
'sat',
|
||||
'sat_cbl',
|
||||
'server',
|
||||
'sirius',
|
||||
'siriusxm',
|
||||
'spotify',
|
||||
'tuner',
|
||||
'tv',
|
||||
'usb_ipod',
|
||||
'v_aux',
|
||||
'vcr',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'media_player',
|
||||
'entity_category': None,
|
||||
'entity_id': 'media_player.modern_zone_2',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Zone 2',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <MediaPlayerDeviceClass.RECEIVER: 'receiver'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Zone 2',
|
||||
'platform': 'marantz_rs232',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <MediaPlayerEntityFeature: 3460>,
|
||||
'translation_key': 'receiver',
|
||||
'unique_id': '01KPBBPM6WCQ8148EFR0TCG1WW_zone_2',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities_created[media_player.modern_zone_2-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'receiver',
|
||||
'friendly_name': 'Modern Zone 2',
|
||||
'source': 'tuner',
|
||||
'source_list': list([
|
||||
'aux1',
|
||||
'aux2',
|
||||
'aux3',
|
||||
'aux4',
|
||||
'aux5',
|
||||
'aux6',
|
||||
'aux7',
|
||||
'bd',
|
||||
'bt',
|
||||
'cd',
|
||||
'cdr',
|
||||
'dvd',
|
||||
'favorites',
|
||||
'flickr',
|
||||
'game',
|
||||
'hdradio',
|
||||
'iradio',
|
||||
'lastfm',
|
||||
'm_xport',
|
||||
'mplay',
|
||||
'napster',
|
||||
'net',
|
||||
'net_usb',
|
||||
'pandora',
|
||||
'phono',
|
||||
'rhapsody',
|
||||
'sat',
|
||||
'sat_cbl',
|
||||
'server',
|
||||
'sirius',
|
||||
'siriusxm',
|
||||
'spotify',
|
||||
'tuner',
|
||||
'tv',
|
||||
'usb_ipod',
|
||||
'v_aux',
|
||||
'vcr',
|
||||
]),
|
||||
'supported_features': <MediaPlayerEntityFeature: 3460>,
|
||||
'volume_level': 0.5102040816326531,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'media_player.modern_zone_2',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities_created[media_player.modern_zone_3-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'source_list': list([
|
||||
'aux1',
|
||||
'aux2',
|
||||
'aux3',
|
||||
'aux4',
|
||||
'aux5',
|
||||
'aux6',
|
||||
'aux7',
|
||||
'bd',
|
||||
'bt',
|
||||
'cd',
|
||||
'cdr',
|
||||
'dvd',
|
||||
'favorites',
|
||||
'flickr',
|
||||
'game',
|
||||
'hdradio',
|
||||
'iradio',
|
||||
'lastfm',
|
||||
'm_xport',
|
||||
'mplay',
|
||||
'napster',
|
||||
'net',
|
||||
'net_usb',
|
||||
'pandora',
|
||||
'phono',
|
||||
'rhapsody',
|
||||
'sat',
|
||||
'sat_cbl',
|
||||
'server',
|
||||
'sirius',
|
||||
'siriusxm',
|
||||
'spotify',
|
||||
'tuner',
|
||||
'tv',
|
||||
'usb_ipod',
|
||||
'v_aux',
|
||||
'vcr',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'media_player',
|
||||
'entity_category': None,
|
||||
'entity_id': 'media_player.modern_zone_3',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Zone 3',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <MediaPlayerDeviceClass.RECEIVER: 'receiver'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Zone 3',
|
||||
'platform': 'marantz_rs232',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <MediaPlayerEntityFeature: 3460>,
|
||||
'translation_key': 'receiver',
|
||||
'unique_id': '01KPBBPM6WCQ8148EFR0TCG1WW_zone_3',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities_created[media_player.modern_zone_3-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'receiver',
|
||||
'friendly_name': 'Modern Zone 3',
|
||||
'source_list': list([
|
||||
'aux1',
|
||||
'aux2',
|
||||
'aux3',
|
||||
'aux4',
|
||||
'aux5',
|
||||
'aux6',
|
||||
'aux7',
|
||||
'bd',
|
||||
'bt',
|
||||
'cd',
|
||||
'cdr',
|
||||
'dvd',
|
||||
'favorites',
|
||||
'flickr',
|
||||
'game',
|
||||
'hdradio',
|
||||
'iradio',
|
||||
'lastfm',
|
||||
'm_xport',
|
||||
'mplay',
|
||||
'napster',
|
||||
'net',
|
||||
'net_usb',
|
||||
'pandora',
|
||||
'phono',
|
||||
'rhapsody',
|
||||
'sat',
|
||||
'sat_cbl',
|
||||
'server',
|
||||
'sirius',
|
||||
'siriusxm',
|
||||
'spotify',
|
||||
'tuner',
|
||||
'tv',
|
||||
'usb_ipod',
|
||||
'v_aux',
|
||||
'vcr',
|
||||
]),
|
||||
'supported_features': <MediaPlayerEntityFeature: 3460>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'media_player.modern_zone_3',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Tests for the Marantz RS-232 config flow."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.marantz_rs232.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_DEVICE, CONF_MODEL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import MOCK_DEVICE
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Prevent config-entry creation tests from setting up the integration."""
|
||||
with patch(
|
||||
"homeassistant.components.marantz_rs232.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup:
|
||||
yield mock_setup
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("model_key", "title", "receiver_class", "receiver_fixture"),
|
||||
[
|
||||
("modern", "Modern", "MarantzV2015Receiver", "mock_v2015_receiver"),
|
||||
("sr7002", "SR7002", "MarantzV2007Receiver", "mock_v2007_receiver"),
|
||||
("sr9300", "SR9300", "MarantzV2003Receiver", "mock_v2003_receiver"),
|
||||
],
|
||||
)
|
||||
async def test_user_form_creates_entry(
|
||||
hass: HomeAssistant,
|
||||
request: pytest.FixtureRequest,
|
||||
mock_setup_entry: AsyncMock,
|
||||
model_key: str,
|
||||
title: str,
|
||||
receiver_class: str,
|
||||
receiver_fixture: str,
|
||||
) -> None:
|
||||
"""Test a successful config flow creates an entry for each protocol."""
|
||||
receiver = request.getfixturevalue(receiver_fixture)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
with patch(
|
||||
f"homeassistant.components.marantz_rs232.config_flow.{receiver_class}",
|
||||
return_value=receiver,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_DEVICE: MOCK_DEVICE, CONF_MODEL: model_key},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == title
|
||||
assert result["data"] == {CONF_DEVICE: MOCK_DEVICE, CONF_MODEL: model_key}
|
||||
mock_setup_entry.assert_awaited_once()
|
||||
receiver.connect.assert_awaited_once()
|
||||
receiver.disconnect.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error"),
|
||||
[
|
||||
(ValueError("Invalid port"), "cannot_connect"),
|
||||
(ConnectionError("No response"), "cannot_connect"),
|
||||
(OSError("No such device"), "cannot_connect"),
|
||||
(RuntimeError("boom"), "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_user_form_error_recovers(
|
||||
hass: HomeAssistant,
|
||||
mock_v2015_receiver: AsyncMock,
|
||||
exception: Exception,
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test the user step reports errors and recovers on retry."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
mock_v2015_receiver.connect.side_effect = exception
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.marantz_rs232.config_flow.MarantzV2015Receiver",
|
||||
return_value=mock_v2015_receiver,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_DEVICE: MOCK_DEVICE, CONF_MODEL: "modern"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
mock_v2015_receiver.connect.side_effect = None
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.marantz_rs232.config_flow.MarantzV2015Receiver",
|
||||
return_value=mock_v2015_receiver,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.marantz_rs232.async_setup_entry",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_DEVICE: MOCK_DEVICE, CONF_MODEL: "modern"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_user_duplicate_port_aborts(hass: HomeAssistant) -> None:
|
||||
"""Test we abort if the same port is already configured."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_DEVICE: MOCK_DEVICE, CONF_MODEL: "modern"},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_DEVICE: MOCK_DEVICE, CONF_MODEL: "modern"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Tests for the Marantz RS-232 integration setup and teardown."""
|
||||
|
||||
from marantz_rs232 import MarantzV2015Receiver
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup_and_unload(
|
||||
hass: HomeAssistant,
|
||||
mock_v2015_receiver: MarantzV2015Receiver,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test a config entry sets up and unloads cleanly."""
|
||||
await setup_integration(
|
||||
hass, mock_config_entry, mock_v2015_receiver, "MarantzV2015Receiver"
|
||||
)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
mock_v2015_receiver.connect.assert_awaited_once()
|
||||
mock_v2015_receiver.query_state.assert_awaited_once()
|
||||
|
||||
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
mock_v2015_receiver.disconnect.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_setup_connect_failure_raises_not_ready(
|
||||
hass: HomeAssistant,
|
||||
mock_v2015_receiver: MarantzV2015Receiver,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test a connection failure puts the entry in retry state."""
|
||||
mock_v2015_receiver.connect.side_effect = ConnectionError("No response")
|
||||
|
||||
await setup_integration(
|
||||
hass, mock_config_entry, mock_v2015_receiver, "MarantzV2015Receiver"
|
||||
)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
mock_v2015_receiver.disconnect.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_remove_entry_while_loaded(
|
||||
hass: HomeAssistant,
|
||||
mock_v2015_receiver: MarantzV2015Receiver,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test removing a loaded entry does not schedule a reload.
|
||||
|
||||
When removing a loaded entry, disconnect() fires the subscriber callback
|
||||
with state=None. The callback must not schedule a reload because the entry
|
||||
is already being removed (state is no longer LOADED).
|
||||
"""
|
||||
await setup_integration(
|
||||
hass, mock_config_entry, mock_v2015_receiver, "MarantzV2015Receiver"
|
||||
)
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await hass.config_entries.async_remove(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
mock_v2015_receiver.disconnect.assert_awaited_once()
|
||||
@@ -0,0 +1,376 @@
|
||||
"""Tests for the Marantz RS-232 media player platform."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from marantz_rs232 import (
|
||||
MarantzV2003Receiver,
|
||||
MarantzV2007Receiver,
|
||||
MarantzV2015Receiver,
|
||||
V2015InputSource,
|
||||
)
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.marantz_rs232.const import DOMAIN
|
||||
from homeassistant.components.marantz_rs232.media_player import (
|
||||
INPUT_SOURCE_V2003_TO_HA,
|
||||
INPUT_SOURCE_V2007_TO_HA,
|
||||
INPUT_SOURCE_V2015_TO_HA,
|
||||
)
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_INPUT_SOURCE,
|
||||
ATTR_INPUT_SOURCE_LIST,
|
||||
ATTR_MEDIA_VOLUME_LEVEL,
|
||||
ATTR_MEDIA_VOLUME_MUTED,
|
||||
DOMAIN as MP_DOMAIN,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_DEVICE,
|
||||
CONF_MODEL,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
SERVICE_VOLUME_DOWN,
|
||||
SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_SET,
|
||||
SERVICE_VOLUME_UP,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util.json import load_json
|
||||
|
||||
from . import MOCK_DEVICE
|
||||
from .conftest import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
MAIN_ENTITY_ID = "media_player.modern"
|
||||
ZONE_2_ENTITY_ID = "media_player.modern_zone_2"
|
||||
ZONE_3_ENTITY_ID = "media_player.modern_zone_3"
|
||||
|
||||
STRINGS_PATH = Path("homeassistant/components/marantz_rs232/strings.json")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_v2015(
|
||||
hass: HomeAssistant,
|
||||
mock_v2015_receiver: MarantzV2015Receiver,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Set up the modern receiver."""
|
||||
await setup_integration(
|
||||
hass, mock_config_entry, mock_v2015_receiver, "MarantzV2015Receiver"
|
||||
)
|
||||
|
||||
|
||||
async def test_entities_created(
|
||||
hass: HomeAssistant,
|
||||
mock_v2015_receiver: MarantzV2015Receiver,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
init_v2015: None,
|
||||
) -> None:
|
||||
"""Test media player entities are created through config entry setup."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
mock_v2015_receiver.query_state.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_inactive_zone_not_created(
|
||||
hass: HomeAssistant,
|
||||
mock_v2015_receiver: MarantzV2015Receiver,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test zones without a queried power state are not created."""
|
||||
mock_v2015_receiver._state.zone_2.power = None
|
||||
mock_v2015_receiver._state.zone_3.power = None
|
||||
|
||||
await setup_integration(
|
||||
hass, mock_config_entry, mock_v2015_receiver, "MarantzV2015Receiver"
|
||||
)
|
||||
|
||||
assert hass.states.get(MAIN_ENTITY_ID) is not None
|
||||
assert hass.states.get(ZONE_2_ENTITY_ID) is None
|
||||
assert hass.states.get(ZONE_3_ENTITY_ID) is None
|
||||
|
||||
|
||||
async def test_state_update_and_unavailable(
|
||||
hass: HomeAssistant,
|
||||
mock_v2015_receiver: MarantzV2015Receiver,
|
||||
init_v2015: None,
|
||||
) -> None:
|
||||
"""Test the entity follows pushed state and goes unavailable on disconnect."""
|
||||
assert hass.states.get(MAIN_ENTITY_ID).state == STATE_ON
|
||||
|
||||
mock_v2015_receiver._state.main_zone.power = False
|
||||
mock_v2015_receiver._notify_subscribers()
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(MAIN_ENTITY_ID).state == STATE_OFF
|
||||
|
||||
mock_v2015_receiver._connected = False
|
||||
mock_v2015_receiver._notify_subscribers()
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(MAIN_ENTITY_ID).state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "service", "data", "expected"),
|
||||
[
|
||||
(MAIN_ENTITY_ID, SERVICE_TURN_ON, {}, ("ZM", "ON")),
|
||||
(MAIN_ENTITY_ID, SERVICE_TURN_OFF, {}, ("ZM", "OFF")),
|
||||
(MAIN_ENTITY_ID, SERVICE_VOLUME_UP, {}, ("MV", "UP")),
|
||||
(MAIN_ENTITY_ID, SERVICE_VOLUME_DOWN, {}, ("MV", "DOWN")),
|
||||
(
|
||||
MAIN_ENTITY_ID,
|
||||
SERVICE_VOLUME_MUTE,
|
||||
{ATTR_MEDIA_VOLUME_MUTED: True},
|
||||
("MU", "ON"),
|
||||
),
|
||||
(
|
||||
MAIN_ENTITY_ID,
|
||||
SERVICE_VOLUME_MUTE,
|
||||
{ATTR_MEDIA_VOLUME_MUTED: False},
|
||||
("MU", "OFF"),
|
||||
),
|
||||
(
|
||||
MAIN_ENTITY_ID,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
{ATTR_INPUT_SOURCE: "net"},
|
||||
("SI", V2015InputSource.NET.value),
|
||||
),
|
||||
(ZONE_2_ENTITY_ID, SERVICE_TURN_ON, {}, ("Z2", "ON")),
|
||||
(
|
||||
ZONE_2_ENTITY_ID,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
{ATTR_INPUT_SOURCE: "cd"},
|
||||
("Z2", V2015InputSource.CD.value),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_v2015_commands(
|
||||
hass: HomeAssistant,
|
||||
mock_v2015_receiver: MarantzV2015Receiver,
|
||||
init_v2015: None,
|
||||
entity_id: str,
|
||||
service: str,
|
||||
data: dict[str, str | bool],
|
||||
expected: tuple[str, str],
|
||||
) -> None:
|
||||
"""Test media player services send the expected serial commands."""
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: entity_id, **data},
|
||||
blocking=True,
|
||||
)
|
||||
mock_v2015_receiver._send_command.assert_awaited_with(*expected)
|
||||
|
||||
|
||||
async def test_v2015_volume_set(
|
||||
hass: HomeAssistant,
|
||||
mock_v2015_receiver: MarantzV2015Receiver,
|
||||
init_v2015: None,
|
||||
) -> None:
|
||||
"""Test setting the volume level sends a volume command."""
|
||||
state = hass.states.get(MAIN_ENTITY_ID)
|
||||
# volume -40 in range [-80, 18] -> (-40 - -80) / 98
|
||||
assert abs(state.attributes[ATTR_MEDIA_VOLUME_LEVEL] - (40 / 98)) < 0.001
|
||||
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_VOLUME_SET,
|
||||
{ATTR_ENTITY_ID: MAIN_ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.5},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_v2015_receiver._send_command.await_args.args[0] == "MV"
|
||||
|
||||
|
||||
async def test_invalid_source_raises(
|
||||
hass: HomeAssistant,
|
||||
init_v2015: None,
|
||||
) -> None:
|
||||
"""Test selecting an unknown source raises an error."""
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
{ATTR_ENTITY_ID: MAIN_ENTITY_ID, ATTR_INPUT_SOURCE: "nonexistent"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_v2007_entities(
|
||||
hass: HomeAssistant,
|
||||
mock_v2007_receiver: MarantzV2007Receiver,
|
||||
) -> None:
|
||||
"""Test a 2007-era receiver creates main and multi-room entities."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_DEVICE: MOCK_DEVICE, CONF_MODEL: "sr7002"},
|
||||
title="SR7002",
|
||||
)
|
||||
await setup_integration(hass, entry, mock_v2007_receiver, "MarantzV2007Receiver")
|
||||
|
||||
main = hass.states.get("media_player.sr7002")
|
||||
assert main.state == STATE_ON
|
||||
assert main.attributes[ATTR_INPUT_SOURCE] == "dvd"
|
||||
assert hass.states.get("media_player.sr7002_multi_room") is not None
|
||||
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: "media_player.sr7002"},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_v2007_receiver._send_command.await_args.args[0] == "PWR"
|
||||
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
{ATTR_ENTITY_ID: "media_player.sr7002", ATTR_INPUT_SOURCE: "tv"},
|
||||
blocking=True,
|
||||
)
|
||||
assert mock_v2007_receiver._send_command.await_args.args[0] == "SRC"
|
||||
|
||||
for service, data in (
|
||||
(SERVICE_TURN_OFF, {}),
|
||||
(SERVICE_VOLUME_UP, {}),
|
||||
(SERVICE_VOLUME_DOWN, {}),
|
||||
(SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 0.5}),
|
||||
(SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: True}),
|
||||
(SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: False}),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: "media_player.sr7002", **data},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Exercise the multi-room volume/mute/source paths.
|
||||
for service, data in (
|
||||
(SERVICE_TURN_ON, {}),
|
||||
(SERVICE_VOLUME_UP, {}),
|
||||
(SERVICE_VOLUME_DOWN, {}),
|
||||
(SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 0.4}),
|
||||
(SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: True}),
|
||||
(SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: "dvd"}),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: "media_player.sr7002_multi_room", **data},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
{ATTR_ENTITY_ID: "media_player.sr7002", ATTR_INPUT_SOURCE: "nope"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_v2007_receiver._state.main.power = False
|
||||
mock_v2007_receiver._notify_subscribers()
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("media_player.sr7002").state == STATE_OFF
|
||||
|
||||
mock_v2007_receiver._connected = False
|
||||
mock_v2007_receiver._notify_subscribers()
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("media_player.sr7002").state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_v2003_entities(
|
||||
hass: HomeAssistant,
|
||||
mock_v2003_receiver: MarantzV2003Receiver,
|
||||
) -> None:
|
||||
"""Test a 2003-era receiver creates main and multi-room entities."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_DEVICE: MOCK_DEVICE, CONF_MODEL: "sr9300"},
|
||||
title="SR9300",
|
||||
)
|
||||
await setup_integration(hass, entry, mock_v2003_receiver, "MarantzV2003Receiver")
|
||||
|
||||
main = hass.states.get("media_player.sr9300")
|
||||
assert main.state == STATE_ON
|
||||
assert main.attributes[ATTR_INPUT_SOURCE] == "cd"
|
||||
|
||||
multi_room = hass.states.get("media_player.sr9300_multi_room")
|
||||
assert multi_room is not None
|
||||
assert ATTR_INPUT_SOURCE_LIST in multi_room.attributes
|
||||
|
||||
for service, data in (
|
||||
(SERVICE_TURN_ON, {}),
|
||||
(SERVICE_TURN_OFF, {}),
|
||||
(SERVICE_VOLUME_UP, {}),
|
||||
(SERVICE_VOLUME_DOWN, {}),
|
||||
(SERVICE_VOLUME_SET, {ATTR_MEDIA_VOLUME_LEVEL: 0.5}),
|
||||
(SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: True}),
|
||||
(SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: False}),
|
||||
(SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: "dvd"}),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: "media_player.sr9300", **data},
|
||||
blocking=True,
|
||||
)
|
||||
mock_v2003_receiver._send_command.assert_awaited()
|
||||
|
||||
# Exercise multi-room power/volume/source paths.
|
||||
for service, data in (
|
||||
(SERVICE_TURN_ON, {}),
|
||||
(SERVICE_TURN_OFF, {}),
|
||||
(SERVICE_VOLUME_UP, {}),
|
||||
(SERVICE_VOLUME_DOWN, {}),
|
||||
(SERVICE_SELECT_SOURCE, {ATTR_INPUT_SOURCE: "tuner"}),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: "media_player.sr9300_multi_room", **data},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await hass.services.async_call(
|
||||
MP_DOMAIN,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
{ATTR_ENTITY_ID: "media_player.sr9300", ATTR_INPUT_SOURCE: "nope"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_v2003_receiver._state.main.power = False
|
||||
mock_v2003_receiver._notify(mock_v2003_receiver._state)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("media_player.sr9300").state == STATE_OFF
|
||||
|
||||
mock_v2003_receiver._connected = False
|
||||
mock_v2003_receiver._notify(None)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("media_player.sr9300").state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
def test_translation_keys_cover_all_sources() -> None:
|
||||
"""Test every mapped source has a matching translation key and vice versa."""
|
||||
mapped = (
|
||||
set(INPUT_SOURCE_V2015_TO_HA.values())
|
||||
| set(INPUT_SOURCE_V2007_TO_HA.values())
|
||||
| set(INPUT_SOURCE_V2003_TO_HA.values())
|
||||
)
|
||||
|
||||
strings = load_json(STRINGS_PATH)
|
||||
declared = set(
|
||||
strings["entity"]["media_player"]["receiver"]["state_attributes"]["source"][
|
||||
"state"
|
||||
]
|
||||
)
|
||||
assert mapped == declared
|
||||
Reference in New Issue
Block a user