mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Move key sequences to bridge in SamsungTV (#67762)
Co-authored-by: epenet <epenet@users.noreply.github.com>
This commit is contained in:
parent
d302b0d14e
commit
1793c29fac
@ -45,6 +45,8 @@ from .const import (
|
|||||||
WEBSOCKET_PORTS,
|
WEBSOCKET_PORTS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
KEY_PRESS_TIMEOUT = 1.2
|
||||||
|
|
||||||
|
|
||||||
def mac_from_device_info(info: dict[str, Any]) -> str | None:
|
def mac_from_device_info(info: dict[str, Any]) -> str | None:
|
||||||
"""Extract the mac address from the device info."""
|
"""Extract the mac address from the device info."""
|
||||||
@ -129,8 +131,8 @@ class SamsungTVBridge(ABC):
|
|||||||
"""Tells if the TV is on."""
|
"""Tells if the TV is on."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def async_send_key(self, key: str) -> None:
|
async def async_send_keys(self, keys: list[str]) -> None:
|
||||||
"""Send a key to the tv and handles exceptions."""
|
"""Send a list of keys to the tv."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def async_close_remote(self) -> None:
|
async def async_close_remote(self) -> None:
|
||||||
@ -238,12 +240,18 @@ class SamsungTVLegacyBridge(SamsungTVBridge):
|
|||||||
pass
|
pass
|
||||||
return self._remote
|
return self._remote
|
||||||
|
|
||||||
async def async_send_key(self, key: str) -> None:
|
async def async_send_keys(self, keys: list[str]) -> None:
|
||||||
"""Send the key using legacy protocol."""
|
"""Send a list of keys using legacy protocol."""
|
||||||
await self.hass.async_add_executor_job(self._send_key, key)
|
first_key = True
|
||||||
|
for key in keys:
|
||||||
|
if first_key:
|
||||||
|
first_key = False
|
||||||
|
else:
|
||||||
|
await asyncio.sleep(KEY_PRESS_TIMEOUT)
|
||||||
|
await self.hass.async_add_executor_job(self._send_key, key)
|
||||||
|
|
||||||
def _send_key(self, key: str) -> None:
|
def _send_key(self, key: str) -> None:
|
||||||
"""Send the key using legacy protocol."""
|
"""Send a key using legacy protocol."""
|
||||||
try:
|
try:
|
||||||
# recreate connection if connection was dead
|
# recreate connection if connection was dead
|
||||||
retry_count = 1
|
retry_count = 1
|
||||||
@ -391,15 +399,18 @@ class SamsungTVWSBridge(SamsungTVBridge):
|
|||||||
|
|
||||||
async def async_launch_app(self, app_id: str) -> None:
|
async def async_launch_app(self, app_id: str) -> None:
|
||||||
"""Send the launch_app command using websocket protocol."""
|
"""Send the launch_app command using websocket protocol."""
|
||||||
await self._async_send_command(ChannelEmitCommand.launch_app(app_id))
|
await self._async_send_commands([ChannelEmitCommand.launch_app(app_id)])
|
||||||
|
|
||||||
async def async_send_key(self, key: str) -> None:
|
async def async_send_keys(self, keys: list[str]) -> None:
|
||||||
"""Send the key using websocket protocol."""
|
"""Send a list of keys using websocket protocol."""
|
||||||
if key == "KEY_POWEROFF":
|
commands: list[SamsungTVCommand] = []
|
||||||
key = "KEY_POWER"
|
for key in keys:
|
||||||
await self._async_send_command(SendRemoteKey.click(key))
|
if key == "KEY_POWEROFF":
|
||||||
|
key = "KEY_POWER"
|
||||||
|
commands.append(SendRemoteKey.click(key))
|
||||||
|
await self._async_send_commands(commands)
|
||||||
|
|
||||||
async def _async_send_command(self, command: SamsungTVCommand) -> None:
|
async def _async_send_commands(self, commands: list[SamsungTVCommand]) -> None:
|
||||||
"""Send the commands using websocket protocol."""
|
"""Send the commands using websocket protocol."""
|
||||||
try:
|
try:
|
||||||
# recreate connection if connection was dead
|
# recreate connection if connection was dead
|
||||||
@ -407,7 +418,8 @@ class SamsungTVWSBridge(SamsungTVBridge):
|
|||||||
for _ in range(retry_count + 1):
|
for _ in range(retry_count + 1):
|
||||||
try:
|
try:
|
||||||
if remote := await self._async_get_remote():
|
if remote := await self._async_get_remote():
|
||||||
await remote.send_command(command)
|
for command in commands:
|
||||||
|
await remote.send_command(command)
|
||||||
break
|
break
|
||||||
except (
|
except (
|
||||||
BrokenPipeError,
|
BrokenPipeError,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
"""Support for interface with an Samsung TV."""
|
"""Support for interface with an Samsung TV."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -47,7 +46,6 @@ from .const import (
|
|||||||
LOGGER,
|
LOGGER,
|
||||||
)
|
)
|
||||||
|
|
||||||
KEY_PRESS_TIMEOUT = 1.2
|
|
||||||
SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"}
|
SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"}
|
||||||
|
|
||||||
SUPPORT_SAMSUNGTV = (
|
SUPPORT_SAMSUNGTV = (
|
||||||
@ -181,12 +179,13 @@ class SamsungTVDevice(MediaPlayerEntity):
|
|||||||
assert isinstance(self._bridge, SamsungTVWSBridge)
|
assert isinstance(self._bridge, SamsungTVWSBridge)
|
||||||
await self._bridge.async_launch_app(app_id)
|
await self._bridge.async_launch_app(app_id)
|
||||||
|
|
||||||
async def _async_send_key(self, key: str) -> None:
|
async def _async_send_keys(self, keys: list[str]) -> None:
|
||||||
"""Send a key to the tv and handles exceptions."""
|
"""Send a key to the tv and handles exceptions."""
|
||||||
if self._power_off_in_progress() and key != "KEY_POWEROFF":
|
assert keys
|
||||||
LOGGER.info("TV is powering off, not sending key: %s", key)
|
if self._power_off_in_progress() and keys[0] != "KEY_POWEROFF":
|
||||||
|
LOGGER.info("TV is powering off, not sending keys: %s", keys)
|
||||||
return
|
return
|
||||||
await self._bridge.async_send_key(key)
|
await self._bridge.async_send_keys(keys)
|
||||||
|
|
||||||
def _power_off_in_progress(self) -> bool:
|
def _power_off_in_progress(self) -> bool:
|
||||||
return (
|
return (
|
||||||
@ -210,21 +209,21 @@ class SamsungTVDevice(MediaPlayerEntity):
|
|||||||
"""Turn off media player."""
|
"""Turn off media player."""
|
||||||
self._end_of_power_off = dt_util.utcnow() + SCAN_INTERVAL_PLUS_OFF_TIME
|
self._end_of_power_off = dt_util.utcnow() + SCAN_INTERVAL_PLUS_OFF_TIME
|
||||||
|
|
||||||
await self._async_send_key("KEY_POWEROFF")
|
await self._async_send_keys(["KEY_POWEROFF"])
|
||||||
# Force closing of remote session to provide instant UI feedback
|
# Force closing of remote session to provide instant UI feedback
|
||||||
await self._bridge.async_close_remote()
|
await self._bridge.async_close_remote()
|
||||||
|
|
||||||
async def async_volume_up(self) -> None:
|
async def async_volume_up(self) -> None:
|
||||||
"""Volume up the media player."""
|
"""Volume up the media player."""
|
||||||
await self._async_send_key("KEY_VOLUP")
|
await self._async_send_keys(["KEY_VOLUP"])
|
||||||
|
|
||||||
async def async_volume_down(self) -> None:
|
async def async_volume_down(self) -> None:
|
||||||
"""Volume down media player."""
|
"""Volume down media player."""
|
||||||
await self._async_send_key("KEY_VOLDOWN")
|
await self._async_send_keys(["KEY_VOLDOWN"])
|
||||||
|
|
||||||
async def async_mute_volume(self, mute: bool) -> None:
|
async def async_mute_volume(self, mute: bool) -> None:
|
||||||
"""Send mute command."""
|
"""Send mute command."""
|
||||||
await self._async_send_key("KEY_MUTE")
|
await self._async_send_keys(["KEY_MUTE"])
|
||||||
|
|
||||||
async def async_media_play_pause(self) -> None:
|
async def async_media_play_pause(self) -> None:
|
||||||
"""Simulate play pause media player."""
|
"""Simulate play pause media player."""
|
||||||
@ -236,20 +235,20 @@ class SamsungTVDevice(MediaPlayerEntity):
|
|||||||
async def async_media_play(self) -> None:
|
async def async_media_play(self) -> None:
|
||||||
"""Send play command."""
|
"""Send play command."""
|
||||||
self._playing = True
|
self._playing = True
|
||||||
await self._async_send_key("KEY_PLAY")
|
await self._async_send_keys(["KEY_PLAY"])
|
||||||
|
|
||||||
async def async_media_pause(self) -> None:
|
async def async_media_pause(self) -> None:
|
||||||
"""Send media pause command to media player."""
|
"""Send media pause command to media player."""
|
||||||
self._playing = False
|
self._playing = False
|
||||||
await self._async_send_key("KEY_PAUSE")
|
await self._async_send_keys(["KEY_PAUSE"])
|
||||||
|
|
||||||
async def async_media_next_track(self) -> None:
|
async def async_media_next_track(self) -> None:
|
||||||
"""Send next track command."""
|
"""Send next track command."""
|
||||||
await self._async_send_key("KEY_CHUP")
|
await self._async_send_keys(["KEY_CHUP"])
|
||||||
|
|
||||||
async def async_media_previous_track(self) -> None:
|
async def async_media_previous_track(self) -> None:
|
||||||
"""Send the previous track command."""
|
"""Send the previous track command."""
|
||||||
await self._async_send_key("KEY_CHDOWN")
|
await self._async_send_keys(["KEY_CHDOWN"])
|
||||||
|
|
||||||
async def async_play_media(
|
async def async_play_media(
|
||||||
self, media_type: str, media_id: str, **kwargs: Any
|
self, media_type: str, media_id: str, **kwargs: Any
|
||||||
@ -270,10 +269,9 @@ class SamsungTVDevice(MediaPlayerEntity):
|
|||||||
LOGGER.error("Media ID must be positive integer")
|
LOGGER.error("Media ID must be positive integer")
|
||||||
return
|
return
|
||||||
|
|
||||||
for digit in media_id:
|
await self._async_send_keys(
|
||||||
await self._async_send_key(f"KEY_{digit}")
|
keys=[f"KEY_{digit}" for digit in media_id] + ["KEY_ENTER"]
|
||||||
await asyncio.sleep(KEY_PRESS_TIMEOUT)
|
)
|
||||||
await self._async_send_key("KEY_ENTER")
|
|
||||||
|
|
||||||
def _wake_on_lan(self) -> None:
|
def _wake_on_lan(self) -> None:
|
||||||
"""Wake the device via wake on lan."""
|
"""Wake the device via wake on lan."""
|
||||||
@ -296,7 +294,7 @@ class SamsungTVDevice(MediaPlayerEntity):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if source in SOURCES:
|
if source in SOURCES:
|
||||||
await self._async_send_key(SOURCES[source])
|
await self._async_send_keys([SOURCES[source]])
|
||||||
return
|
return
|
||||||
|
|
||||||
LOGGER.error("Unsupported source")
|
LOGGER.error("Unsupported source")
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
"""Tests for samsungtv component."""
|
"""Tests for samsungtv component."""
|
||||||
import asyncio
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
@ -650,7 +649,7 @@ async def test_turn_off_websocket(
|
|||||||
assert await hass.services.async_call(
|
assert await hass.services.async_call(
|
||||||
DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True
|
DOMAIN, SERVICE_VOLUME_UP, {ATTR_ENTITY_ID: ENTITY_ID}, True
|
||||||
)
|
)
|
||||||
assert "TV is powering off, not sending key: KEY_VOLUP" in caplog.text
|
assert "TV is powering off, not sending keys: ['KEY_VOLUP']" in caplog.text
|
||||||
assert await hass.services.async_call(
|
assert await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_SELECT_SOURCE,
|
SERVICE_SELECT_SOURCE,
|
||||||
@ -853,15 +852,8 @@ async def test_turn_on_without_turnon(hass: HomeAssistant, remote: Mock) -> None
|
|||||||
|
|
||||||
async def test_play_media(hass: HomeAssistant, remote: Mock) -> None:
|
async def test_play_media(hass: HomeAssistant, remote: Mock) -> None:
|
||||||
"""Test for play_media."""
|
"""Test for play_media."""
|
||||||
asyncio_sleep = asyncio.sleep
|
|
||||||
sleeps = []
|
|
||||||
|
|
||||||
async def sleep(duration):
|
|
||||||
sleeps.append(duration)
|
|
||||||
await asyncio_sleep(0)
|
|
||||||
|
|
||||||
await setup_samsungtv(hass, MOCK_CONFIG)
|
await setup_samsungtv(hass, MOCK_CONFIG)
|
||||||
with patch("asyncio.sleep", new=sleep):
|
with patch("homeassistant.components.samsungtv.bridge.asyncio.sleep") as sleep:
|
||||||
assert await hass.services.async_call(
|
assert await hass.services.async_call(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_PLAY_MEDIA,
|
SERVICE_PLAY_MEDIA,
|
||||||
@ -872,17 +864,17 @@ async def test_play_media(hass: HomeAssistant, remote: Mock) -> None:
|
|||||||
},
|
},
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
# keys and update called
|
# keys and update called
|
||||||
assert remote.control.call_count == 4
|
assert remote.control.call_count == 4
|
||||||
assert remote.control.call_args_list == [
|
assert remote.control.call_args_list == [
|
||||||
call("KEY_5"),
|
call("KEY_5"),
|
||||||
call("KEY_7"),
|
call("KEY_7"),
|
||||||
call("KEY_6"),
|
call("KEY_6"),
|
||||||
call("KEY_ENTER"),
|
call("KEY_ENTER"),
|
||||||
]
|
]
|
||||||
assert remote.close.call_count == 1
|
assert remote.close.call_count == 1
|
||||||
assert remote.close.call_args_list == [call()]
|
assert remote.close.call_args_list == [call()]
|
||||||
assert len(sleeps) == 3
|
assert sleep.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
async def test_play_media_invalid_type(hass: HomeAssistant) -> None:
|
async def test_play_media_invalid_type(hass: HomeAssistant) -> None:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user