mirror of https://github.com/home-assistant/core
Add multi-zone support to Anthem AV receiver and distribution solution (#74779)
* Add multi-zone support to Anthem AV receiver and distribution amplifier * Fix typo in comment * Convert properties to attribute and add test * Migrate entity name * Fix after rebase add strict typing and bump version * fix typing * Simplify test * Small improvement * remove dispatcher send and use callback
This commit is contained in:
parent
8181da7090
commit
ace359b1bd
|
@ -56,6 +56,7 @@ homeassistant.components.ambee.*
|
||||||
homeassistant.components.ambient_station.*
|
homeassistant.components.ambient_station.*
|
||||||
homeassistant.components.amcrest.*
|
homeassistant.components.amcrest.*
|
||||||
homeassistant.components.ampio.*
|
homeassistant.components.ampio.*
|
||||||
|
homeassistant.components.anthemav.*
|
||||||
homeassistant.components.aseko_pool_live.*
|
homeassistant.components.aseko_pool_live.*
|
||||||
homeassistant.components.asuswrt.*
|
homeassistant.components.asuswrt.*
|
||||||
homeassistant.components.automation.*
|
homeassistant.components.automation.*
|
||||||
|
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import anthemav
|
import anthemav
|
||||||
|
from anthemav.device_error import DeviceError
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform
|
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform
|
||||||
|
@ -11,7 +12,7 @@ from homeassistant.core import Event, HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||||
|
|
||||||
from .const import ANTHEMAV_UDATE_SIGNAL, DOMAIN
|
from .const import ANTHEMAV_UDATE_SIGNAL, DEVICE_TIMEOUT_SECONDS, DOMAIN
|
||||||
|
|
||||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||||
|
|
||||||
|
@ -22,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up Anthem A/V Receivers from a config entry."""
|
"""Set up Anthem A/V Receivers from a config entry."""
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_anthemav_update_callback(message):
|
def async_anthemav_update_callback(message: str) -> None:
|
||||||
"""Receive notification from transport that new data exists."""
|
"""Receive notification from transport that new data exists."""
|
||||||
_LOGGER.debug("Received update callback from AVR: %s", message)
|
_LOGGER.debug("Received update callback from AVR: %s", message)
|
||||||
async_dispatcher_send(hass, f"{ANTHEMAV_UDATE_SIGNAL}_{entry.entry_id}")
|
async_dispatcher_send(hass, f"{ANTHEMAV_UDATE_SIGNAL}_{entry.entry_id}")
|
||||||
|
@ -34,7 +35,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
update_callback=async_anthemav_update_callback,
|
update_callback=async_anthemav_update_callback,
|
||||||
)
|
)
|
||||||
|
|
||||||
except OSError as err:
|
# Wait for the zones to be initialised based on the model
|
||||||
|
await avr.protocol.wait_for_device_initialised(DEVICE_TIMEOUT_SECONDS)
|
||||||
|
except (OSError, DeviceError) as err:
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = avr
|
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = avr
|
||||||
|
|
|
@ -15,9 +15,13 @@ from homeassistant.data_entry_flow import FlowResult
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.device_registry import format_mac
|
from homeassistant.helpers.device_registry import format_mac
|
||||||
|
|
||||||
from .const import CONF_MODEL, DEFAULT_NAME, DEFAULT_PORT, DOMAIN
|
from .const import (
|
||||||
|
CONF_MODEL,
|
||||||
DEVICE_TIMEOUT_SECONDS = 4.0
|
DEFAULT_NAME,
|
||||||
|
DEFAULT_PORT,
|
||||||
|
DEVICE_TIMEOUT_SECONDS,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -5,3 +5,4 @@ DEFAULT_NAME = "Anthem AV"
|
||||||
DEFAULT_PORT = 14999
|
DEFAULT_PORT = 14999
|
||||||
DOMAIN = "anthemav"
|
DOMAIN = "anthemav"
|
||||||
MANUFACTURER = "Anthem"
|
MANUFACTURER = "Anthem"
|
||||||
|
DEVICE_TIMEOUT_SECONDS = 4.0
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"domain": "anthemav",
|
"domain": "anthemav",
|
||||||
"name": "Anthem A/V Receivers",
|
"name": "Anthem A/V Receivers",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/anthemav",
|
"documentation": "https://www.home-assistant.io/integrations/anthemav",
|
||||||
"requirements": ["anthemav==1.3.2"],
|
"requirements": ["anthemav==1.4.1"],
|
||||||
"dependencies": ["repairs"],
|
"dependencies": ["repairs"],
|
||||||
"codeowners": ["@hyralex"],
|
"codeowners": ["@hyralex"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
|
|
|
@ -2,13 +2,14 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from anthemav.connection import Connection
|
from anthemav.connection import Connection
|
||||||
|
from anthemav.protocol import AVR
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA,
|
||||||
|
MediaPlayerDeviceClass,
|
||||||
MediaPlayerEntity,
|
MediaPlayerEntity,
|
||||||
MediaPlayerEntityFeature,
|
MediaPlayerEntityFeature,
|
||||||
)
|
)
|
||||||
|
@ -22,7 +23,7 @@ from homeassistant.const import (
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
|
@ -88,20 +89,28 @@ async def async_setup_entry(
|
||||||
mac_address = config_entry.data[CONF_MAC]
|
mac_address = config_entry.data[CONF_MAC]
|
||||||
model = config_entry.data[CONF_MODEL]
|
model = config_entry.data[CONF_MODEL]
|
||||||
|
|
||||||
avr = hass.data[DOMAIN][config_entry.entry_id]
|
avr: Connection = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
entity = AnthemAVR(avr, name, mac_address, model, config_entry.entry_id)
|
entities = []
|
||||||
|
for zone_number in avr.protocol.zones:
|
||||||
|
_LOGGER.debug("Initializing Zone %s", zone_number)
|
||||||
|
entity = AnthemAVR(
|
||||||
|
avr.protocol, name, mac_address, model, zone_number, config_entry.entry_id
|
||||||
|
)
|
||||||
|
entities.append(entity)
|
||||||
|
|
||||||
_LOGGER.debug("Device data dump: %s", entity.dump_avrdata)
|
|
||||||
_LOGGER.debug("Connection data dump: %s", avr.dump_conndata)
|
_LOGGER.debug("Connection data dump: %s", avr.dump_conndata)
|
||||||
|
|
||||||
async_add_entities([entity])
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class AnthemAVR(MediaPlayerEntity):
|
class AnthemAVR(MediaPlayerEntity):
|
||||||
"""Entity reading values from Anthem AVR protocol."""
|
"""Entity reading values from Anthem AVR protocol."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
|
_attr_device_class = MediaPlayerDeviceClass.RECEIVER
|
||||||
|
_attr_icon = "mdi:audio-video"
|
||||||
_attr_supported_features = (
|
_attr_supported_features = (
|
||||||
MediaPlayerEntityFeature.VOLUME_SET
|
MediaPlayerEntityFeature.VOLUME_SET
|
||||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||||
|
@ -111,23 +120,33 @@ class AnthemAVR(MediaPlayerEntity):
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, avr: Connection, name: str, mac_address: str, model: str, entry_id: str
|
self,
|
||||||
|
avr: AVR,
|
||||||
|
name: str,
|
||||||
|
mac_address: str,
|
||||||
|
model: str,
|
||||||
|
zone_number: int,
|
||||||
|
entry_id: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize entity with transport."""
|
"""Initialize entity with transport."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.avr = avr
|
self.avr = avr
|
||||||
self._entry_id = entry_id
|
self._entry_id = entry_id
|
||||||
self._attr_name = name
|
self._zone_number = zone_number
|
||||||
self._attr_unique_id = mac_address
|
self._zone = avr.zones[zone_number]
|
||||||
|
if zone_number > 1:
|
||||||
|
self._attr_name = f"zone {zone_number}"
|
||||||
|
self._attr_unique_id = f"{mac_address}_{zone_number}"
|
||||||
|
else:
|
||||||
|
self._attr_unique_id = mac_address
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, mac_address)},
|
identifiers={(DOMAIN, mac_address)},
|
||||||
name=name,
|
name=name,
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
model=model,
|
model=model,
|
||||||
)
|
)
|
||||||
|
self.set_states()
|
||||||
def _lookup(self, propname: str, dval: Any | None = None) -> Any | None:
|
|
||||||
return getattr(self.avr.protocol, propname, dval)
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""When entity is added to hass."""
|
"""When entity is added to hass."""
|
||||||
|
@ -135,82 +154,42 @@ class AnthemAVR(MediaPlayerEntity):
|
||||||
async_dispatcher_connect(
|
async_dispatcher_connect(
|
||||||
self.hass,
|
self.hass,
|
||||||
f"{ANTHEMAV_UDATE_SIGNAL}_{self._entry_id}",
|
f"{ANTHEMAV_UDATE_SIGNAL}_{self._entry_id}",
|
||||||
self.async_write_ha_state,
|
self.update_states,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@callback
|
||||||
def state(self) -> str | None:
|
def update_states(self) -> None:
|
||||||
"""Return state of power on/off."""
|
"""Update states for the current zone."""
|
||||||
pwrstate = self._lookup("power")
|
self.set_states()
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
if pwrstate is True:
|
def set_states(self) -> None:
|
||||||
return STATE_ON
|
"""Set all the states from the device to the entity."""
|
||||||
if pwrstate is False:
|
self._attr_state = STATE_ON if self._zone.power is True else STATE_OFF
|
||||||
return STATE_OFF
|
self._attr_is_volume_muted = self._zone.mute
|
||||||
return None
|
self._attr_volume_level = self._zone.volume_as_percentage
|
||||||
|
self._attr_media_title = self._zone.input_name
|
||||||
@property
|
self._attr_app_name = self._zone.input_format
|
||||||
def is_volume_muted(self) -> bool | None:
|
self._attr_source = self._zone.input_name
|
||||||
"""Return boolean reflecting mute state on device."""
|
self._attr_source_list = self.avr.input_list
|
||||||
return self._lookup("mute", False)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def volume_level(self) -> float | None:
|
|
||||||
"""Return volume level from 0 to 1."""
|
|
||||||
return self._lookup("volume_as_percentage", 0.0)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def media_title(self) -> str | None:
|
|
||||||
"""Return current input name (closest we have to media title)."""
|
|
||||||
return self._lookup("input_name", "No Source")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def app_name(self) -> str | None:
|
|
||||||
"""Return details about current video and audio stream."""
|
|
||||||
return (
|
|
||||||
f"{self._lookup('video_input_resolution_text', '')} "
|
|
||||||
f"{self._lookup('audio_input_name', '')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def source(self) -> str | None:
|
|
||||||
"""Return currently selected input."""
|
|
||||||
return self._lookup("input_name", "Unknown")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def source_list(self) -> list[str] | None:
|
|
||||||
"""Return all active, configured inputs."""
|
|
||||||
return self._lookup("input_list", ["Unknown"])
|
|
||||||
|
|
||||||
async def async_select_source(self, source: str) -> None:
|
async def async_select_source(self, source: str) -> None:
|
||||||
"""Change AVR to the designated source (by name)."""
|
"""Change AVR to the designated source (by name)."""
|
||||||
self._update_avr("input_name", source)
|
self._zone.input_name = source
|
||||||
|
|
||||||
async def async_turn_off(self) -> None:
|
async def async_turn_off(self) -> None:
|
||||||
"""Turn AVR power off."""
|
"""Turn AVR power off."""
|
||||||
self._update_avr("power", False)
|
self._zone.power = False
|
||||||
|
|
||||||
async def async_turn_on(self) -> None:
|
async def async_turn_on(self) -> None:
|
||||||
"""Turn AVR power on."""
|
"""Turn AVR power on."""
|
||||||
self._update_avr("power", True)
|
self._zone.power = True
|
||||||
|
|
||||||
async def async_set_volume_level(self, volume: float) -> None:
|
async def async_set_volume_level(self, volume: float) -> None:
|
||||||
"""Set AVR volume (0 to 1)."""
|
"""Set AVR volume (0 to 1)."""
|
||||||
self._update_avr("volume_as_percentage", volume)
|
self._zone.volume_as_percentage = volume
|
||||||
|
|
||||||
async def async_mute_volume(self, mute: bool) -> None:
|
async def async_mute_volume(self, mute: bool) -> None:
|
||||||
"""Engage AVR mute."""
|
"""Engage AVR mute."""
|
||||||
self._update_avr("mute", mute)
|
self._zone.mute = mute
|
||||||
|
|
||||||
def _update_avr(self, propname: str, value: Any | None) -> None:
|
|
||||||
"""Update a property in the AVR."""
|
|
||||||
_LOGGER.debug("Sending command to AVR: set %s to %s", propname, str(value))
|
|
||||||
setattr(self.avr.protocol, propname, value)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def dump_avrdata(self):
|
|
||||||
"""Return state of avr object for debugging forensics."""
|
|
||||||
attrs = vars(self)
|
|
||||||
items_string = ", ".join(f"{item}: {item}" for item in attrs.items())
|
|
||||||
return f"dump_avrdata: {items_string}"
|
|
||||||
|
|
11
mypy.ini
11
mypy.ini
|
@ -339,6 +339,17 @@ no_implicit_optional = true
|
||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
|
|
||||||
|
[mypy-homeassistant.components.anthemav.*]
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
disallow_subclassing_any = true
|
||||||
|
disallow_untyped_calls = true
|
||||||
|
disallow_untyped_decorators = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
no_implicit_optional = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unreachable = true
|
||||||
|
|
||||||
[mypy-homeassistant.components.aseko_pool_live.*]
|
[mypy-homeassistant.components.aseko_pool_live.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
|
|
@ -313,7 +313,7 @@ androidtv[async]==0.0.67
|
||||||
anel_pwrctrl-homeassistant==0.0.1.dev2
|
anel_pwrctrl-homeassistant==0.0.1.dev2
|
||||||
|
|
||||||
# homeassistant.components.anthemav
|
# homeassistant.components.anthemav
|
||||||
anthemav==1.3.2
|
anthemav==1.4.1
|
||||||
|
|
||||||
# homeassistant.components.apcupsd
|
# homeassistant.components.apcupsd
|
||||||
apcaccess==0.0.13
|
apcaccess==0.0.13
|
||||||
|
|
|
@ -279,7 +279,7 @@ ambiclimate==0.2.1
|
||||||
androidtv[async]==0.0.67
|
androidtv[async]==0.0.67
|
||||||
|
|
||||||
# homeassistant.components.anthemav
|
# homeassistant.components.anthemav
|
||||||
anthemav==1.3.2
|
anthemav==1.4.1
|
||||||
|
|
||||||
# homeassistant.components.apprise
|
# homeassistant.components.apprise
|
||||||
apprise==0.9.9
|
apprise==0.9.9
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
"""Fixtures for anthemav integration tests."""
|
"""Fixtures for anthemav integration tests."""
|
||||||
|
from typing import Callable
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.anthemav.const import CONF_MODEL, DOMAIN
|
from homeassistant.components.anthemav.const import CONF_MODEL, DOMAIN
|
||||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT
|
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
@ -16,17 +18,25 @@ def mock_anthemav() -> AsyncMock:
|
||||||
avr.protocol.macaddress = "000000000001"
|
avr.protocol.macaddress = "000000000001"
|
||||||
avr.protocol.model = "MRX 520"
|
avr.protocol.model = "MRX 520"
|
||||||
avr.reconnect = AsyncMock()
|
avr.reconnect = AsyncMock()
|
||||||
|
avr.protocol.wait_for_device_initialised = AsyncMock()
|
||||||
avr.close = MagicMock()
|
avr.close = MagicMock()
|
||||||
avr.protocol.input_list = []
|
avr.protocol.input_list = []
|
||||||
avr.protocol.audio_listening_mode_list = []
|
avr.protocol.audio_listening_mode_list = []
|
||||||
avr.protocol.power = False
|
avr.protocol.zones = {1: get_zone(), 2: get_zone()}
|
||||||
return avr
|
return avr
|
||||||
|
|
||||||
|
|
||||||
|
def get_zone() -> MagicMock:
|
||||||
|
"""Return a mocked zone."""
|
||||||
|
zone = MagicMock()
|
||||||
|
|
||||||
|
zone.power = False
|
||||||
|
return zone
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_connection_create(mock_anthemav: AsyncMock) -> AsyncMock:
|
def mock_connection_create(mock_anthemav: AsyncMock) -> AsyncMock:
|
||||||
"""Return the default mocked connection.create."""
|
"""Return the default mocked connection.create."""
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"anthemav.Connection.create",
|
"anthemav.Connection.create",
|
||||||
return_value=mock_anthemav,
|
return_value=mock_anthemav,
|
||||||
|
@ -34,6 +44,12 @@ def mock_connection_create(mock_anthemav: AsyncMock) -> AsyncMock:
|
||||||
yield mock
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def update_callback(mock_connection_create: AsyncMock) -> Callable[[str], None]:
|
||||||
|
"""Return the update_callback used when creating the connection."""
|
||||||
|
return mock_connection_create.call_args[1]["update_callback"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def mock_config_entry() -> MockConfigEntry:
|
def mock_config_entry() -> MockConfigEntry:
|
||||||
"""Return the default mocked config entry."""
|
"""Return the default mocked config entry."""
|
||||||
|
@ -48,3 +64,18 @@ def mock_config_entry() -> MockConfigEntry:
|
||||||
},
|
},
|
||||||
unique_id="00:00:00:00:00:01",
|
unique_id="00:00:00:00:00:01",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def init_integration(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
mock_connection_create: AsyncMock,
|
||||||
|
) -> MockConfigEntry:
|
||||||
|
"""Set up the AnthemAv integration for testing."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
return mock_config_entry
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
"""Test the Anthem A/V Receivers config flow."""
|
"""Test the Anthem A/V Receivers config flow."""
|
||||||
|
from typing import Callable
|
||||||
from unittest.mock import ANY, AsyncMock, patch
|
from unittest.mock import ANY, AsyncMock, patch
|
||||||
|
|
||||||
|
from anthemav.device_error import DeviceError
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON
|
from homeassistant.const import STATE_OFF, STATE_ON
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
@ -12,35 +16,31 @@ async def test_load_unload_config_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_connection_create: AsyncMock,
|
mock_connection_create: AsyncMock,
|
||||||
mock_anthemav: AsyncMock,
|
mock_anthemav: AsyncMock,
|
||||||
mock_config_entry: MockConfigEntry,
|
init_integration: MockConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test load and unload AnthemAv component."""
|
"""Test load and unload AnthemAv component."""
|
||||||
mock_config_entry.add_to_hass(hass)
|
|
||||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
# assert avr is created
|
# assert avr is created
|
||||||
mock_connection_create.assert_called_with(
|
mock_connection_create.assert_called_with(
|
||||||
host="1.1.1.1", port=14999, update_callback=ANY
|
host="1.1.1.1", port=14999, update_callback=ANY
|
||||||
)
|
)
|
||||||
assert mock_config_entry.state == config_entries.ConfigEntryState.LOADED
|
assert init_integration.state == config_entries.ConfigEntryState.LOADED
|
||||||
|
|
||||||
# unload
|
# unload
|
||||||
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
await hass.config_entries.async_unload(init_integration.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
# assert unload and avr is closed
|
# assert unload and avr is closed
|
||||||
assert mock_config_entry.state == config_entries.ConfigEntryState.NOT_LOADED
|
assert init_integration.state == config_entries.ConfigEntryState.NOT_LOADED
|
||||||
mock_anthemav.close.assert_called_once()
|
mock_anthemav.close.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
async def test_config_entry_not_ready(
|
@pytest.mark.parametrize("error", [OSError, DeviceError])
|
||||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
async def test_config_entry_not_ready_when_oserror(
|
||||||
|
hass: HomeAssistant, mock_config_entry: MockConfigEntry, error: Exception
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test AnthemAV configuration entry not ready."""
|
"""Test AnthemAV configuration entry not ready."""
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"anthemav.Connection.create",
|
"anthemav.Connection.create",
|
||||||
side_effect=OSError,
|
side_effect=error,
|
||||||
):
|
):
|
||||||
mock_config_entry.add_to_hass(hass)
|
mock_config_entry.add_to_hass(hass)
|
||||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
|
@ -52,23 +52,18 @@ async def test_anthemav_dispatcher_signal(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_connection_create: AsyncMock,
|
mock_connection_create: AsyncMock,
|
||||||
mock_anthemav: AsyncMock,
|
mock_anthemav: AsyncMock,
|
||||||
mock_config_entry: MockConfigEntry,
|
init_integration: MockConfigEntry,
|
||||||
|
update_callback: Callable[[str], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test send update signal to dispatcher."""
|
"""Test send update signal to dispatcher."""
|
||||||
mock_config_entry.add_to_hass(hass)
|
|
||||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
states = hass.states.get("media_player.anthem_av")
|
states = hass.states.get("media_player.anthem_av")
|
||||||
assert states
|
assert states
|
||||||
assert states.state == STATE_OFF
|
assert states.state == STATE_OFF
|
||||||
|
|
||||||
# change state of the AVR
|
# change state of the AVR
|
||||||
mock_anthemav.protocol.power = True
|
mock_anthemav.protocol.zones[1].power = True
|
||||||
|
|
||||||
# get the callback function that trigger the signal to update the state
|
update_callback("power")
|
||||||
avr_update_callback = mock_connection_create.call_args[1]["update_callback"]
|
|
||||||
avr_update_callback("power")
|
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
"""Test the Anthem A/V Receivers config flow."""
|
||||||
|
from typing import Callable
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.media_player.const import (
|
||||||
|
ATTR_APP_NAME,
|
||||||
|
ATTR_INPUT_SOURCE,
|
||||||
|
ATTR_INPUT_SOURCE_LIST,
|
||||||
|
ATTR_MEDIA_TITLE,
|
||||||
|
ATTR_MEDIA_VOLUME_MUTED,
|
||||||
|
)
|
||||||
|
from homeassistant.components.siren.const import ATTR_VOLUME_LEVEL
|
||||||
|
from homeassistant.const import STATE_OFF, STATE_ON
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"entity_id,entity_name",
|
||||||
|
[
|
||||||
|
("media_player.anthem_av", "Anthem AV"),
|
||||||
|
("media_player.anthem_av_zone_2", "Anthem AV zone 2"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_zones_loaded(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
init_integration: MockConfigEntry,
|
||||||
|
entity_id: str,
|
||||||
|
entity_name: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test zones are loaded."""
|
||||||
|
|
||||||
|
states = hass.states.get(entity_id)
|
||||||
|
|
||||||
|
assert states
|
||||||
|
assert states.state == STATE_OFF
|
||||||
|
assert states.name == entity_name
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_states_zone1(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
init_integration: MockConfigEntry,
|
||||||
|
mock_anthemav: AsyncMock,
|
||||||
|
update_callback: Callable[[str], None],
|
||||||
|
) -> None:
|
||||||
|
"""Test zone states are updated."""
|
||||||
|
|
||||||
|
mock_zone = mock_anthemav.protocol.zones[1]
|
||||||
|
|
||||||
|
mock_zone.power = True
|
||||||
|
mock_zone.mute = True
|
||||||
|
mock_zone.volume_as_percentage = 42
|
||||||
|
mock_zone.input_name = "TEST INPUT"
|
||||||
|
mock_zone.input_format = "2.0 PCM"
|
||||||
|
mock_anthemav.protocol.input_list = ["TEST INPUT", "INPUT 2"]
|
||||||
|
|
||||||
|
update_callback("command")
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
states = hass.states.get("media_player.anthem_av")
|
||||||
|
assert states
|
||||||
|
assert states.state == STATE_ON
|
||||||
|
assert states.attributes[ATTR_VOLUME_LEVEL] == 42
|
||||||
|
assert states.attributes[ATTR_MEDIA_VOLUME_MUTED] is True
|
||||||
|
assert states.attributes[ATTR_INPUT_SOURCE] == "TEST INPUT"
|
||||||
|
assert states.attributes[ATTR_MEDIA_TITLE] == "TEST INPUT"
|
||||||
|
assert states.attributes[ATTR_APP_NAME] == "2.0 PCM"
|
||||||
|
assert states.attributes[ATTR_INPUT_SOURCE_LIST] == ["TEST INPUT", "INPUT 2"]
|
Loading…
Reference in New Issue