Add madvr envy integration (#120382)

* feat: Add madvr envy

* fix: await and pass entry directly

* fix: add attributes and unique id for sensors

* fix: reflect power state well, improve state detection

* fix: don't connect on init, add options, add reload on change, keep on during test

* fix: cancel tasks on unload

* fix: test connection via library

* fix: wait for boot time

* docs: add readme and license

* fix: broken pipe in lib

* fix: detect out of band power off

* fix: improve extra attributes

* fix: fix unloading, add config flow test, limit to one platform

* fix: use conf, refresh coordinator, other comments

* fix: remove event data

* fix: fix tests passing, remove wake on lan

* fix: dont allow to proceed unless connection works

* chore: update dep

* fix: update config flow, add constants

* fix: write state, use runtime data instead

* fix: remove await

* fix: move unloading and stuff to coordinator/init

* fix: pass in config entry with correct type

* fix: move queue and tasks to library

* fix: config flow error flow, tests, name, and update lib

* fix: update lib, leave connection open on setup

* fix: update lib

* fix: address comments, remove wol from lib

* fix: remove unneeded options

* fix: remove fields

* fix: simplify code, address comments

* fix: move error to lib

* fix: fix test

* fix: stronger types

* fix: update lib

* fix: missing text from options flow

* chore: remove options flow

* chore: remove import

* chore: update comments

* fix: get mac from device, persist

* fix: add mac stuff to test

* fix: startup import errors

* chore: stale comment

* fix: get mac from persisted config

* chore: update lib

* fix: persist mac in a better way

* feat: use mac as unique ID for entry

* fix: use unique ID from mac, add proper device

* fix: will not be set in init potentially

* fix: access mac

* fix: optimize, move error to lib

* feat: add coordinator test, use conf

* fix: use one mock, add init test

* fix: not async

* feat: add remote test

* fix: types

* fix: patch client, expand remote tests

* fix: use snapshot test

* fix: update branding

* fix: add description, fix type check

* fix: update tests

* fix: test

* fix: update test

* fix: camelcase

* Fix

* feat: strict typing

* fix: strict typing in lib

* fix: type will never be None

* fix: reference to mac, all tests passing

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
pull/121460/head
ilan 2024-07-07 14:41:53 -04:00 committed by GitHub
parent 8110b60fc9
commit 12228d8a00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 760 additions and 0 deletions

View File

@ -287,6 +287,7 @@ homeassistant.components.logger.*
homeassistant.components.london_underground.*
homeassistant.components.lookin.*
homeassistant.components.luftdaten.*
homeassistant.components.madvr.*
homeassistant.components.mailbox.*
homeassistant.components.map.*
homeassistant.components.mastodon.*

View File

@ -827,6 +827,8 @@ build.json @home-assistant/supervisor
/tests/components/lutron_caseta/ @swails @bdraco @danaues @eclair4151
/homeassistant/components/lyric/ @timmo001
/tests/components/lyric/ @timmo001
/homeassistant/components/madvr/ @iloveicedgreentea
/tests/components/madvr/ @iloveicedgreentea
/homeassistant/components/mastodon/ @fabaff
/homeassistant/components/matrix/ @PaarthShah
/tests/components/matrix/ @PaarthShah

View File

@ -0,0 +1,70 @@
"""The madvr-envy integration."""
from __future__ import annotations
import logging
from madvr.madvr import Madvr
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant, callback
from .coordinator import MadVRCoordinator
PLATFORMS: list[Platform] = [Platform.REMOTE]
type MadVRConfigEntry = ConfigEntry[MadVRCoordinator]
_LOGGER = logging.getLogger(__name__)
async def async_handle_unload(coordinator: MadVRCoordinator) -> None:
"""Handle unload."""
_LOGGER.debug("Integration unloading")
coordinator.client.stop()
await coordinator.client.async_cancel_tasks()
_LOGGER.debug("Integration closing connection")
await coordinator.client.close_connection()
_LOGGER.debug("Unloaded")
async def async_setup_entry(hass: HomeAssistant, entry: MadVRConfigEntry) -> bool:
"""Set up the integration from a config entry."""
assert entry.unique_id
madVRClient = Madvr(
host=entry.data[CONF_HOST],
logger=_LOGGER,
port=entry.data[CONF_PORT],
mac=entry.unique_id,
connect_timeout=10,
loop=hass.loop,
)
coordinator = MadVRCoordinator(hass, madVRClient)
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@callback
async def handle_unload(event: Event) -> None:
"""Handle unload."""
await async_handle_unload(coordinator=coordinator)
# listen for core stop event
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, handle_unload)
# handle loading operations
await coordinator.handle_coordinator_load()
return True
async def async_unload_entry(hass: HomeAssistant, entry: MadVRConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
coordinator: MadVRCoordinator = entry.runtime_data
await async_handle_unload(coordinator=coordinator)
return unload_ok

View File

@ -0,0 +1,116 @@
"""Config flow for the integration."""
import asyncio
import logging
from typing import Any
import aiohttp
from madvr.madvr import HeartBeatError, Madvr
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PORT
from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN
from .errors import CannotConnect
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(
CONF_HOST,
): str,
vol.Required(
CONF_PORT,
default=DEFAULT_PORT,
): int,
}
)
RETRY_INTERVAL = 1
class MadVRConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for the integration."""
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:
host = user_input[CONF_HOST]
port = user_input[CONF_PORT]
try:
# ensure we can connect and get the mac address from device
mac = await self._test_connection(host, port)
except CannotConnect:
_LOGGER.error("CannotConnect error caught")
errors["base"] = "cannot_connect"
else:
if not mac:
errors["base"] = "no_mac"
if not errors:
_LOGGER.debug("MAC address found: %s", mac)
# this will prevent the user from adding the same device twice and persist the mac address
await self.async_set_unique_id(mac)
self._abort_if_unique_id_configured()
# create the entry
return self.async_create_entry(
title=DEFAULT_NAME,
data=user_input,
)
# this will show the form or allow the user to retry if there was an error
return self.async_show_form(
step_id="user",
data_schema=self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA, user_input
),
errors=errors,
)
async def _test_connection(self, host: str, port: int) -> str:
"""Test if we can connect to the device and grab the mac."""
madvr_client = Madvr(host=host, port=port, loop=self.hass.loop)
_LOGGER.debug("Testing connection to madVR at %s:%s", host, port)
# try to connect
try:
await asyncio.wait_for(madvr_client.open_connection(), timeout=15)
# connection can raise HeartBeatError if the device is not available or connection does not work
except (TimeoutError, aiohttp.ClientError, OSError, HeartBeatError) as err:
_LOGGER.error("Error connecting to madVR: %s", err)
raise CannotConnect from err
# check if we are connected
if not madvr_client.connected:
raise CannotConnect("Connection failed")
# background tasks needed to capture realtime info
await madvr_client.async_add_tasks()
# wait for client to capture device info
retry_time = 15
while not madvr_client.mac_address and retry_time > 0:
await asyncio.sleep(RETRY_INTERVAL)
retry_time -= 1
mac_address = madvr_client.mac_address
if mac_address:
_LOGGER.debug("Connected to madVR with MAC: %s", mac_address)
# close this connection because this client object will not be reused
await self._close_test_connection(madvr_client)
_LOGGER.debug("Connection test successful")
return mac_address
async def _close_test_connection(self, madvr_client: Madvr) -> None:
"""Close the test connection."""
madvr_client.stop()
await madvr_client.async_cancel_tasks()
await madvr_client.close_connection()

View File

@ -0,0 +1,6 @@
"""Constants for the madvr-envy integration."""
DOMAIN = "madvr"
DEFAULT_NAME = "envy"
DEFAULT_PORT = 44077

View File

@ -0,0 +1,50 @@
"""Coordinator for handling data fetching and updates."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from madvr.madvr import Madvr
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
if TYPE_CHECKING:
from . import MadVRConfigEntry
class MadVRCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Madvr coordinator for Envy (push-based API)."""
config_entry: MadVRConfigEntry
def __init__(
self,
hass: HomeAssistant,
client: Madvr,
) -> None:
"""Initialize madvr coordinator."""
super().__init__(hass, _LOGGER, name=DOMAIN)
assert self.config_entry.unique_id
self.mac = self.config_entry.unique_id
self.client = client
self.client.set_update_callback(self.handle_push_data)
_LOGGER.debug("MadVRCoordinator initialized with mac: %s", self.mac)
def handle_push_data(self, data: dict[str, Any]) -> None:
"""Handle new data pushed from the API."""
_LOGGER.debug("Received push data: %s", data)
# inform HA that we have new data
self.async_set_updated_data(data)
async def handle_coordinator_load(self) -> None:
"""Handle operations on integration load."""
_LOGGER.debug("Using loop: %s", self.client.loop)
# tell the library to start background tasks
await self.client.async_add_tasks()
_LOGGER.debug("Added %s tasks to client", len(self.client.tasks))

View File

@ -0,0 +1,5 @@
"""Errors for the madvr component."""
class CannotConnect(Exception):
"""Error to indicate we cannot connect."""

View File

@ -0,0 +1,10 @@
{
"domain": "madvr",
"name": "madVR Envy",
"codeowners": ["@iloveicedgreentea"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/madvr",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["py-madvr2==1.6.27"]
}

View File

@ -0,0 +1,86 @@
"""Support for madVR remote control."""
from __future__ import annotations
from collections.abc import Iterable
import logging
from typing import Any
from homeassistant.components.remote import RemoteEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import MadVRConfigEntry
from .const import DOMAIN
from .coordinator import MadVRCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
entry: MadVRConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the madVR remote."""
coordinator = entry.runtime_data
async_add_entities(
[
MadvrRemote(coordinator),
]
)
class MadvrRemote(CoordinatorEntity[MadVRCoordinator], RemoteEntity):
"""Remote entity for the madVR integration."""
_attr_has_entity_name = True
_attr_name = None
def __init__(
self,
coordinator: MadVRCoordinator,
) -> None:
"""Initialize the remote entity."""
super().__init__(coordinator)
self.madvr_client = coordinator.client
self._attr_unique_id = coordinator.mac
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.mac)},
name="madVR Envy",
manufacturer="madVR",
model="Envy",
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)},
)
@property
def is_on(self) -> bool:
"""Return true if the device is on."""
return self.madvr_client.is_on
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the device."""
_LOGGER.debug("Turning off")
try:
await self.madvr_client.power_off()
except (ConnectionError, NotImplementedError) as err:
_LOGGER.error("Failed to turn off device %s", err)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the device."""
_LOGGER.debug("Turning on device")
try:
await self.madvr_client.power_on(mac=self.coordinator.mac)
except (ConnectionError, NotImplementedError) as err:
_LOGGER.error("Failed to turn on device %s", err)
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send a command to one device."""
_LOGGER.debug("adding command %s", command)
try:
await self.madvr_client.add_command_to_queue(command)
except (ConnectionError, NotImplementedError) as err:
_LOGGER.error("Failed to send command %s", err)

View File

@ -0,0 +1,25 @@
{
"config": {
"step": {
"user": {
"title": "Setup madVR Envy",
"description": "Your device needs to be turned in order to add the integation. ",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of your madVR Envy device.",
"port": "The port your madVR Envy is listening on. In 99% of cases, leave this as the default."
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"no_mac": "A MAC address was not found. It required to identify the device. Please ensure your device is connectable."
}
}
}

View File

@ -322,6 +322,7 @@ FLOWS = {
"lutron",
"lutron_caseta",
"lyric",
"madvr",
"mailgun",
"matter",
"mealie",

View File

@ -3453,6 +3453,12 @@
"integration_type": "virtual",
"supported_by": "motion_blinds"
},
"madvr": {
"name": "madVR Envy",
"integration_type": "device",
"config_flow": true,
"iot_class": "local_push"
},
"mailgun": {
"name": "Mailgun",
"integration_type": "hub",

View File

@ -2633,6 +2633,16 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.madvr.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.mailbox.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

@ -1637,6 +1637,9 @@ py-dormakaba-dkey==1.0.5
# homeassistant.components.improv_ble
py-improv-ble-client==1.0.3
# homeassistant.components.madvr
py-madvr2==1.6.27
# homeassistant.components.melissa
py-melissa-climate==2.1.4

View File

@ -1311,6 +1311,9 @@ py-dormakaba-dkey==1.0.5
# homeassistant.components.improv_ble
py-improv-ble-client==1.0.3
# homeassistant.components.madvr
py-madvr2==1.6.27
# homeassistant.components.melissa
py-melissa-climate==2.1.4

View File

@ -0,0 +1,13 @@
"""Tests for the madvr-envy integration."""
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Fixture for setting up the component."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

View File

@ -0,0 +1,54 @@
"""MadVR conftest for shared testing setup."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.madvr.const import DEFAULT_NAME, DOMAIN
from homeassistant.const import CONF_HOST, CONF_PORT
from .const import MOCK_CONFIG, MOCK_MAC
from tests.common import MockConfigEntry
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.madvr.async_setup_entry",
return_value=True,
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture
def mock_madvr_client() -> Generator[AsyncMock, None, None]:
"""Mock a MadVR client."""
with (
patch(
"homeassistant.components.madvr.config_flow.Madvr", autospec=True
) as mock_client,
patch("homeassistant.components.madvr.Madvr", new=mock_client),
):
client = mock_client.return_value
client.host = MOCK_CONFIG[CONF_HOST]
client.port = MOCK_CONFIG[CONF_PORT]
client.mac_address = MOCK_MAC
client.connected.return_value = True
client.is_device_connectable.return_value = True
client.loop = AsyncMock()
client.tasks = AsyncMock()
yield client
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Mock a config entry."""
return MockConfigEntry(
domain=DOMAIN,
data=MOCK_CONFIG,
unique_id=MOCK_MAC,
title=DEFAULT_NAME,
)

View File

@ -0,0 +1,10 @@
"""Constants for the MadVR tests."""
from homeassistant.const import CONF_HOST, CONF_PORT
MOCK_CONFIG = {
CONF_HOST: "192.168.1.1",
CONF_PORT: 44077,
}
MOCK_MAC = "00:11:22:33:44:55"

View File

@ -0,0 +1,48 @@
# serializer version: 1
# name: test_remote_setup[remote.madvr_envy-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'remote',
'entity_category': None,
'entity_id': 'remote.madvr_envy',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'madvr',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00:11:22:33:44:55',
'unit_of_measurement': None,
})
# ---
# name: test_remote_setup[remote.madvr_envy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'madVR Envy',
'supported_features': <RemoteEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'remote.madvr_envy',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@ -0,0 +1,128 @@
"""Tests for the MadVR config flow."""
from collections.abc import AsyncGenerator
from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.madvr.const import DEFAULT_NAME, DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from .const import MOCK_CONFIG, MOCK_MAC
from tests.common import MockConfigEntry
@pytest.fixture(autouse=True)
async def avoid_wait() -> AsyncGenerator[None, None]:
"""Mock sleep."""
with patch("homeassistant.components.madvr.config_flow.RETRY_INTERVAL", 0):
yield
async def test_full_flow(
hass: HomeAssistant,
mock_madvr_client: AsyncMock,
mock_setup_entry: AsyncMock,
) -> None:
"""Test full config flow."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: MOCK_CONFIG[CONF_HOST], CONF_PORT: MOCK_CONFIG[CONF_PORT]},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["data"] == {
CONF_HOST: MOCK_CONFIG[CONF_HOST],
CONF_PORT: MOCK_CONFIG[CONF_PORT],
}
assert result["result"].unique_id == MOCK_MAC
mock_madvr_client.open_connection.assert_called_once()
mock_madvr_client.async_add_tasks.assert_called_once()
mock_madvr_client.async_cancel_tasks.assert_called_once()
async def test_flow_errors(
hass: HomeAssistant,
mock_madvr_client: AsyncMock,
mock_setup_entry: AsyncMock,
) -> None:
"""Test error handling in config flow."""
mock_madvr_client.open_connection.side_effect = TimeoutError
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_HOST: MOCK_CONFIG[CONF_HOST], CONF_PORT: MOCK_CONFIG[CONF_PORT]},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"}
mock_madvr_client.open_connection.side_effect = None
mock_madvr_client.connected = False
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: MOCK_CONFIG[CONF_HOST], CONF_PORT: MOCK_CONFIG[CONF_PORT]},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "cannot_connect"}
mock_madvr_client.connected = True
mock_madvr_client.mac_address = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: MOCK_CONFIG[CONF_HOST], CONF_PORT: MOCK_CONFIG[CONF_PORT]},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "no_mac"}
# ensure an error is recoverable
mock_madvr_client.mac_address = MOCK_MAC
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_HOST: MOCK_CONFIG[CONF_HOST], CONF_PORT: MOCK_CONFIG[CONF_PORT]},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == DEFAULT_NAME
assert result["data"] == {
CONF_HOST: MOCK_CONFIG[CONF_HOST],
CONF_PORT: MOCK_CONFIG[CONF_PORT],
}
# Verify method calls
assert mock_madvr_client.open_connection.call_count == 4
assert mock_madvr_client.async_add_tasks.call_count == 2
# the first call will not call this due to timeout as expected
assert mock_madvr_client.async_cancel_tasks.call_count == 2
async def test_duplicate(
hass: HomeAssistant,
mock_madvr_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test duplicate config entries."""
mock_config_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_HOST: MOCK_CONFIG[CONF_HOST], CONF_PORT: MOCK_CONFIG[CONF_PORT]},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"

View File

@ -0,0 +1,28 @@
"""Tests for the MadVR integration."""
from __future__ import annotations
from unittest.mock import AsyncMock
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from . import setup_integration
from tests.common import MockConfigEntry
async def test_load_unload_entry(
hass: HomeAssistant,
mock_madvr_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test load and unload entry."""
await setup_integration(hass, mock_config_entry)
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

View File

@ -0,0 +1,85 @@
"""Tests for the MadVR remote entity."""
from __future__ import annotations
from unittest.mock import AsyncMock, patch
from syrupy import SnapshotAssertion
from homeassistant.components.remote import DOMAIN as REMOTE_DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_ON,
Platform,
)
from homeassistant.core import HomeAssistant
import homeassistant.helpers.entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
async def test_remote_setup(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test setup of the remote entity."""
with patch("homeassistant.components.madvr.PLATFORMS", [Platform.REMOTE]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_remote_power(
hass: HomeAssistant,
mock_madvr_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test turning on the remote entity."""
await setup_integration(hass, mock_config_entry)
entity_id = "remote.madvr_envy"
remote = hass.states.get(entity_id)
assert remote.state == STATE_ON
await hass.services.async_call(
REMOTE_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True
)
mock_madvr_client.power_off.assert_called_once()
await hass.services.async_call(
REMOTE_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True
)
await hass.async_block_till_done()
mock_madvr_client.power_on.assert_called_once()
async def test_send_command(
hass: HomeAssistant,
mock_madvr_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test sending command to the remote entity."""
await setup_integration(hass, mock_config_entry)
entity_id = "remote.madvr_envy"
remote = hass.states.get(entity_id)
assert remote.state == STATE_ON
await hass.services.async_call(
REMOTE_DOMAIN,
"send_command",
{ATTR_ENTITY_ID: entity_id, "command": "test"},
blocking=True,
)
mock_madvr_client.add_command_to_queue.assert_called_once_with(["test"])