Migrate BraviaTV to new async backend (#75727)

This commit is contained in:
Artem Draft 2022-08-10 14:11:49 +03:00 committed by GitHub
parent 4a938ec33e
commit 19295d33ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 385 additions and 363 deletions

View file

@ -139,6 +139,7 @@ omit =
homeassistant/components/bosch_shc/switch.py homeassistant/components/bosch_shc/switch.py
homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/__init__.py
homeassistant/components/braviatv/const.py homeassistant/components/braviatv/const.py
homeassistant/components/braviatv/coordinator.py
homeassistant/components/braviatv/entity.py homeassistant/components/braviatv/entity.py
homeassistant/components/braviatv/media_player.py homeassistant/components/braviatv/media_player.py
homeassistant/components/braviatv/remote.py homeassistant/components/braviatv/remote.py

View file

@ -1,27 +1,20 @@
"""The Bravia TV component.""" """The Bravia TV integration."""
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Iterable
from datetime import timedelta
import logging
from typing import Final from typing import Final
from bravia_tv import BraviaRC from aiohttp import CookieJar
from bravia_tv.braviarc import NoIPControl from pybravia import BraviaTV
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN, Platform from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import CLIENTID_PREFIX, CONF_IGNORED_SOURCES, DOMAIN, NICKNAME from .const import CONF_IGNORED_SOURCES, DOMAIN
from .coordinator import BraviaTVCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: Final[list[Platform]] = [Platform.MEDIA_PLAYER, Platform.REMOTE] PLATFORMS: Final[list[Platform]] = [Platform.MEDIA_PLAYER, Platform.REMOTE]
SCAN_INTERVAL: Final = timedelta(seconds=10)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
@ -31,7 +24,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
pin = config_entry.data[CONF_PIN] pin = config_entry.data[CONF_PIN]
ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES, []) ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES, [])
coordinator = BraviaTVCoordinator(hass, host, mac, pin, ignored_sources) session = async_create_clientsession(
hass, cookie_jar=CookieJar(unsafe=True, quote_cookie=False)
)
client = BraviaTV(host, mac, session=session)
coordinator = BraviaTVCoordinator(hass, client, pin, ignored_sources)
config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
@ -59,229 +56,3 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Handle options update.""" """Handle options update."""
await hass.config_entries.async_reload(config_entry.entry_id) await hass.config_entries.async_reload(config_entry.entry_id)
class BraviaTVCoordinator(DataUpdateCoordinator[None]):
"""Representation of a Bravia TV Coordinator.
An instance is used per device to share the same power state between
several platforms.
"""
def __init__(
self,
hass: HomeAssistant,
host: str,
mac: str,
pin: str,
ignored_sources: list[str],
) -> None:
"""Initialize Bravia TV Client."""
self.braviarc = BraviaRC(host, mac)
self.pin = pin
self.ignored_sources = ignored_sources
self.muted: bool = False
self.channel_name: str | None = None
self.media_title: str | None = None
self.source: str | None = None
self.source_list: list[str] = []
self.original_content_list: list[str] = []
self.content_mapping: dict[str, str] = {}
self.duration: int | None = None
self.content_uri: str | None = None
self.program_media_type: str | None = None
self.audio_output: str | None = None
self.min_volume: int | None = None
self.max_volume: int | None = None
self.volume_level: float | None = None
self.is_on = False
# Assume that the TV is in Play mode
self.playing = True
self.state_lock = asyncio.Lock()
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
request_refresh_debouncer=Debouncer(
hass, _LOGGER, cooldown=1.0, immediate=False
),
)
def _send_command(self, command: Iterable[str], repeats: int = 1) -> None:
"""Send a command to the TV."""
for _ in range(repeats):
for cmd in command:
self.braviarc.send_command(cmd)
def _get_source(self) -> str | None:
"""Return the name of the source."""
for key, value in self.content_mapping.items():
if value == self.content_uri:
return key
return None
def _refresh_volume(self) -> bool:
"""Refresh volume information."""
volume_info = self.braviarc.get_volume_info(self.audio_output)
if volume_info is not None:
volume = volume_info.get("volume")
self.volume_level = volume / 100 if volume is not None else None
self.audio_output = volume_info.get("target")
self.min_volume = volume_info.get("minVolume")
self.max_volume = volume_info.get("maxVolume")
self.muted = volume_info.get("mute", False)
return True
return False
def _refresh_channels(self) -> bool:
"""Refresh source and channels list."""
if not self.source_list:
self.content_mapping = self.braviarc.load_source_list()
self.source_list = []
if not self.content_mapping:
return False
for key in self.content_mapping:
if key not in self.ignored_sources:
self.source_list.append(key)
return True
def _refresh_playing_info(self) -> None:
"""Refresh playing information."""
playing_info = self.braviarc.get_playing_info()
program_name = playing_info.get("programTitle")
self.channel_name = playing_info.get("title")
self.program_media_type = playing_info.get("programMediaType")
self.content_uri = playing_info.get("uri")
self.source = self._get_source()
self.duration = playing_info.get("durationSec")
if not playing_info:
self.channel_name = "App"
if self.channel_name is not None:
self.media_title = self.channel_name
if program_name is not None:
self.media_title = f"{self.media_title}: {program_name}"
else:
self.media_title = None
def _update_tv_data(self) -> None:
"""Connect and update TV info."""
power_status = self.braviarc.get_power_status()
if power_status != "off":
connected = self.braviarc.is_connected()
if not connected:
try:
connected = self.braviarc.connect(
self.pin, CLIENTID_PREFIX, NICKNAME
)
except NoIPControl:
_LOGGER.error("IP Control is disabled in the TV settings")
if not connected:
power_status = "off"
if power_status == "active":
self.is_on = True
if self._refresh_volume() and self._refresh_channels():
self._refresh_playing_info()
return
self.is_on = False
async def _async_update_data(self) -> None:
"""Fetch the latest data."""
if self.state_lock.locked():
return
await self.hass.async_add_executor_job(self._update_tv_data)
async def async_turn_on(self) -> None:
"""Turn the device on."""
async with self.state_lock:
await self.hass.async_add_executor_job(self.braviarc.turn_on)
await self.async_request_refresh()
async def async_turn_off(self) -> None:
"""Turn off device."""
async with self.state_lock:
await self.hass.async_add_executor_job(self.braviarc.turn_off)
await self.async_request_refresh()
async def async_set_volume_level(self, volume: float) -> None:
"""Set volume level, range 0..1."""
async with self.state_lock:
await self.hass.async_add_executor_job(
self.braviarc.set_volume_level, volume, self.audio_output
)
await self.async_request_refresh()
async def async_volume_up(self) -> None:
"""Send volume up command to device."""
async with self.state_lock:
await self.hass.async_add_executor_job(
self.braviarc.volume_up, self.audio_output
)
await self.async_request_refresh()
async def async_volume_down(self) -> None:
"""Send volume down command to device."""
async with self.state_lock:
await self.hass.async_add_executor_job(
self.braviarc.volume_down, self.audio_output
)
await self.async_request_refresh()
async def async_volume_mute(self, mute: bool) -> None:
"""Send mute command to device."""
async with self.state_lock:
await self.hass.async_add_executor_job(self.braviarc.mute_volume, mute)
await self.async_request_refresh()
async def async_media_play(self) -> None:
"""Send play command to device."""
async with self.state_lock:
await self.hass.async_add_executor_job(self.braviarc.media_play)
self.playing = True
await self.async_request_refresh()
async def async_media_pause(self) -> None:
"""Send pause command to device."""
async with self.state_lock:
await self.hass.async_add_executor_job(self.braviarc.media_pause)
self.playing = False
await self.async_request_refresh()
async def async_media_stop(self) -> None:
"""Send stop command to device."""
async with self.state_lock:
await self.hass.async_add_executor_job(self.braviarc.media_stop)
self.playing = False
await self.async_request_refresh()
async def async_media_next_track(self) -> None:
"""Send next track command."""
async with self.state_lock:
await self.hass.async_add_executor_job(self.braviarc.media_next_track)
await self.async_request_refresh()
async def async_media_previous_track(self) -> None:
"""Send previous track command."""
async with self.state_lock:
await self.hass.async_add_executor_job(self.braviarc.media_previous_track)
await self.async_request_refresh()
async def async_select_source(self, source: str) -> None:
"""Set the input source."""
if source in self.content_mapping:
uri = self.content_mapping[source]
async with self.state_lock:
await self.hass.async_add_executor_job(self.braviarc.play_content, uri)
await self.async_request_refresh()
async def async_send_command(self, command: Iterable[str], repeats: int) -> None:
"""Send command to device."""
async with self.state_lock:
await self.hass.async_add_executor_job(self._send_command, command, repeats)
await self.async_request_refresh()

View file

@ -1,4 +1,4 @@
"""Adds config flow for Bravia TV integration.""" """Config flow to configure the Bravia TV integration."""
from __future__ import annotations from __future__ import annotations
from contextlib import suppress from contextlib import suppress
@ -6,17 +6,19 @@ import ipaddress
import re import re
from typing import Any from typing import Any
from bravia_tv import BraviaRC from aiohttp import CookieJar
from bravia_tv.braviarc import NoIPControl from pybravia import BraviaTV, BraviaTVError, BraviaTVNotSupported
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, exceptions from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_create_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from . import BraviaTVCoordinator
from .const import ( from .const import (
ATTR_CID, ATTR_CID,
ATTR_MAC, ATTR_MAC,
@ -42,35 +44,11 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
client: BraviaTV
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize.""" """Initialize config flow."""
self.braviarc: BraviaRC | None = None self.device_config: dict[str, Any] = {}
self.host: str | None = None
self.title = ""
self.mac: str | None = None
async def init_device(self, pin: str) -> None:
"""Initialize Bravia TV device."""
assert self.braviarc is not None
await self.hass.async_add_executor_job(
self.braviarc.connect, pin, CLIENTID_PREFIX, NICKNAME
)
connected = await self.hass.async_add_executor_job(self.braviarc.is_connected)
if not connected:
raise CannotConnect()
system_info = await self.hass.async_add_executor_job(
self.braviarc.get_system_info
)
if not system_info:
raise ModelNotSupported()
await self.async_set_unique_id(system_info[ATTR_CID].lower())
self._abort_if_unique_id_configured()
self.title = system_info[ATTR_MODEL]
self.mac = system_info[ATTR_MAC]
@staticmethod @staticmethod
@callback @callback
@ -78,6 +56,24 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Bravia TV options callback.""" """Bravia TV options callback."""
return BraviaTVOptionsFlowHandler(config_entry) return BraviaTVOptionsFlowHandler(config_entry)
async def async_init_device(self) -> FlowResult:
"""Initialize and create Bravia TV device from config."""
pin = self.device_config[CONF_PIN]
await self.client.connect(pin=pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME)
await self.client.set_wol_mode(True)
system_info = await self.client.get_system_info()
cid = system_info[ATTR_CID].lower()
title = system_info[ATTR_MODEL]
self.device_config[CONF_MAC] = system_info[ATTR_MAC]
await self.async_set_unique_id(cid)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=title, data=self.device_config)
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:
@ -85,9 +81,14 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
if host_valid(user_input[CONF_HOST]): host = user_input[CONF_HOST]
self.host = user_input[CONF_HOST] if host_valid(host):
self.braviarc = BraviaRC(self.host) session = async_create_clientsession(
self.hass,
cookie_jar=CookieJar(unsafe=True, quote_cookie=False),
)
self.client = BraviaTV(host=host, session=session)
self.device_config[CONF_HOST] = host
return await self.async_step_authorize() return await self.async_step_authorize()
@ -106,23 +107,17 @@ class BraviaTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
self.device_config[CONF_PIN] = user_input[CONF_PIN]
try: try:
await self.init_device(user_input[CONF_PIN]) return await self.async_init_device()
except CannotConnect: except BraviaTVNotSupported:
errors["base"] = "cannot_connect"
except ModelNotSupported:
errors["base"] = "unsupported_model" errors["base"] = "unsupported_model"
else: except BraviaTVError:
user_input[CONF_HOST] = self.host errors["base"] = "cannot_connect"
user_input[CONF_MAC] = self.mac
return self.async_create_entry(title=self.title, data=user_input)
# Connecting with th PIN "0000" to start the pairing process on the TV.
try: try:
assert self.braviarc is not None await self.client.pair(CLIENTID_PREFIX, NICKNAME)
await self.hass.async_add_executor_job( except BraviaTVError:
self.braviarc.connect, "0000", CLIENTID_PREFIX, NICKNAME
)
except NoIPControl:
return self.async_abort(reason="no_ip_control") return self.async_abort(reason="no_ip_control")
return self.async_show_form( return self.async_show_form(
@ -138,26 +133,20 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config_entry: ConfigEntry) -> None: def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize Bravia TV options flow.""" """Initialize Bravia TV options flow."""
self.config_entry = config_entry self.config_entry = config_entry
self.pin = config_entry.data[CONF_PIN]
self.ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES) self.ignored_sources = config_entry.options.get(CONF_IGNORED_SOURCES)
self.source_list: dict[str, str] = {} self.source_list: list[str] = []
async def async_step_init( async def async_step_init(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> FlowResult: ) -> FlowResult:
"""Manage the options.""" """Manage the options."""
coordinator = self.hass.data[DOMAIN][self.config_entry.entry_id] coordinator: BraviaTVCoordinator = self.hass.data[DOMAIN][
braviarc = coordinator.braviarc self.config_entry.entry_id
connected = await self.hass.async_add_executor_job(braviarc.is_connected) ]
if not connected:
await self.hass.async_add_executor_job(
braviarc.connect, self.pin, CLIENTID_PREFIX, NICKNAME
)
content_mapping = await self.hass.async_add_executor_job( await coordinator.async_update_sources()
braviarc.load_source_list sources = coordinator.source_map.values()
) self.source_list = [item["title"] for item in sources]
self.source_list = {item: item for item in content_mapping}
return await self.async_step_user() return await self.async_step_user()
async def async_step_user( async def async_step_user(
@ -177,11 +166,3 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow):
} }
), ),
) )
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
class ModelNotSupported(exceptions.HomeAssistantError):
"""Error to indicate not supported model."""

