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
parent
8110b60fc9
commit
12228d8a00
|
@ -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.*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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()
|
|
@ -0,0 +1,6 @@
|
|||
"""Constants for the madvr-envy integration."""
|
||||
|
||||
DOMAIN = "madvr"
|
||||
|
||||
DEFAULT_NAME = "envy"
|
||||
DEFAULT_PORT = 44077
|
|
@ -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))
|
|
@ -0,0 +1,5 @@
|
|||
"""Errors for the madvr component."""
|
||||
|
||||
|
||||
class CannotConnect(Exception):
|
||||
"""Error to indicate we cannot connect."""
|
|
@ -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"]
|
||||
}
|
|
@ -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)
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -322,6 +322,7 @@ FLOWS = {
|
|||
"lutron",
|
||||
"lutron_caseta",
|
||||
"lyric",
|
||||
"madvr",
|
||||
"mailgun",
|
||||
"matter",
|
||||
"mealie",
|
||||
|
|
|
@ -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",
|
||||
|
|
10
mypy.ini
10
mypy.ini
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -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,
|
||||
)
|
|
@ -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"
|
|
@ -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',
|
||||
})
|
||||
# ---
|
|
@ -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"
|
|
@ -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
|
|
@ -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"])
|
Loading…
Reference in New Issue