diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py
index 2c59c998b65..1ad7ed31571 100644
--- a/homeassistant/components/roku/__init__.py
+++ b/homeassistant/components/roku/__init__.py
@@ -15,7 +15,7 @@ from .coordinator import RokuDataUpdateCoordinator
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
-PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE]
+PLATFORMS = [Platform.BINARY_SENSOR, Platform.MEDIA_PLAYER, Platform.REMOTE]
_LOGGER = logging.getLogger(__name__)
diff --git a/homeassistant/components/roku/binary_sensor.py b/homeassistant/components/roku/binary_sensor.py
new file mode 100644
index 00000000000..f7b40def249
--- /dev/null
+++ b/homeassistant/components/roku/binary_sensor.py
@@ -0,0 +1,93 @@
+"""Support for Roku binary sensors."""
+from __future__ import annotations
+
+from collections.abc import Callable
+from dataclasses import dataclass
+
+from rokuecp.models import Device as RokuDevice
+
+from homeassistant.components.binary_sensor import (
+ BinarySensorEntity,
+ BinarySensorEntityDescription,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers.entity import EntityCategory
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
+from .const import DOMAIN
+from .entity import RokuEntity
+
+
+@dataclass
+class RokuBinarySensorEntityDescriptionMixin:
+ """Mixin for required keys."""
+
+ value_fn: Callable[[RokuDevice], bool | None]
+
+
+@dataclass
+class RokuBinarySensorEntityDescription(
+ BinarySensorEntityDescription, RokuBinarySensorEntityDescriptionMixin
+):
+ """Describes a Roku binary sensor entity."""
+
+
+BINARY_SENSORS: tuple[RokuBinarySensorEntityDescription, ...] = (
+ RokuBinarySensorEntityDescription(
+ key="headphones_connected",
+ name="Headphones Connected",
+ icon="mdi:headphones",
+ value_fn=lambda device: device.info.headphones_connected,
+ ),
+ RokuBinarySensorEntityDescription(
+ key="supports_airplay",
+ name="Supports AirPlay",
+ icon="mdi:cast-variant",
+ entity_category=EntityCategory.DIAGNOSTIC,
+ value_fn=lambda device: device.info.supports_airplay,
+ ),
+ RokuBinarySensorEntityDescription(
+ key="supports_ethernet",
+ name="Supports Ethernet",
+ icon="mdi:ethernet",
+ entity_category=EntityCategory.DIAGNOSTIC,
+ value_fn=lambda device: device.info.ethernet_support,
+ ),
+ RokuBinarySensorEntityDescription(
+ key="supports_find_remote",
+ name="Supports Find Remote",
+ icon="mdi:remote",
+ entity_category=EntityCategory.DIAGNOSTIC,
+ value_fn=lambda device: device.info.supports_find_remote,
+ ),
+)
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up a Roku binary sensors based on a config entry."""
+ coordinator = hass.data[DOMAIN][entry.entry_id]
+ unique_id = coordinator.data.info.serial_number
+ async_add_entities(
+ RokuBinarySensorEntity(
+ device_id=unique_id,
+ coordinator=coordinator,
+ description=description,
+ )
+ for description in BINARY_SENSORS
+ )
+
+
+class RokuBinarySensorEntity(RokuEntity, BinarySensorEntity):
+ """Defines a Roku binary sensor."""
+
+ entity_description: RokuBinarySensorEntityDescription
+
+ @property
+ def is_on(self) -> bool:
+ """Return the state of the sensor."""
+ return bool(self.entity_description.value_fn(self.coordinator.data))
diff --git a/homeassistant/components/roku/entity.py b/homeassistant/components/roku/entity.py
index baaf7a4b175..17a39a17609 100644
--- a/homeassistant/components/roku/entity.py
+++ b/homeassistant/components/roku/entity.py
@@ -1,7 +1,7 @@
"""Base Entity for Roku."""
from __future__ import annotations
-from homeassistant.helpers.entity import DeviceInfo
+from homeassistant.helpers.entity import DeviceInfo, EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import RokuDataUpdateCoordinator
@@ -14,12 +14,21 @@ class RokuEntity(CoordinatorEntity):
coordinator: RokuDataUpdateCoordinator
def __init__(
- self, *, device_id: str, coordinator: RokuDataUpdateCoordinator
+ self,
+ *,
+ device_id: str,
+ coordinator: RokuDataUpdateCoordinator,
+ description: EntityDescription | None = None,
) -> None:
"""Initialize the Roku entity."""
super().__init__(coordinator)
self._device_id = device_id
+ if description is not None:
+ self.entity_description = description
+ self._attr_name = f"{coordinator.data.info.name} {description.name}"
+ self._attr_unique_id = f"{device_id}_{description.key}"
+
@property
def device_info(self) -> DeviceInfo:
"""Return device information about this Roku device."""
diff --git a/tests/components/roku/fixtures/rokutv-device-info.xml b/tests/components/roku/fixtures/rokutv-device-info.xml
index 658fc130629..cbb538ba4c1 100644
--- a/tests/components/roku/fixtures/rokutv-device-info.xml
+++ b/tests/components/roku/fixtures/rokutv-device-info.xml
@@ -59,6 +59,7 @@
true
true
true
+ true
true
true
https://www.onntvsupport.com/
diff --git a/tests/components/roku/test_binary_sensor.py b/tests/components/roku/test_binary_sensor.py
new file mode 100644
index 00000000000..e4d9ccad675
--- /dev/null
+++ b/tests/components/roku/test_binary_sensor.py
@@ -0,0 +1,161 @@
+"""Tests for the sensors provided by the Roku integration."""
+from homeassistant.components.binary_sensor import STATE_OFF, STATE_ON
+from homeassistant.components.roku.const import DOMAIN
+from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_FRIENDLY_NAME, ATTR_ICON
+from homeassistant.core import HomeAssistant
+from homeassistant.helpers import device_registry as dr, entity_registry as er
+from homeassistant.helpers.entity import EntityCategory
+
+from tests.components.roku import UPNP_SERIAL, setup_integration
+from tests.test_util.aiohttp import AiohttpClientMocker
+
+
+async def test_roku_binary_sensors(
+ hass: HomeAssistant,
+ aioclient_mock: AiohttpClientMocker,
+) -> None:
+ """Test the Roku binary sensors."""
+ await setup_integration(hass, aioclient_mock)
+
+ entity_registry = er.async_get(hass)
+ device_registry = dr.async_get(hass)
+
+ state = hass.states.get("binary_sensor.my_roku_3_headphones_connected")
+ entry = entity_registry.async_get("binary_sensor.my_roku_3_headphones_connected")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{UPNP_SERIAL}_headphones_connected"
+ assert entry.entity_category is None
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Headphones Connected"
+ assert state.attributes.get(ATTR_ICON) == "mdi:headphones"
+ assert ATTR_DEVICE_CLASS not in state.attributes
+
+ state = hass.states.get("binary_sensor.my_roku_3_supports_airplay")
+ entry = entity_registry.async_get("binary_sensor.my_roku_3_supports_airplay")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{UPNP_SERIAL}_supports_airplay"
+ assert entry.entity_category == EntityCategory.DIAGNOSTIC
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports AirPlay"
+ assert state.attributes.get(ATTR_ICON) == "mdi:cast-variant"
+ assert ATTR_DEVICE_CLASS not in state.attributes
+
+ state = hass.states.get("binary_sensor.my_roku_3_supports_ethernet")
+ entry = entity_registry.async_get("binary_sensor.my_roku_3_supports_ethernet")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{UPNP_SERIAL}_supports_ethernet"
+ assert entry.entity_category == EntityCategory.DIAGNOSTIC
+ assert state.state == STATE_ON
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports Ethernet"
+ assert state.attributes.get(ATTR_ICON) == "mdi:ethernet"
+ assert ATTR_DEVICE_CLASS not in state.attributes
+
+ state = hass.states.get("binary_sensor.my_roku_3_supports_find_remote")
+ entry = entity_registry.async_get("binary_sensor.my_roku_3_supports_find_remote")
+ assert entry
+ assert state
+ assert entry.unique_id == f"{UPNP_SERIAL}_supports_find_remote"
+ assert entry.entity_category == EntityCategory.DIAGNOSTIC
+ assert state.state == STATE_OFF
+ assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Roku 3 Supports Find Remote"
+ assert state.attributes.get(ATTR_ICON) == "mdi:remote"
+ assert ATTR_DEVICE_CLASS not in state.attributes
+
+ assert entry.device_id
+ device_entry = device_registry.async_get(entry.device_id)
+ assert device_entry
+ assert device_entry.identifiers == {(DOMAIN, UPNP_SERIAL)}
+ assert device_entry.manufacturer == "Roku"
+ assert device_entry.model == "Roku 3"
+ assert device_entry.name == "My Roku 3"
+ assert device_entry.entry_type is None
+ assert device_entry.sw_version == "7.5.0"
+
+
+async def test_rokutv_binary_sensors(
+ hass: HomeAssistant,
+ aioclient_mock: AiohttpClientMocker,
+) -> None:
+ """Test the Roku binary sensors."""
+ await setup_integration(
+ hass,
+ aioclient_mock,
+ device="rokutv",
+ app="tvinput-dtv",
+ host="192.168.1.161",
+ unique_id="YN00H5555555",
+ )
+
+ entity_registry = er.async_get(hass)
+ device_registry = dr.async_get(hass)
+
+ state = hass.states.get("binary_sensor.58_onn_roku_tv_headphones_connected")
+ entry = entity_registry.async_get(
+ "binary_sensor.58_onn_roku_tv_headphones_connected"
+ )
+ assert entry
+ assert state
+ assert entry.unique_id == "YN00H5555555_headphones_connected"
+ assert entry.entity_category is None
+ assert state.state == STATE_OFF
+ assert (
+ state.attributes.get(ATTR_FRIENDLY_NAME)
+ == '58" Onn Roku TV Headphones Connected'
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:headphones"
+ assert ATTR_DEVICE_CLASS not in state.attributes
+
+ state = hass.states.get("binary_sensor.58_onn_roku_tv_supports_airplay")
+ entry = entity_registry.async_get("binary_sensor.58_onn_roku_tv_supports_airplay")
+ assert entry
+ assert state
+ assert entry.unique_id == "YN00H5555555_supports_airplay"
+ assert entry.entity_category == EntityCategory.DIAGNOSTIC
+ assert state.state == STATE_ON
+ assert (
+ state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Supports AirPlay'
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:cast-variant"
+ assert ATTR_DEVICE_CLASS not in state.attributes
+
+ state = hass.states.get("binary_sensor.58_onn_roku_tv_supports_ethernet")
+ entry = entity_registry.async_get("binary_sensor.58_onn_roku_tv_supports_ethernet")
+ assert entry
+ assert state
+ assert entry.unique_id == "YN00H5555555_supports_ethernet"
+ assert entry.entity_category == EntityCategory.DIAGNOSTIC
+ assert state.state == STATE_ON
+ assert (
+ state.attributes.get(ATTR_FRIENDLY_NAME) == '58" Onn Roku TV Supports Ethernet'
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:ethernet"
+ assert ATTR_DEVICE_CLASS not in state.attributes
+
+ state = hass.states.get("binary_sensor.58_onn_roku_tv_supports_find_remote")
+ entry = entity_registry.async_get(
+ "binary_sensor.58_onn_roku_tv_supports_find_remote"
+ )
+ assert entry
+ assert state
+ assert entry.unique_id == "YN00H5555555_supports_find_remote"
+ assert entry.entity_category == EntityCategory.DIAGNOSTIC
+ assert state.state == STATE_ON
+ assert (
+ state.attributes.get(ATTR_FRIENDLY_NAME)
+ == '58" Onn Roku TV Supports Find Remote'
+ )
+ assert state.attributes.get(ATTR_ICON) == "mdi:remote"
+ assert ATTR_DEVICE_CLASS not in state.attributes
+
+ assert entry.device_id
+ device_entry = device_registry.async_get(entry.device_id)
+ assert device_entry
+ assert device_entry.identifiers == {(DOMAIN, "YN00H5555555")}
+ assert device_entry.manufacturer == "Onn"
+ assert device_entry.model == "100005844"
+ assert device_entry.name == '58" Onn Roku TV'
+ assert device_entry.entry_type is None
+ assert device_entry.sw_version == "9.2.0"