Add binary sensor to madVR integration (#121465)

* feat: add binary sensor and tests

* fix: update test

* fix: use entity description

* feat: use translation key

* feat: implement base entity

* fix: change device classes

* fix: remove some types

* fix: coordinator.data none on init

* fix: names, tests

* feat: parameterize tests
This commit is contained in:
ilan 2024-07-09 13:11:08 -04:00 committed by GitHub
parent 898803abe9
commit 31dc80c616
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 648 additions and 14 deletions

View File

@ -12,7 +12,7 @@ from homeassistant.core import Event, HomeAssistant, callback
from .coordinator import MadVRCoordinator
PLATFORMS: list[Platform] = [Platform.REMOTE]
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.REMOTE]
type MadVRConfigEntry = ConfigEntry[MadVRCoordinator]

View File

@ -0,0 +1,86 @@
"""Binary sensor entities for the madVR integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import MadVRConfigEntry
from .coordinator import MadVRCoordinator
from .entity import MadVREntity
_HDR_FLAG = "hdr_flag"
_OUTGOING_HDR_FLAG = "outgoing_hdr_flag"
_POWER_STATE = "power_state"
_SIGNAL_STATE = "signal_state"
@dataclass(frozen=True, kw_only=True)
class MadvrBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describe madVR binary sensor entity."""
value_fn: Callable[[MadVRCoordinator], bool]
BINARY_SENSORS: tuple[MadvrBinarySensorEntityDescription, ...] = (
MadvrBinarySensorEntityDescription(
key=_POWER_STATE,
translation_key=_POWER_STATE,
value_fn=lambda coordinator: coordinator.data.get("is_on", False),
),
MadvrBinarySensorEntityDescription(
key=_SIGNAL_STATE,
translation_key=_SIGNAL_STATE,
value_fn=lambda coordinator: coordinator.data.get("is_signal", False),
),
MadvrBinarySensorEntityDescription(
key=_HDR_FLAG,
translation_key=_HDR_FLAG,
value_fn=lambda coordinator: coordinator.data.get("hdr_flag", False),
),
MadvrBinarySensorEntityDescription(
key=_OUTGOING_HDR_FLAG,
translation_key=_OUTGOING_HDR_FLAG,
value_fn=lambda coordinator: coordinator.data.get("outgoing_hdr_flag", False),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: MadVRConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the binary sensor entities."""
coordinator = entry.runtime_data
async_add_entities(
MadvrBinarySensor(coordinator, description) for description in BINARY_SENSORS
)
class MadvrBinarySensor(MadVREntity, BinarySensorEntity):
"""Base class for madVR binary sensors."""
entity_description: MadvrBinarySensorEntityDescription
def __init__(
self,
coordinator: MadVRCoordinator,
description: MadvrBinarySensorEntityDescription,
) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.mac}_{description.key}"
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return self.entity_description.value_fn(self.coordinator)

View File

@ -33,6 +33,9 @@ class MadVRCoordinator(DataUpdateCoordinator[dict[str, Any]]):
assert self.config_entry.unique_id
self.mac = self.config_entry.unique_id
self.client = client
# this does not use poll/refresh, so we need to set this to not None on init
self.data = {}
# this passes a callback to the client to push new data to the coordinator
self.client.set_update_callback(self.handle_push_data)
_LOGGER.debug("MadVRCoordinator initialized with mac: %s", self.mac)

View File

@ -0,0 +1,24 @@
"""Base class for madVR entities."""
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import MadVRCoordinator
class MadVREntity(CoordinatorEntity[MadVRCoordinator]):
"""Defines a base madVR entity."""
_attr_has_entity_name = True
def __init__(self, coordinator: MadVRCoordinator) -> None:
"""Initialize madvr entity."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.mac)},
name="madVR Envy",
manufacturer="madVR",
model="Envy",
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)},
)

View File

