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 from .coordinator import MadVRCoordinator
PLATFORMS: list[Platform] = [Platform.REMOTE] PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.REMOTE]
type MadVRConfigEntry = ConfigEntry[MadVRCoordinator] 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 assert self.config_entry.unique_id
self.mac = self.config_entry.unique_id self.mac = self.config_entry.unique_id
self.client = client 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) self.client.set_update_callback(self.handle_push_data)
_LOGGER.debug("MadVRCoordinator initialized with mac: %s", self.mac) _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.components.remote import RemoteEntity
from homeassistant.core import HomeAssistant 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.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import MadVRConfigEntry from . import MadVRConfigEntry
from .const import DOMAIN
from .coordinator import MadVRCoordinator from .coordinator import MadVRCoordinator
from .entity import MadVREntity
_LOGGER = logging.getLogger(__name__) _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.""" """Remote entity for the madVR integration."""
_attr_has_entity_name = True
_attr_name = None _attr_name = None
def __init__( def __init__(
@ -47,13 +44,6 @@ class MadvrRemote(CoordinatorEntity[MadVRCoordinator], RemoteEntity):
super().__init__(coordinator) super().__init__(coordinator)
self.madvr_client = coordinator.client self.madvr_client = coordinator.client
self._attr_unique_id = coordinator.mac 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 @property
def is_on(self) -> bool: def is_on(self) -> bool:

View file

@ -21,5 +21,21 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "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." "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.""" """MadVR conftest for shared testing setup."""
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch
import pytest import pytest
@ -40,6 +40,12 @@ def mock_madvr_client() -> Generator[AsyncMock, None, None]:
client.is_device_connectable.return_value = True client.is_device_connectable.return_value = True
client.loop = AsyncMock() client.loop = AsyncMock()
client.tasks = 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 yield client
@ -52,3 +58,30 @@ def mock_config_entry() -> MockConfigEntry:
unique_id=MOCK_MAC, unique_id=MOCK_MAC,
title=DEFAULT_NAME, 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