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: 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()
class BraviaTVDevice(MediaPlayerDevice): """Representation of a Sony Bravia TV.""" def __init__(self, host, mac, name, pin): """Initialize the Sony Bravia device.""" self._pin = pin self._braviarc = BraviaRC(host, mac) self._name = name self._state = STATE_OFF self._muted = False self._program_name = None self._channel_name = None self._channel_number = None self._source = None self._source_list = [] self._original_content_list = [] self._content_mapping = {} self._duration = None self._content_uri = None self._id = None self._playing = False self._start_date_time = None self._program_media_type = None self._min_volume = None self._max_volume = None self._volume = None self._braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME) if self._braviarc.is_connected(): self.update() else: self._state = STATE_OFF def update(self): """Update TV info.""" if not self._braviarc.is_connected(): if self._braviarc.get_power_status() != "off": self._braviarc.connect(self._pin, CLIENTID_PREFIX, NICKNAME) if not self._braviarc.is_connected(): return # Retrieve the latest data. try: if self._state == STATE_ON: # refresh volume info: self._refresh_volume() self._refresh_channels() power_status = self._braviarc.get_power_status() if power_status == "active": self._state = STATE_ON playing_info = self._braviarc.get_playing_info() self._reset_playing_info() if playing_info is None or not playing_info: self._channel_name = "App" else: self._program_name = playing_info.get("programTitle") self._channel_name = playing_info.get("title") self._program_media_type = playing_info.get( "programMediaType") self._channel_number = playing_info.get("dispNum") self._source = playing_info.get("source") self._content_uri = playing_info.get("uri") self._duration = playing_info.get("durationSec") self._start_date_time = playing_info.get("startDateTime") else: self._state = STATE_OFF except Exception as exception_instance: # pylint: disable=broad-except _LOGGER.error(exception_instance) self._state = STATE_OFF def _reset_playing_info(self): self._program_name = None self._channel_name = None self._program_media_type = None self._channel_number = None self._source = None self._content_uri = None self._duration = None self._start_date_time = None def _refresh_volume(self): """Refresh volume information.""" volume_info = self._braviarc.get_volume_info() if volume_info is not None: self._volume = volume_info.get("volume") self._min_volume = volume_info.get("minVolume") self._max_volume = volume_info.get("maxVolume") self._muted = volume_info.get("mute") def _refresh_channels(self): if not self._source_list: self._content_mapping = self._braviarc.load_source_list() self._source_list = [] for key in self._content_mapping: self._source_list.append(key) @property def name(self): """Return the name of the device.""" return self._name @property def state(self): """Return the state of the device.""" return self._state @property def source(self): """Return the current input source.""" return self._source @property def source_list(self): """List of available input sources.""" return self._source_list @property def volume_level(self): """Volume level of the media player (0..1).""" if self._volume is not None: return self._volume / 100 return None @property def is_volume_muted(self): """Boolean if volume is currently muted.""" return self._muted @property def supported_features(self): """Flag media player features that are supported.""" return SUPPORT_BRAVIA @property def media_title(self): """Title of current playing media.""" return_value = None if self._channel_name is not None: return_value = self._channel_name if self._program_name is not None: return_value = f"{return_value}: {self._program_name}" return return_value @property def media_content_id(self): """Content ID of current playing media.""" return self._channel_name @property def media_duration(self): """Duration of current playing media in seconds.""" return self._duration def set_volume_level(self, volume): """Set volume level, range 0..1.""" self._braviarc.set_volume_level(volume) def turn_on(self): """Turn the media player on.""" self._braviarc.turn_on() def turn_off(self): """Turn off media player.""" self._braviarc.turn_off() def volume_up(self): """Volume up the media player.""" self._braviarc.volume_up() def volume_down(self): """Volume down media player.""" self._braviarc.volume_down() def mute_volume(self, mute): """Send mute command.""" self._braviarc.mute_volume(mute) def select_source(self, source): """Set the input source.""" if source in self._content_mapping: uri = self._content_mapping[source] self._braviarc.play_content(uri) def media_play_pause(self): """Simulate play pause media player.""" if self._playing: self.media_pause() else: self.media_play() def media_play(self): """Send play command.""" self._playing = True self._braviarc.media_play() def media_pause(self): """Send media pause command to media player.""" self._playing = False self._braviarc.media_pause() def media_stop(self): """Send media stop command to media player.""" self._playing = False self._braviarc.media_stop() def media_next_track(self): """Send next track command.""" self._braviarc.media_next_track() def media_previous_track(self): """Send the previous track command.""" self._braviarc.media_previous_track()