Add support for receivers to HomeKit (#100717)

This commit is contained in:
J. Nick Koston 2023-09-25 09:36:01 -05:00 committed by GitHub
parent c1b9400833
commit 4c255677c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 83 additions and 18 deletions

View file

@ -183,7 +183,9 @@ def get_accessory( # noqa: C901
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
feature_list = config.get(CONF_FEATURE_LIST, [])
if device_class == MediaPlayerDeviceClass.TV:
if device_class == MediaPlayerDeviceClass.RECEIVER:
a_type = "ReceiverMediaPlayer"
elif device_class == MediaPlayerDeviceClass.TV:
a_type = "TelevisionMediaPlayer"
elif validate_media_player_features(state, feature_list):
a_type = "MediaPlayer"
@ -274,7 +276,7 @@ class HomeAccessory(Accessory): # type: ignore[misc]
aid: int,
config: dict,
*args: Any,
category: str = CATEGORY_OTHER,
category: int = CATEGORY_OTHER,
device_id: str | None = None,
**kwargs: Any,
) -> None:

View file

@ -115,6 +115,9 @@ TYPE_SPRINKLER = "sprinkler"
TYPE_SWITCH = "switch"
TYPE_VALVE = "valve"
# #### Categories ####
CATEGORY_RECEIVER = 34
# #### Services ####
SERV_ACCESSORY_INFO = "AccessoryInformation"
SERV_AIR_QUALITY_SENSOR = "AirQualitySensor"

View file

@ -11,7 +11,7 @@
"include_exclude_mode": "Inclusion Mode",
"domains": "[%key:component::homekit::config::step::user::data::include_domains%]"
},
"description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be included to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.",
"description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV or RECEIVER device class to function properly. Entities in the \u201cDomains to include\u201d will be included to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.",
"title": "Select mode and domains."
},
"accessory": {
@ -57,7 +57,7 @@
"data": {
"include_domains": "Domains to include"
},
"description": "Choose the domains to be included. All supported entities in the domain will be included except for categorized entities. A separate HomeKit instance in accessory mode will be created for each tv media player, activity based remote, lock, and camera.",
"description": "Choose the domains to be included. All supported entities in the domain will be included except for categorized entities. A separate HomeKit instance in accessory mode will be created for each tv/receiver media player, activity based remote, lock, and camera.",
"title": "Select domains to be included"
},
"pairing": {

View file

@ -1,5 +1,6 @@
"""Class to hold all media player accessories."""
import logging
from typing import Any
from pyhap.const import CATEGORY_SWITCH
@ -36,6 +37,7 @@ from homeassistant.core import callback
from .accessories import TYPES, HomeAccessory
from .const import (
ATTR_KEY_NAME,
CATEGORY_RECEIVER,
CHAR_ACTIVE,
CHAR_MUTE,
CHAR_NAME,
@ -218,18 +220,20 @@ class MediaPlayer(HomeAccessory):
class TelevisionMediaPlayer(RemoteInputSelectAccessory):
"""Generate a Television Media Player accessory."""
def __init__(self, *args):
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize a Television Media Player accessory object."""
super().__init__(
MediaPlayerEntityFeature.SELECT_SOURCE,
ATTR_INPUT_SOURCE,
ATTR_INPUT_SOURCE_LIST,
*args,
**kwargs,
)
state = self.hass.states.get(self.entity_id)
assert state
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
self.chars_speaker = []
self.chars_speaker: list[str] = []
self._supports_play_pause = features & (
MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE
@ -358,3 +362,17 @@ class TelevisionMediaPlayer(RemoteInputSelectAccessory):
self.char_mute.set_value(current_mute_state)
self._async_update_input_state(hk_state, new_state)
@TYPES.register("ReceiverMediaPlayer")
class ReceiverMediaPlayer(TelevisionMediaPlayer):
"""Generate a Receiver Media Player accessory.
For HomeKit, a Receiver Media Player is exactly the same as a
Television Media Player except it has a different category
which will tell HomeKit how to render the device.
"""
def __init__(self, *args: Any) -> None:
"""Initialize a Receiver Media Player accessory object."""
super().__init__(*args, category=CATEGORY_RECEIVER)

View file

@ -1,6 +1,7 @@
"""Class to hold remote accessories."""
from abc import ABC, abstractmethod
import logging
from typing import Any
from pyhap.const import CATEGORY_TELEVISION
@ -80,19 +81,21 @@ class RemoteInputSelectAccessory(HomeAccessory, ABC):
def __init__(
self,
required_feature,
source_key,
source_list_key,
*args,
**kwargs,
):
required_feature: int,
source_key: str,
source_list_key: str,
*args: Any,
category: int = CATEGORY_TELEVISION,
**kwargs: Any,
) -> None:
"""Initialize a InputSelect accessory object."""
super().__init__(*args, category=CATEGORY_TELEVISION, **kwargs)
super().__init__(*args, category=category, **kwargs)
state = self.hass.states.get(self.entity_id)
assert state
features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
self._mapped_sources_list = []
self._mapped_sources = {}
self._mapped_sources_list: list[str] = []
self._mapped_sources: dict[str, str] = {}
self.source_key = source_key
self.source_list_key = source_list_key
self.sources = []

View file

@ -614,7 +614,8 @@ def state_needs_accessory_mode(state: State) -> bool:
return (
state.domain == MEDIA_PLAYER_DOMAIN
and state.attributes.get(ATTR_DEVICE_CLASS) == MediaPlayerDeviceClass.TV
and state.attributes.get(ATTR_DEVICE_CLASS)
in (MediaPlayerDeviceClass.TV, MediaPlayerDeviceClass.RECEIVER)
or state.domain == REMOTE_DOMAIN
and state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
& RemoteEntityFeature.ACTIVITY

View file

@ -17,7 +17,10 @@ from homeassistant.components.homekit.const import (
TYPE_SWITCH,
TYPE_VALVE,
)
from homeassistant.components.media_player import MediaPlayerEntityFeature
from homeassistant.components.media_player import (
MediaPlayerDeviceClass,
MediaPlayerEntityFeature,
)
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.vacuum import VacuumEntityFeature
from homeassistant.const import (
@ -202,7 +205,14 @@ def test_type_covers(type_name, entity_id, state, attrs) -> None:
"TelevisionMediaPlayer",
"media_player.tv",
"on",
{ATTR_DEVICE_CLASS: "tv"},
{ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.TV},
{},
),
(
"ReceiverMediaPlayer",
"media_player.receiver",
"on",
{ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.RECEIVER},
{},
),
],

View file

@ -1,6 +1,7 @@
"""Test different accessory types: Media Players."""
import pytest
from homeassistant.components.homekit.accessories import HomeDriver
from homeassistant.components.homekit.const import (
ATTR_KEY_NAME,
ATTR_VALUE,
@ -15,6 +16,7 @@ from homeassistant.components.homekit.const import (
)
from homeassistant.components.homekit.type_media_players import (
MediaPlayer,
ReceiverMediaPlayer,
TelevisionMediaPlayer,
)
from homeassistant.components.media_player import (
@ -629,3 +631,29 @@ async def test_media_player_television_unsafe_chars(
assert events[-1].data[ATTR_VALUE] is None
assert acc.char_input_source.value == 4
async def test_media_player_receiver(
hass: HomeAssistant, hk_driver: HomeDriver, caplog: pytest.LogCaptureFixture
) -> None:
"""Test if television accessory with unsafe characters."""
entity_id = "media_player.receiver"
sources = ["MUSIC", "HDMI 3/ARC", "SCREEN MIRRORING", "HDMI 2/MHL", "HDMI", "MUSIC"]
hass.states.async_set(
entity_id,
None,
{
ATTR_DEVICE_CLASS: MediaPlayerDeviceClass.TV,
ATTR_SUPPORTED_FEATURES: 3469,
ATTR_MEDIA_VOLUME_MUTED: False,
ATTR_INPUT_SOURCE: "HDMI 2/MHL",
ATTR_INPUT_SOURCE_LIST: sources,
},
)
await hass.async_block_till_done()
acc = ReceiverMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None)
await acc.run()
await hass.async_block_till_done()
assert acc.aid == 2
assert acc.category == 34 # Receiver