-
-
Notifications
You must be signed in to change notification settings - Fork 37.5k
Add madvr envy integration #120382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
joostlek
merged 74 commits into
home-assistant:dev
from
iloveicedgreentea:add-madvr-envy
Jul 7, 2024
Merged
Add madvr envy integration #120382
Changes from all commits
Commits
Show all changes
74 commits
Select commit
Hold shift + click to select a range
77683bc
feat: Add madvr envy
iloveicedgreentea 5b3141b
fix: await and pass entry directly
iloveicedgreentea 86b73a2
fix: add attributes and unique id for sensors
iloveicedgreentea 5d9591c
fix: reflect power state well, improve state detection
iloveicedgreentea 0ce0ebf
fix: don't connect on init, add options, add reload on change, keep o…
iloveicedgreentea f126e2e
fix: cancel tasks on unload
iloveicedgreentea b427f91
fix: test connection via library
iloveicedgreentea 91eea26
fix: wait for boot time
iloveicedgreentea e39c2e7
docs: add readme and license
iloveicedgreentea f12776a
fix: broken pipe in lib
iloveicedgreentea a1d7eb3
fix: detect out of band power off
iloveicedgreentea 8e6b9de
fix: improve extra attributes
iloveicedgreentea f1ecd96
fix: fix unloading, add config flow test, limit to one platform
iloveicedgreentea df7320f
fix: use conf, refresh coordinator, other comments
iloveicedgreentea 8f72143
fix: remove event data
iloveicedgreentea 291858d
fix: fix tests passing, remove wake on lan
iloveicedgreentea b0fa76d
fix: dont allow to proceed unless connection works
iloveicedgreentea 70f115e
chore: update dep
iloveicedgreentea 37d9189
fix: update config flow, add constants
iloveicedgreentea 657940b
fix: write state, use runtime data instead
iloveicedgreentea 8f14119
fix: remove await
iloveicedgreentea c5c58c6
fix: move unloading and stuff to coordinator/init
iloveicedgreentea 5044994
fix: pass in config entry with correct type
iloveicedgreentea 5d108ae
fix: move queue and tasks to library
iloveicedgreentea 919a356
fix: config flow error flow, tests, name, and update lib
iloveicedgreentea e2266c7
fix: update lib, leave connection open on setup
iloveicedgreentea 214bdbb
fix: update lib
iloveicedgreentea 3e3dc7f
fix: address comments, remove wol from lib
iloveicedgreentea b566c05
fix: remove unneeded options
iloveicedgreentea ec397db
fix: remove fields
iloveicedgreentea f6ab532
fix: simplify code, address comments
iloveicedgreentea 12e91ac
fix: move error to lib
iloveicedgreentea 381fbb2
fix: fix test
iloveicedgreentea 25caf1e
fix: stronger types
iloveicedgreentea 1041668
fix: update lib
iloveicedgreentea 639f063
fix: missing text from options flow
iloveicedgreentea 7f486c0
chore: remove options flow
iloveicedgreentea b8d3be1
chore: remove import
iloveicedgreentea 8a6c035
chore: update comments
iloveicedgreentea 0dfdad0
fix: get mac from device, persist
iloveicedgreentea c49a180
fix: add mac stuff to test
iloveicedgreentea 1ff2c5a
fix: startup import errors
iloveicedgreentea 4bb0efc
chore: stale comment
iloveicedgreentea 35e33b7
fix: get mac from persisted config
iloveicedgreentea 4f57fc0
chore: update lib
iloveicedgreentea f11fd6b
fix: persist mac in a better way
iloveicedgreentea b4cc6db
feat: use mac as unique ID for entry
iloveicedgreentea 3720ad1
fix: use unique ID from mac, add proper device
iloveicedgreentea b372063
fix: will not be set in init potentially
iloveicedgreentea a96151a
fix: access mac
iloveicedgreentea e4e0e6b
fix: optimize, move error to lib
iloveicedgreentea 7db53e5
feat: add coordinator test, use conf
iloveicedgreentea 88fa8be
fix: use one mock, add init test
iloveicedgreentea af64d39
fix: not async
iloveicedgreentea 73ec34c
feat: add remote test
iloveicedgreentea 4a70401
Merge branch 'dev' into add-madvr-envy
iloveicedgreentea 750842c
fix: types
iloveicedgreentea befb6bc
fix: patch client, expand remote tests
iloveicedgreentea d9a2597
fix: use snapshot test
iloveicedgreentea 577b150
Merge branch 'dev' into add-madvr-envy
iloveicedgreentea b85237d
fix: update branding
iloveicedgreentea 2ef3b2b
fix: add description, fix type check
iloveicedgreentea a25bec0
fix: update tests
iloveicedgreentea e2e5a59
fix: test
iloveicedgreentea f44c83b
fix: update test
iloveicedgreentea 613e1f5
fix: camelcase
iloveicedgreentea 927afeb
Fix
joostlek b976524
Merge branch 'dev' into add-madvr-envy
iloveicedgreentea e20c8d5
Merge branch 'refs/heads/add-madvr-envy2' into add-madvr-envy
joostlek fba9ac9
feat: strict typing
iloveicedgreentea 58cdf68
fix: strict typing in lib
iloveicedgreentea a11aebe
fix: type will never be None
iloveicedgreentea 239f964
fix: reference to mac, all tests passing
iloveicedgreentea d64f2a6
Merge branch 'dev' into add-madvr-envy
iloveicedgreentea File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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): | ||
|
iloveicedgreentea marked this conversation as resolved.
|
||
| """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) | ||
|
iloveicedgreentea marked this conversation as resolved.
|
||
| 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, | ||
| ) | ||
|
iloveicedgreentea marked this conversation as resolved.
|
||
|
|
||
| # 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 | ||
|
iloveicedgreentea marked this conversation as resolved.
|
||
|
|
||
| # 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() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| """Constants for the madvr-envy integration.""" | ||
|
|
||
| DOMAIN = "madvr" | ||
|
|
||
| DEFAULT_NAME = "envy" | ||
| DEFAULT_PORT = 44077 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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)) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| """Errors for the madvr component.""" | ||
|
|
||
|
|
||
| class CannotConnect(Exception): | ||
| """Error to indicate we cannot connect.""" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"] | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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." | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.