@ -0,0 +1,30 @@
{
"entity": {
"binary_sensor": {
"hdr_flag": {
"default": "mdi:hdr",
"state": {
"off": "mdi:hdr-off"
}
},
"outgoing_hdr_flag": {
"default": "mdi:hdr",
"state": {
"off": "mdi:hdr-off"
}
},
"power_state": {
"default": "mdi:power",
"state": {
"off": "mdi:power-off"
}
},
"signal_state": {
"default": "mdi:signal",
"state": {
"off": "mdi:signal-off"
}
}
}
}
}

View File

@ -8,13 +8,11 @@ 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
from .entity import MadVREntity
_LOGGER = logging.getLogger(__name__)
@ -33,10 +31,9 @@ async def async_setup_entry(
)
class MadvrRemote(CoordinatorEntity[MadVRCoordinator], RemoteEntity):
class MadvrRemote(MadVREntity, RemoteEntity):
"""Remote entity for the madVR integration."""
_attr_has_entity_name = True
_attr_name = None
def __init__(
@ -47,13 +44,6 @@ class MadvrRemote(CoordinatorEntity[MadVRCoordinator], RemoteEntity):
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:

View File

@ -21,5 +21,21 @@
"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."
}
},
"entity": {
"binary_sensor": {
"hdr_flag": {
"name": "HDR flag"
},
"outgoing_hdr_flag": {
"name": "Outgoing HDR flag"
},
"power_state": {
"name": "Power state"
},
"signal_state": {
"name": "Signal state"
}
}
}
}

View File