View file

@ -10,7 +10,6 @@ ATTR_MODEL: Final = "model"
CONF_IGNORED_SOURCES: Final = "ignored_sources" CONF_IGNORED_SOURCES: Final = "ignored_sources"
BRAVIA_CONFIG_FILE: Final = "bravia.conf"
CLIENTID_PREFIX: Final = "HomeAssistant" CLIENTID_PREFIX: Final = "HomeAssistant"
DOMAIN: Final = "braviatv" DOMAIN: Final = "braviatv"
NICKNAME: Final = "Home Assistant" NICKNAME: Final = "Home Assistant"

View file

@ -0,0 +1,258 @@
"""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 typing import Any, Final, TypeVar
from pybravia import BraviaTV, BraviaTVError
from typing_extensions import Concatenate, ParamSpec
from homeassistant.components.media_player.const import (
MEDIA_TYPE_APP,
MEDIA_TYPE_CHANNEL,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CLIENTID_PREFIX, DOMAIN, NICKNAME
_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,
pin: str,
ignored_sources: list[str],
) -> None:
"""Initialize Bravia TV Client."""
self.client = client
self.pin = pin
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: str | 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
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:
await self.client.connect(
pin=self.pin, clientid=CLIENTID_PREFIX, nickname=NICKNAME
)
self.connected = True
power_status = await self.client.get_power_status()
self.is_on = power_status == "active"
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 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 = MEDIA_TYPE_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 = MEDIA_TYPE_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:
await self.client.send_command(cmd)

View file

@ -2,9 +2,9 @@
"domain": "braviatv", "domain": "braviatv",
"name": "Sony Bravia TV", "name": "Sony Bravia TV",
"documentation": "https://www.home-assistant.io/integrations/braviatv", "documentation": "https://www.home-assistant.io/integrations/braviatv",
"requirements": ["bravia-tv==1.0.11"], "requirements": ["pybravia==0.2.0"],
"codeowners": ["@bieniu", "@Drafteed"], "codeowners": ["@bieniu", "@Drafteed"],
"config_flow": true, "config_flow": true,
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["bravia_tv"] "loggers": ["pybravia"]
} }

View file

@ -1,4 +1,4 @@
"""Support for interface with a Bravia TV.""" """Media player support for Bravia TV integration."""
from __future__ import annotations from __future__ import annotations
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
@ -74,7 +74,7 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity):
@property @property
def is_volume_muted(self) -> bool: def is_volume_muted(self) -> bool:
"""Boolean if volume is currently muted.""" """Boolean if volume is currently muted."""
return self.coordinator.muted return self.coordinator.volume_muted
@property @property
def media_title(self) -> str | None: def media_title(self) -> str | None:
@ -84,12 +84,17 @@ class BraviaTVMediaPlayer(BraviaTVEntity, MediaPlayerEntity):
@property @property
def media_content_id(self) -> str | None: def media_content_id(self) -> str | None:
"""Content ID of current playing media.""" """Content ID of current playing media."""
return self.coordinator.channel_name return self.coordinator.media_content_id
@property
def media_content_type(self) -> str | None:
"""Content type of current playing media."""
return self.coordinator.media_content_type
@property @property
def media_duration(self) -> int | None: def media_duration(self) -> int | None:
"""Duration of current playing media in seconds.""" """Duration of current playing media in seconds."""
return self.coordinator.duration return self.coordinator.media_duration
async def async_turn_on(self) -> None: async def async_turn_on(self) -> None:
"""Turn the device on.""" """Turn the device on."""

View file

@ -436,9 +436,6 @@ boschshcpy==0.2.30
# homeassistant.components.route53 # homeassistant.components.route53
boto3==1.20.24 boto3==1.20.24
# homeassistant.components.braviatv
bravia-tv==1.0.11
# homeassistant.components.broadlink # homeassistant.components.broadlink
broadlink==0.18.2 broadlink==0.18.2
@ -1421,6 +1418,9 @@ pyblackbird==0.5
# homeassistant.components.neato # homeassistant.components.neato
pybotvac==0.0.23 pybotvac==0.0.23
# homeassistant.components.braviatv
pybravia==0.2.0
# homeassistant.components.nissan_leaf # homeassistant.components.nissan_leaf
pycarwings2==2.13 pycarwings2==2.13

View file

@ -343,9 +343,6 @@ bond-async==0.1.22
# homeassistant.components.bosch_shc # homeassistant.components.bosch_shc
boschshcpy==0.2.30 boschshcpy==0.2.30
# homeassistant.components.braviatv
bravia-tv==1.0.11
# homeassistant.components.broadlink # homeassistant.components.broadlink
broadlink==0.18.2 broadlink==0.18.2
@ -994,6 +991,9 @@ pyblackbird==0.5
# homeassistant.components.neato # homeassistant.components.neato
pybotvac==0.0.23 pybotvac==0.0.23
# homeassistant.components.braviatv
pybravia==0.2.0
# homeassistant.components.cloudflare # homeassistant.components.cloudflare
pycfdns==1.2.2 pycfdns==1.2.2

View file

@ -1,7 +1,7 @@
"""Define tests for the Bravia TV config flow.""" """Define tests for the Bravia TV config flow."""
from unittest.mock import patch from unittest.mock import patch
from bravia_tv.braviarc import NoIPControl from pybravia import BraviaTVConnectionError, BraviaTVNotSupported
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.components.braviatv.const import CONF_IGNORED_SOURCES, DOMAIN from homeassistant.components.braviatv.const import CONF_IGNORED_SOURCES, DOMAIN
@ -23,13 +23,13 @@ BRAVIA_SYSTEM_INFO = {
"cid": "very_unique_string", "cid": "very_unique_string",
} }
BRAVIA_SOURCE_LIST = { BRAVIA_SOURCES = [
"HDMI 1": "extInput:hdmi?port=1", {"title": "HDMI 1", "uri": "extInput:hdmi?port=1"},
"HDMI 2": "extInput:hdmi?port=2", {"title": "HDMI 2", "uri": "extInput:hdmi?port=2"},
"HDMI 3/ARC": "extInput:hdmi?port=3", {"title": "HDMI 3/ARC", "uri": "extInput:hdmi?port=3"},
"HDMI 4": "extInput:hdmi?port=4", {"title": "HDMI 4", "uri": "extInput:hdmi?port=4"},
"AV/Component": "extInput:component?port=1", {"title": "AV/Component", "uri": "extInput:component?port=1"},
} ]
async def test_show_form(hass): async def test_show_form(hass):
@ -53,9 +53,10 @@ async def test_user_invalid_host(hass):
async def test_authorize_cannot_connect(hass): async def test_authorize_cannot_connect(hass):
"""Test that errors are shown when cannot connect to host at the authorize step.""" """Test that errors are shown when cannot connect to host at the authorize step."""
with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( with patch(
"bravia_tv.BraviaRC.is_connected", return_value=False "pybravia.BraviaTV.connect",
): side_effect=BraviaTVConnectionError,
), patch("pybravia.BraviaTV.pair"):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"}
) )
@ -68,12 +69,14 @@ async def test_authorize_cannot_connect(hass):
async def test_authorize_model_unsupported(hass): async def test_authorize_model_unsupported(hass):
"""Test that errors are shown when the TV is not supported at the authorize step.""" """Test that errors are shown when the TV is not supported at the authorize step."""
with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( with patch(
"bravia_tv.BraviaRC.is_connected", return_value=True "pybravia.BraviaTV.connect",
), patch("bravia_tv.BraviaRC.get_system_info", return_value={}): side_effect=BraviaTVNotSupported,
), patch("pybravia.BraviaTV.pair"):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "10.10.10.12"} DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "10.10.10.12"}
) )
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={CONF_PIN: "1234"} result["flow_id"], user_input={CONF_PIN: "1234"}
) )
@ -83,7 +86,6 @@ async def test_authorize_model_unsupported(hass):
async def test_authorize_no_ip_control(hass): async def test_authorize_no_ip_control(hass):
"""Test that errors are shown when IP Control is disabled on the TV.""" """Test that errors are shown when IP Control is disabled on the TV."""
with patch("bravia_tv.BraviaRC.connect", side_effect=NoIPControl("No IP Control")):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"}
) )
@ -106,9 +108,12 @@ async def test_duplicate_error(hass):
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch(
"bravia_tv.BraviaRC.is_connected", return_value=True "pybravia.BraviaTV.set_wol_mode"
), patch("bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO): ), patch(
"pybravia.BraviaTV.get_system_info",
return_value=BRAVIA_SYSTEM_INFO,
):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"} DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: "bravia-host"}
@ -123,10 +128,11 @@ async def test_duplicate_error(hass):
async def test_create_entry(hass): async def test_create_entry(hass):
"""Test that the user step works.""" """Test that the user step works."""
with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch(
"bravia_tv.BraviaRC.is_connected", return_value=True "pybravia.BraviaTV.set_wol_mode"
), patch( ), patch(
"bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO "pybravia.BraviaTV.get_system_info",
return_value=BRAVIA_SYSTEM_INFO,
), patch( ), patch(
"homeassistant.components.braviatv.async_setup_entry", return_value=True "homeassistant.components.braviatv.async_setup_entry", return_value=True
): ):
@ -154,10 +160,11 @@ async def test_create_entry(hass):
async def test_create_entry_with_ipv6_address(hass): async def test_create_entry_with_ipv6_address(hass):
"""Test that the user step works with device IPv6 address.""" """Test that the user step works with device IPv6 address."""
with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( with patch("pybravia.BraviaTV.connect"), patch("pybravia.BraviaTV.pair"), patch(
"bravia_tv.BraviaRC.is_connected", return_value=True "pybravia.BraviaTV.set_wol_mode"
), patch( ), patch(
"bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO "pybravia.BraviaTV.get_system_info",
return_value=BRAVIA_SYSTEM_INFO,
), patch( ), patch(
"homeassistant.components.braviatv.async_setup_entry", return_value=True "homeassistant.components.braviatv.async_setup_entry", return_value=True
): ):
@ -199,19 +206,19 @@ async def test_options_flow(hass):
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
with patch("bravia_tv.BraviaRC.connect", return_value=True), patch( with patch("pybravia.BraviaTV.connect"), patch(
"bravia_tv.BraviaRC.is_connected", return_value=True "pybravia.BraviaTV.get_power_status",
), patch("bravia_tv.BraviaRC.get_power_status"), patch( return_value="active",
"bravia_tv.BraviaRC.get_system_info", return_value=BRAVIA_SYSTEM_INFO ), patch(
"pybravia.BraviaTV.get_external_status",
return_value=BRAVIA_SOURCES,
), patch(
"pybravia.BraviaTV.send_rest_req",
return_value={},
): ):
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
with patch("bravia_tv.BraviaRC.connect", return_value=True), patch(
"bravia_tv.BraviaRC.is_connected", return_value=False
), patch("bravia_tv.BraviaRC.get_power_status"), patch(
"bravia_tv.BraviaRC.load_source_list", return_value=BRAVIA_SOURCE_LIST
):
result = await hass.config_entries.options.async_init(config_entry.entry_id) result = await hass.config_entries.options.async_init(config_entry.entry_id)
assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["type"] == data_entry_flow.FlowResultType.FORM