Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions homeassistant/components/apple_tv/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.const import CONF_NAME
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

Expand All @@ -21,23 +21,33 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Load Apple TV binary sensor based on a config entry."""
# apple_tv config entries always have a unique id
manager = config_entry.runtime_data
cb: CALLBACK_TYPE
added = False

@callback
def setup_entities(atv: AppleTV) -> None:
nonlocal added
if added:
return
if atv.features.in_state(FeatureState.Available, FeatureName.TextFocusState):
assert config_entry.unique_id is not None
name: str = config_entry.data[CONF_NAME]
async_add_entities(
[AppleTVKeyboardFocused(name, config_entry.unique_id, manager)]
)
cb()
added = True

cb = async_dispatcher_connect(
hass, f"{SIGNAL_CONNECTED}_{config_entry.unique_id}", setup_entities
config_entry.async_on_unload(
async_dispatcher_connect(
hass, f"{SIGNAL_CONNECTED}_{config_entry.unique_id}", setup_entities
)
)
config_entry.async_on_unload(cb)

# The manager may have already connected (and dispatched SIGNAL_CONNECTED)
# before this platform was forwarded, in which case the signal above was
# missed; handle that case directly.
if manager.atv is not None:
setup_entities(manager.atv)
Comment thread
kroehre marked this conversation as resolved.


class AppleTVKeyboardFocused(AppleTVEntity, BinarySensorEntity, KeyboardListener):
Expand Down
62 changes: 62 additions & 0 deletions tests/components/apple_tv/test_binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Tests for Apple TV binary sensor."""

from unittest.mock import AsyncMock, MagicMock, patch

from pyatv.const import DeviceModel, KeyboardFocusState, Protocol

from homeassistant.components.apple_tv.const import DOMAIN
from homeassistant.const import CONF_ADDRESS, CONF_NAME
from homeassistant.core import HomeAssistant

from .common import create_conf, mrp_service

from tests.common import MockConfigEntry


async def test_keyboard_focus_entity_created_on_setup(
hass: HomeAssistant,
mock_async_zeroconf: MagicMock,
) -> None:
"""Test the keyboard focus binary sensor is created when the device supports it.

Regression test for https://github.com/home-assistant/core/issues/170075 — the
initial SIGNAL_CONNECTED dispatch happens in async_first_connect (before platform
forwarding), so the binary_sensor platform must also handle the already-connected
case rather than relying solely on the dispatcher signal.
"""
atv = AsyncMock()
atv.close = MagicMock()
atv.features = MagicMock()
atv.features.in_state = MagicMock(return_value=True)
atv.keyboard = AsyncMock()
atv.keyboard.text_focus_state = KeyboardFocusState.Unfocused
atv.push_updater = MagicMock()
atv.device_info.model = DeviceModel.Gen4K
atv.device_info.raw_model = "AppleTV6,2"
atv.device_info.version = "15.0"
atv.device_info.mac = "AA:BB:CC:DD:EE:FF"

entry = MockConfigEntry(
domain=DOMAIN,
title="Living Room",
unique_id="mrpid",
data={
CONF_ADDRESS: "127.0.0.1",
CONF_NAME: "Living Room",
"credentials": {str(Protocol.MRP.value): "mrp_creds"},
"identifiers": ["mrpid"],
},
)
entry.add_to_hass(hass)

scan_result = create_conf("127.0.0.1", "Living Room", mrp_service())

with (
patch("homeassistant.components.apple_tv.scan", return_value=[scan_result]),
patch("homeassistant.components.apple_tv.connect", return_value=atv),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()

state = hass.states.get("binary_sensor.living_room_keyboard_focus")
assert state is not None