@ -1,7 +1,7 @@
"""MadVR conftest for shared testing setup."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
import pytest
@ -40,6 +40,12 @@ def mock_madvr_client() -> Generator[AsyncMock, None, None]:
client.is_device_connectable.return_value = True
client.loop = AsyncMock()
client.tasks = AsyncMock()
client.set_update_callback = MagicMock()
# mock the property to be off on startup (which it is)
is_on_mock = PropertyMock(return_value=True)
type(client).is_on = is_on_mock
yield client
@ -52,3 +58,30 @@ def mock_config_entry() -> MockConfigEntry:
unique_id=MOCK_MAC,
title=DEFAULT_NAME,
)
def get_update_callback(mock_client: MagicMock):
"""Retrieve the update callback function from the mocked client.
This function extracts the callback that was passed to set_update_callback
on the mocked MadVR client. This callback is typically the handle_push_data
method of the MadVRCoordinator.
Args:
mock_client (MagicMock): The mocked MadVR client.
Returns:
function: The update callback function.
"""
# Get all the calls made to set_update_callback
calls = mock_client.set_update_callback.call_args_list
if not calls:
raise ValueError("set_update_callback was not called on the mock client")
# Get the first (and usually only) call
first_call = calls[0]
# Get the first argument of this call, which should be the callback function
return first_call.args[0]

View File

@ -0,0 +1,373 @@
# serializer version: 1
# name: test_binary_sensor_setup[binary_sensor.madvr_envy_hdr_flag-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.madvr_envy_hdr_flag',
'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': 'HDR flag',
'platform': 'madvr',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'hdr_flag',
'unique_id': '00:11:22:33:44:55_hdr_flag',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor_setup[binary_sensor.madvr_envy_hdr_flag-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'madVR Envy HDR flag',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.madvr_envy_hdr_flag',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensor_setup[binary_sensor.madvr_envy_madvr_hdr_flag-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.madvr_envy_madvr_hdr_flag',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:hdr-off',
'original_name': 'madvr HDR Flag',
'platform': 'madvr',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00:11:22:33:44:55_hdr_flag',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor_setup[binary_sensor.madvr_envy_madvr_hdr_flag-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'madVR Envy madvr HDR Flag',
'icon': 'mdi:hdr-off',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.madvr_envy_madvr_hdr_flag',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensor_setup[binary_sensor.madvr_envy_madvr_outgoing_hdr_flag-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.madvr_envy_madvr_outgoing_hdr_flag',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:hdr-off',
'original_name': 'madvr Outgoing HDR Flag',
'platform': 'madvr',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00:11:22:33:44:55_outgoing_hdr_flag',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor_setup[binary_sensor.madvr_envy_madvr_outgoing_hdr_flag-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'madVR Envy madvr Outgoing HDR Flag',
'icon': 'mdi:hdr-off',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.madvr_envy_madvr_outgoing_hdr_flag',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensor_setup[binary_sensor.madvr_envy_madvr_power_state-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.madvr_envy_madvr_power_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:power-off',
'original_name': 'madvr Power State',
'platform': 'madvr',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00:11:22:33:44:55_power_state',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor_setup[binary_sensor.madvr_envy_madvr_power_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'madVR Envy madvr Power State',
'icon': 'mdi:power-off',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.madvr_envy_madvr_power_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensor_setup[binary_sensor.madvr_envy_madvr_signal_state-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.madvr_envy_madvr_signal_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': 'mdi:signal-off',
'original_name': 'madvr Signal State',
'platform': 'madvr',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '00:11:22:33:44:55_signal_state',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor_setup[binary_sensor.madvr_envy_madvr_signal_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'madVR Envy madvr Signal State',
'icon': 'mdi:signal-off',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.madvr_envy_madvr_signal_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensor_setup[binary_sensor.madvr_envy_outgoing_hdr_flag-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.madvr_envy_outgoing_hdr_flag',
'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': 'Outgoing HDR flag',
'platform': 'madvr',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'outgoing_hdr_flag',
'unique_id': '00:11:22:33:44:55_outgoing_hdr_flag',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor_setup[binary_sensor.madvr_envy_outgoing_hdr_flag-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'madVR Envy Outgoing HDR flag',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.madvr_envy_outgoing_hdr_flag',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensor_setup[binary_sensor.madvr_envy_power_state-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.madvr_envy_power_state',
'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': 'Power state',
'platform': 'madvr',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'power_state',
'unique_id': '00:11:22:33:44:55_power_state',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor_setup[binary_sensor.madvr_envy_power_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'madVR Envy Power state',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.madvr_envy_power_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensor_setup[binary_sensor.madvr_envy_signal_state-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': None,
'entity_id': 'binary_sensor.madvr_envy_signal_state',
'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': 'Signal state',
'platform': 'madvr',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'signal_state',
'unique_id': '00:11:22:33:44:55_signal_state',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensor_setup[binary_sensor.madvr_envy_signal_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'madVR Envy Signal state',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.madvr_envy_signal_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@ -0,0 +1,79 @@
"""Tests for the MadVR binary sensor entities."""
from __future__ import annotations
from unittest.mock import AsyncMock, patch
import pytest
from syrupy import SnapshotAssertion
from homeassistant.const import STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant
import homeassistant.helpers.entity_registry as er
from . import setup_integration
from .conftest import get_update_callback
from tests.common import MockConfigEntry, snapshot_platform
async def test_binary_sensor_setup(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test setup of the binary sensor entities."""
with patch("homeassistant.components.madvr.PLATFORMS", [Platform.BINARY_SENSOR]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
@pytest.mark.parametrize(
("entity_id", "positive_payload", "negative_payload"),
[
(
"binary_sensor.madvr_envy_power_state",
{"is_on": True},
{"is_on": False},
),
(
"binary_sensor.madvr_envy_signal_state",
{"is_signal": True},
{"is_signal": False},
),
(
"binary_sensor.madvr_envy_hdr_flag",
{"hdr_flag": True},
{"hdr_flag": False},
),
(
"binary_sensor.madvr_envy_outgoing_hdr_flag",
{"outgoing_hdr_flag": True},
{"outgoing_hdr_flag": False},
),
],
)
async def test_binary_sensors(
hass: HomeAssistant,
mock_madvr_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_id: str,
positive_payload: dict,
negative_payload: dict,
) -> None:
"""Test the binary sensors."""
await setup_integration(hass, mock_config_entry)
update_callback = get_update_callback(mock_madvr_client)
# Test positive state
update_callback(positive_payload)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == STATE_ON
# Test negative state
update_callback(negative_payload)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state.state == STATE_OFF