315 lines
11 KiB
Python
315 lines
11 KiB
Python
"""Update coordinator for Bravia TV integration."""
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Awaitable, Callable, Coroutine, Iterable
|
|
from datetime import timedelta
|
|
from functools import wraps
|
|
import logging
|
|
from types import MappingProxyType
|
|
from typing import Any, Final, TypeVar
|
|
|
|
from pybravia import (
|
|
BraviaTV,
|
|
BraviaTVAuthError,
|
|
BraviaTVConnectionError,
|
|
BraviaTVConnectionTimeout,
|
|
BraviaTVError,
|
|
BraviaTVNotFound,
|
|
BraviaTVTurnedOff,
|
|
)
|
|
from typing_extensions import Concatenate, ParamSpec
|
|
|
|
from homeassistant.components.media_player import MediaType
|
|
from homeassistant.const import CONF_PIN
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
|
from homeassistant.helpers.debounce import Debouncer
|
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
|
|
|
from .const import (
|
|
CONF_CLIENT_ID,
|
|
CONF_NICKNAME,
|
|
CONF_USE_PSK,
|
|
DOMAIN,
|
|
LEGACY_CLIENT_ID,
|
|
NICKNAME_PREFIX,
|
|
)
|
|
|
|
_BraviaTVCoordinatorT = TypeVar("_BraviaTVCoordinatorT", bound="BraviaTVCoordinator")
|
|
_P = ParamSpec("_P")
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
SCAN_INTERVAL: Final = timedelta(seconds=10)
|
|
|
|
|
|
def catch_braviatv_errors(
|
|
func: Callable[Concatenate[_BraviaTVCoordinatorT, _P], Awaitable[None]]
|
|
) -> Callable[Concatenate[_BraviaTVCoordinatorT, _P], Coroutine[Any, Any, None]]:
|
|
"""Catch BraviaTV errors."""
|
|
|
|
@wraps(func)
|
|
async def wrapper(
|
|
self: _BraviaTVCoordinatorT,
|
|
*args: _P.args,
|
|
**kwargs: _P.kwargs,
|
|
) -> None:
|
|
"""Catch BraviaTV errors and log message."""
|
|
try:
|
|
await func(self, *args, **kwargs)
|
|
except BraviaTVError as err:
|
|
_LOGGER.error("Command error: %s", err)
|
|
await self.async_request_refresh()
|
|
|
|
return wrapper
|
|
|
|
|
|
class BraviaTVCoordinator(DataUpdateCoordinator[None]):
|
|
"""Representation of a Bravia TV Coordinator."""
|
|
|
|
def __init__(
|
|
self,
|
|
hass: HomeAssistant,
|
|
client: BraviaTV,
|
|
config: MappingProxyType[str, Any],
|
|
ignored_sources: list[str],
|
|
) -> None:
|
|
"""Initialize Bravia TV Client."""
|
|
|
|
self.client = client
|
|
self.pin = config[CONF_PIN]
|
|
self.use_psk = config.get(CONF_USE_PSK, False)
|
|
self.client_id = config.get(CONF_CLIENT_ID, LEGACY_CLIENT_ID)
|
|
self.nickname = config.get(CONF_NICKNAME, NICKNAME_PREFIX)
|
|
self.ignored_sources = ignored_sources
|
|
self.source: str | None = None
|
|
self.source_list: list[str] = []
|
|
self.source_map: dict[str, dict] = {}
|
|
self.media_title: str | None = None
|
|
self.media_content_id: str | None = None
|
|
self.media_content_type: MediaType | None = None
|
|
self.media_uri: str | None = None
|
|
self.media_duration: int | None = None
|
|
self.volume_level: float | None = None
|
|
self.volume_target: str | None = None
|
|
self.volume_muted = False
|
|
self.is_on = False
|
|
self.is_channel = False
|
|
self.connected = False
|
|
# Assume that the TV is in Play mode
|
|
self.playing = True
|
|
self.skipped_updates = 0
|
|
|
|
super().__init__(
|
|
hass,
|
|
_LOGGER,
|
|
name=DOMAIN,
|
|
update_interval=SCAN_INTERVAL,
|
|
request_refresh_debouncer=Debouncer(
|
|
hass, _LOGGER, cooldown=1.0, immediate=False
|
|
),
|
|
)
|
|
|
|
def _sources_extend(self, sources: list[dict], source_type: str) -> None:
|
|
"""Extend source map and source list."""
|
|
for item in sources:
|
|
item["type"] = source_type
|
|
title = item.get("title")
|
|
uri = item.get("uri")
|
|
if not title or not uri:
|
|
continue
|
|
self.source_map[uri] = item
|
|
if title not in self.ignored_sources:
|
|
self.source_list.append(title)
|
|
|
|
async def _async_update_data(self) -> None:
|
|
"""Connect and fetch data."""
|
|
try:
|
|
if not self.connected:
|
|
if self.use_psk:
|
|
await self.client.connect(psk=self.pin)
|
|
else:
|
|
await self.client.connect(
|
|
pin=self.pin, clientid=self.client_id, nickname=self.nickname
|
|
)
|
|
self.connected = True
|
|
|
|
power_status = await self.client.get_power_status()
|
|
self.is_on = power_status == "active"
|
|
self.skipped_updates = 0
|
|
|
|
if self.is_on is False:
|
|
return
|
|
|
|
if not self.source_map:
|
|
await self.async_update_sources()
|
|
await self.async_update_volume()
|
|
await self.async_update_playing()
|
|
except BraviaTVNotFound as err:
|
|
if self.skipped_updates < 10:
|
|
self.connected = False
|
|
self.skipped_updates += 1
|
|
_LOGGER.debug("Update skipped, Bravia API service is reloading")
|
|
return
|
|
raise UpdateFailed("Error communicating with device") from err
|
|
except BraviaTVAuthError as err:
|
|
raise ConfigEntryAuthFailed from err
|
|
except (BraviaTVConnectionError, BraviaTVConnectionTimeout, BraviaTVTurnedOff):
|
|
self.is_on = False
|
|
self.connected = False
|
|
_LOGGER.debug("Update skipped, Bravia TV is off")
|
|
except BraviaTVError as err:
|
|
self.is_on = False
|
|
self.connected = False
|
|
raise UpdateFailed("Error communicating with device") from err
|
|
|
|
async def async_update_sources(self) -> None:
|
|
"""Update sources."""
|
|
self.source_list = []
|
|
self.source_map = {}
|
|
|
|
externals = await self.client.get_external_status()
|
|
self._sources_extend(externals, "input")
|
|
|
|
apps = await self.client.get_app_list()
|
|
self._sources_extend(apps, "app")
|
|
|
|
channels = await self.client.get_content_list_all("tv")
|
|
self._sources_extend(channels, "channel")
|
|
|
|
async def async_update_volume(self) -> None:
|
|
"""Update volume information."""
|
|
volume_info = await self.client.get_volume_info()
|
|
volume_level = volume_info.get("volume")
|
|
if volume_level is not None:
|
|
self.volume_level = volume_level / 100
|
|
self.volume_muted = volume_info.get("mute", False)
|
|
self.volume_target = volume_info.get("target")
|
|
|
|
async def async_update_playing(self) -> None:
|
|
"""Update current playing information."""
|
|
playing_info = await self.client.get_playing_info()
|
|
self.media_title = playing_info.get("title")
|
|
self.media_uri = playing_info.get("uri")
|
|
self.media_duration = playing_info.get("durationSec")
|
|
if program_title := playing_info.get("programTitle"):
|
|
self.media_title = f"{self.media_title}: {program_title}"
|
|
if self.media_uri:
|
|
source = self.source_map.get(self.media_uri, {})
|
|
self.source = source.get("title")
|
|
self.is_channel = self.media_uri[:2] == "tv"
|
|
if self.is_channel:
|
|
self.media_content_id = playing_info.get("dispNum")
|
|
self.media_content_type = MediaType.CHANNEL
|
|
else:
|
|
self.media_content_id = self.media_uri
|
|
self.media_content_type = None
|
|
else:
|
|
self.source = None
|
|
self.is_channel = False
|
|
self.media_content_id = None
|
|
self.media_content_type = None
|
|
if not playing_info:
|
|
self.media_title = "Smart TV"
|
|
self.media_content_type = MediaType.APP
|
|
|
|
@catch_braviatv_errors
|
|
async def async_turn_on(self) -> None:
|
|
"""Turn the device on."""
|
|
await self.client.turn_on()
|
|
|
|
@catch_braviatv_errors
|
|
async def async_turn_off(self) -> None:
|
|
"""Turn off device."""
|
|
await self.client.turn_off()
|
|
|
|
@catch_braviatv_errors
|
|
async def async_set_volume_level(self, volume: float) -> None:
|
|
"""Set volume level, range 0..1."""
|
|
await self.client.volume_level(round(volume * 100))
|
|
|
|
@catch_braviatv_errors
|
|
async def async_volume_up(self) -> None:
|
|
"""Send volume up command to device."""
|
|
await self.client.volume_up()
|
|
|
|
@catch_braviatv_errors
|
|
async def async_volume_down(self) -> None:
|
|
"""Send volume down command to device."""
|
|
await self.client.volume_down()
|
|
|
|
@catch_braviatv_errors
|
|
async def async_volume_mute(self, mute: bool) -> None:
|
|
"""Send mute command to device."""
|
|
await self.client.volume_mute()
|
|
|
|
@catch_braviatv_errors
|
|
async def async_media_play(self) -> None:
|
|
"""Send play command to device."""
|
|
await self.client.play()
|
|
self.playing = True
|
|
|
|
@catch_braviatv_errors
|
|
async def async_media_pause(self) -> None:
|
|
"""Send pause command to device."""
|
|
await self.client.pause()
|
|
self.playing = False
|
|
|
|
@catch_braviatv_errors
|
|
async def async_media_stop(self) -> None:
|
|
"""Send stop command to device."""
|
|
await self.client.stop()
|
|
|
|
@catch_braviatv_errors
|
|
async def async_media_next_track(self) -> None:
|
|
"""Send next track command."""
|
|
if self.is_channel:
|
|
await self.client.channel_up()
|
|
else:
|
|
await self.client.next_track()
|
|
|
|
@catch_braviatv_errors
|
|
async def async_media_previous_track(self) -> None:
|
|
"""Send previous track command."""
|
|
if self.is_channel:
|
|
await self.client.channel_down()
|
|
else:
|
|
await self.client.previous_track()
|
|
|
|
@catch_braviatv_errors
|
|
async def async_select_source(self, source: str) -> None:
|
|
"""Set the input source."""
|
|
for uri, item in self.source_map.items():
|
|
if item.get("title") == source:
|
|
if item.get("type") == "app":
|
|
await self.client.set_active_app(uri)
|
|
else:
|
|
await self.client.set_play_content(uri)
|
|
break
|
|
|
|
@catch_braviatv_errors
|
|
async def async_send_command(self, command: Iterable[str], repeats: int) -> None:
|
|
"""Send command to device."""
|
|
for _ in range(repeats):
|
|
for cmd in command:
|
|
response = await self.client.send_command(cmd)
|
|
if not response:
|
|
commands = await self.client.get_command_list()
|
|
commands_keys = ", ".join(commands.keys())
|
|
# Logging an error instead of raising a ValueError
|
|
# https://github.com/home-assistant/core/pull/77329#discussion_r955768245
|
|
_LOGGER.error(
|
|
"Unsupported command: %s, list of available commands: %s",
|
|
cmd,
|
|
commands_keys,
|
|
)
|
|
|
|
@catch_braviatv_errors
|
|
async def async_reboot_device(self) -> None:
|
|
"""Send command to reboot the device."""
|
|
await self.client.reboot()
|
|
|
|
@catch_braviatv_errors
|
|
async def async_terminate_apps(self) -> None:
|
|
"""Send command to terminate all applications."""
|
|
await self.client.terminate_apps()
|