Skip to content
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

Add support for receivers to HomeKit #100717

Merged
merged 2 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions homeassistant/components/homekit/accessories.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -274,7 +276,7 @@ def __init__(
aid: int,
config: dict,
*args: Any,
category: str = CATEGORY_OTHER,
category: int = CATEGORY_OTHER,
device_id: str | None = None,
**kwargs: Any,
) -> None:
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/homekit/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@
TYPE_SWITCH = "switch"
TYPE_VALVE = "valve"

# #### Categories ####
CATEGORY_RECEIVER = 34

# #### Services ####
SERV_ACCESSORY_INFO = "AccessoryInformation"
SERV_AIR_QUALITY_SENSOR = "AirQualitySensor"
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/homekit/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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": {
Expand Down
22 changes: 20 additions & 2 deletions homeassistant/components/homekit/type_media_players.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Class to hold all media player accessories."""
import logging
from typing import Any

from pyhap.const import CATEGORY_SWITCH

Expand Down Expand Up @@ -36,6 +37,7 @@
from .accessories import TYPES, HomeAccessory
from .const import (
ATTR_KEY_NAME,
CATEGORY_RECEIVER,
CHAR_ACTIVE,
CHAR_MUTE,
CHAR_NAME,
Expand Down Expand Up @@ -218,18 +220,20 @@ def async_update_state(self, new_state):
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
Expand Down Expand Up @@ -358,3 +362,17 @@ def async_update_state(self, new_state):
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)
21 changes: 12 additions & 9 deletions homeassistant/components/homekit/type_remotes.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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 = []
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/homekit/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 12 additions & 2 deletions tests/components/homekit/test_get_accessories.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
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 (
Expand Down Expand Up @@ -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},
{},
),
],
Expand Down
28 changes: 28 additions & 0 deletions tests/components/homekit/test_type_media_players.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -15,6 +16,7 @@
)
from homeassistant.components.homekit.type_media_players import (
MediaPlayer,
ReceiverMediaPlayer,
TelevisionMediaPlayer,
)
from homeassistant.components.media_player import (
Expand Down Expand Up @@ -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