From 3677a57e00044626d266e45f4d0fcbe0b13caac7 Mon Sep 17 00:00:00 2001 From: Markus Zehnder Date: Mon, 12 May 2025 22:07:56 +0200 Subject: [PATCH 1/4] refactor: fallback volume control, Cast command status codes For volume up, down and mute use normal Android TV volume control if Google Cast volume control is enabled but not available. Return SERVICE_UNAVAILABLE if Google Cast is required for a command, like seek, but not connected. Return SERVER_ERROR if a Google Cast command failed. --- CHANGELOG.md | 6 ++++++ src/tv.py | 45 +++++++++++++++++++++------------------------ 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c9e937..6ade5b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased _Changes in the next release_ +### Added +- Configurable volume step when using Google Cast volume control. Contributed by @albaintor, thanks! ([#72](https://github.com/unfoldedcircle/integration-androidtv/pull/71)) + +### Fixed +- Normal volume control with Android TV keycodes ([#72](https://github.com/unfoldedcircle/integration-androidtv/issues/72)). + --- ## v0.7.2 - 2025-04-27 diff --git a/src/tv.py b/src/tv.py index af46c7b..ab28acf 100644 --- a/src/tv.py +++ b/src/tv.py @@ -955,20 +955,20 @@ async def _handle_new_cast_status(self, status: CastStatus): self.events.emit(Events.UPDATE, self._identifier, update) async def media_seek(self, position: float) -> ucapi.StatusCodes: - """Seek the media at the given position.""" + """Seek the media at the given position using Google Cast.""" try: - if self._chromecast: - self._chromecast.media_controller.seek(position, timeout=CONNECTION_TIMEOUT) - return ucapi.StatusCodes.OK + if self._chromecast is None: + return ucapi.StatusCodes.SERVICE_UNAVAILABLE + self._chromecast.media_controller.seek(position, timeout=CONNECTION_TIMEOUT) + return ucapi.StatusCodes.OK except Exception as ex: _LOG.error("[%s] Chromecast error seeking command : %s", self.log_id, ex) - return ucapi.StatusCodes.BAD_REQUEST + return ucapi.StatusCodes.SERVER_ERROR async def volume_up(self) -> ucapi.StatusCodes: - """Change volume up.""" - if self.device_config.use_chromecast and self.device_config.use_chromecast_volume: - if self._chromecast is None: - return ucapi.StatusCodes.NOT_IMPLEMENTED + """Change volume up. Use Google Cast volume control if enabled, otherwise use Android TV volume control.""" + if self.device_config.use_chromecast and self.device_config.use_chromecast_volume and self._chromecast: + # if chromecast is not connected, use default volume control. Google cast sometimes disconnects! try: _LOG.debug( "[%s] Volume up : current %.2f + step %s", @@ -980,14 +980,13 @@ async def volume_up(self) -> ucapi.StatusCodes: return ucapi.StatusCodes.OK except PyChromecastError as ex: _LOG.error("[%s] Chromecast error sending command : %s", self.log_id, ex) - return ucapi.StatusCodes.BAD_REQUEST + return ucapi.StatusCodes.SERVER_ERROR return await self.send_media_player_command(media_player.Commands.VOLUME_UP) async def volume_down(self) -> ucapi.StatusCodes: - """Change volume down.""" - if self.device_config.use_chromecast and self.device_config.use_chromecast_volume: - if self._chromecast is None: - return ucapi.StatusCodes.NOT_IMPLEMENTED + """Change volume down. Use Google Cast volume control if enabled, otherwise use Android TV volume control.""" + if self.device_config.use_chromecast and self.device_config.use_chromecast_volume and self._chromecast: + # if chromecast is not connected, use default volume control. Google cast sometimes disconnects! try: _LOG.debug( "[%s] Volume down : current %.2f - step %s", @@ -999,14 +998,12 @@ async def volume_down(self) -> ucapi.StatusCodes: return ucapi.StatusCodes.OK except PyChromecastError as ex: _LOG.error("[%s] Chromecast error sending command : %s", self.log_id, ex) - return ucapi.StatusCodes.BAD_REQUEST + return ucapi.StatusCodes.SERVER_ERROR return await self.send_media_player_command(media_player.Commands.VOLUME_DOWN) async def volume_mute_toggle(self) -> ucapi.StatusCodes: - """Mute toggle.""" - if self.device_config.use_chromecast and self.device_config.use_chromecast_volume: - if self._chromecast is None: - return ucapi.StatusCodes.NOT_IMPLEMENTED + """Mute toggle. Use Google Cast volume control if enabled, otherwise use Android TV volume control.""" + if self.device_config.use_chromecast and self.device_config.use_chromecast_volume and self._chromecast: try: self._muted = not self._muted _LOG.debug("[%s] Mute toggle : %s", self.log_id, self._muted) @@ -1014,14 +1011,14 @@ async def volume_mute_toggle(self) -> ucapi.StatusCodes: return ucapi.StatusCodes.OK except PyChromecastError as ex: _LOG.error("[%s] Chromecast error sending command : %s", self.log_id, ex) - return ucapi.StatusCodes.BAD_REQUEST + return ucapi.StatusCodes.SERVER_ERROR return await self.send_media_player_command(media_player.Commands.MUTE_TOGGLE) async def volume_set(self, volume: int | None) -> ucapi.StatusCodes: - """Set volume.""" + """Set volume using Google Cast.""" if self._chromecast is None: - return ucapi.StatusCodes.NOT_IMPLEMENTED - if volume is None: + return ucapi.StatusCodes.SERVICE_UNAVAILABLE + if volume is None or volume < 0 or volume > 100: return ucapi.StatusCodes.BAD_REQUEST try: _LOG.debug("[%s] Set volume : %.2f", self.log_id, float(volume) / 100) @@ -1029,4 +1026,4 @@ async def volume_set(self, volume: int | None) -> ucapi.StatusCodes: return ucapi.StatusCodes.OK except PyChromecastError as ex: _LOG.error("[%s] Chromecast error sending command : %s", self.log_id, ex) - return ucapi.StatusCodes.BAD_REQUEST + return ucapi.StatusCodes.SERVER_ERROR From f63a3ba103b04a07cc0ddf4e2faf09cde9f4fc1b Mon Sep 17 00:00:00 2001 From: Markus Zehnder Date: Mon, 12 May 2025 22:12:57 +0200 Subject: [PATCH 2/4] refactor: de-duplicate setup-flow configuration options Only define configuration settings once and re-use for initial device setup and for device reconfiguration. Add missing German texts. --- src/config.py | 2 +- src/setup_flow.py | 130 +++++++++++++++++++++------------------------- 2 files changed, 60 insertions(+), 72 deletions(-) diff --git a/src/config.py b/src/config.py index 0397091..72ca8d4 100644 --- a/src/config.py +++ b/src/config.py @@ -40,7 +40,7 @@ class AtvDevice: """Enable Chromecast features.""" use_chromecast_volume: bool = False """Enable volume driven by Chromecast protocol.""" - volume_step: float = 10 + volume_step: int = 10 """Volume step (1 to 100).""" diff --git a/src/setup_flow.py b/src/setup_flow.py index e59fb26..fb92805 100644 --- a/src/setup_flow.py +++ b/src/setup_flow.py @@ -49,7 +49,7 @@ class SetupSteps(IntEnum): _reconfigured_device: AtvDevice | None = None _use_chromecast: bool = False _use_chromecast_volume: bool = False -_volume_step: float = 10 +_volume_step: int = 10 # TODO #9 externalize language texts _user_input_discovery = RequestUserInput( @@ -313,40 +313,10 @@ async def handle_configuration_mode( "fr": "Configurez votre Android TV", }, [ - { - "id": "chromecast", - "label": { - "en": "Preview feature: Enable Chromecast features", - "de": "Vorschaufunktion: Aktiviere Chromecast-Features", - "fr": "Fonctionnalité en aperçu: Activer les fonctionnalités de Chromecast", - }, - "field": {"checkbox": {"value": use_chromecast}}, - }, - { - "id": "chromecast_volume", - "label": { - "en": "Set volume through Chromecast", - "fr": "Régler le volume par Chromecast", - }, - "field": {"checkbox": {"value": use_chromecast_volume}}, - }, - { - "id": "external_metadata", - "label": { - "en": "Preview feature: Enable external Google Play metadata", - "de": "Vorschaufunktion: Aktiviere externe Google Play Metadaten", - "fr": "Fonctionnalité en aperçu: Activer les métadonnées externes de Google Play", - }, - "field": {"checkbox": {"value": use_external_metadata}}, - }, - { - "id": "volume_step", - "label": { - "en": "Volume step in percent (Chromecast only)", - "fr": "Pallier de volume en pourcentage (Chromecast uniquement)", - }, - "field": {"number": {"value": volume_step, "min": 1, "max": 50, "steps": 1, "decimals": 0}}, - }, + __cfg_use_chromecast(use_chromecast), + __cfg_chromecast_volume(use_chromecast_volume), + __cfg_volume_step(volume_step), + __cfg_external_metadata(use_external_metadata), ], ) case "reset": @@ -450,40 +420,10 @@ async def _handle_discovery(msg: UserDataResponse) -> RequestUserInput | SetupEr "fr": "Choisir votre Android TV", }, }, - { - "id": "chromecast", - "label": { - "en": "Preview feature: Enable Chromecast features", - "de": "Vorschaufunktion: Aktiviere Chromecast-Features", - "fr": "Fonctionnalité en aperçu: Activer les fonctionnalités de Chromecast", - }, - "field": {"checkbox": {"value": False}}, - }, - { - "id": "chromecast_volume", - "label": { - "en": "Set volume through Chromecast", - "fr": "Régler le volume par Chromecast", - }, - "field": {"checkbox": {"value": False}}, - }, - { - "id": "external_metadata", - "label": { - "en": "Preview feature: Enable external Google Play metadata", - "de": "Vorschaufunktion: Aktiviere externe Google Play Metadaten", - "fr": "Fonctionnalité en aperçu: Activer les métadonnées externes de Google Play", - }, - "field": {"checkbox": {"value": False}}, - }, - { - "id": "volume_step", - "label": { - "en": "Volume step in percent (Chromecast only)", - "fr": "Pallier de volume en pourcent (Chromecast uniquement)", - }, - "field": {"number": {"value": 10, "min": 1, "max": 50, "steps": 1, "decimals": 0}}, - }, + __cfg_use_chromecast(False), + __cfg_chromecast_volume(False), + __cfg_volume_step(10), + __cfg_external_metadata(False), ], ) @@ -508,7 +448,7 @@ async def handle_device_choice(msg: UserDataResponse) -> RequestUserInput | Setu _use_external_metadata = msg.input_values.get("external_metadata", "false") == "true" _use_chromecast = msg.input_values.get("chromecast", "false") == "true" _use_chromecast_volume = msg.input_values.get("chromecast_volume", "false") == "true" - _volume_step = float(msg.input_values.get("volume_step", 10)) + _volume_step = int(msg.input_values.get("volume_step", 10)) name = "" for discovered_tv in _discovered_android_tvs: @@ -643,7 +583,7 @@ async def _handle_device_reconfigure( use_chromecast = msg.input_values.get("chromecast", "false") == "true" use_chromecast_volume = msg.input_values.get("chromecast_volume", "false") == "true" use_external_metadata = msg.input_values.get("external_metadata", "false") == "true" - volume_step = float(msg.input_values.get("volume_step", 10)) + volume_step = int(msg.input_values.get("volume_step", 10)) _LOG.debug("User has changed configuration") _reconfigured_device.use_chromecast = use_chromecast @@ -674,3 +614,51 @@ def _setup_error_from_device_state(state: tv.DeviceState) -> SetupError: error_type = IntegrationSetupError.CONNECTION_REFUSED return SetupError(error_type=error_type) + + +def __cfg_use_chromecast(enabled: bool): + return { + "id": "chromecast", + "label": { + "en": "Preview feature: Enable Chromecast features", + "de": "Vorschaufunktion: Aktiviere Chromecast-Features", + "fr": "Fonctionnalité en aperçu: Activer les fonctionnalités de Chromecast", + }, + "field": {"checkbox": {"value": enabled}}, + } + + +def __cfg_chromecast_volume(enabled: bool): + return { + "id": "chromecast_volume", + "label": { + "en": "Preview feature: Set volume through Chromecast", + "de": "Vorschaufunktion: Lautstärkeregelung mittels Chromecast", + "fr": "Fonctionnalité en aperçu: Régler le volume par Chromecast", + }, + "field": {"checkbox": {"value": enabled}}, + } + + +def __cfg_volume_step(value: int): + return { + "id": "volume_step", + "label": { + "en": "Volume step in percent (Chromecast only)", + "de": "Lautstärkeregelung in Prozent (nur Chromecast)", + "fr": "Pallier de volume en pourcentage (Chromecast uniquement)", + }, + "field": {"number": {"value": value, "min": 1, "max": 50, "steps": 1, "decimals": 0}}, + } + + +def __cfg_external_metadata(enabled: bool): + return { + "id": "external_metadata", + "label": { + "en": "Preview feature: Enable external Google Play metadata", + "de": "Vorschaufunktion: Aktiviere externe Google Play Metadaten", + "fr": "Fonctionnalité en aperçu: Activer les métadonnées externes de Google Play", + }, + "field": {"checkbox": {"value": enabled}}, + } From 69abb14e4f051967567b47a7cc50c97a7623c76f Mon Sep 17 00:00:00 2001 From: Markus Zehnder Date: Tue, 13 May 2025 14:30:33 +0200 Subject: [PATCH 3/4] refactor: Pre-select configure action in setup flow if a device exists --- src/setup_flow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/setup_flow.py b/src/setup_flow.py index fb92805..c9cbe11 100644 --- a/src/setup_flow.py +++ b/src/setup_flow.py @@ -168,6 +168,7 @@ async def handle_driver_setup(msg: DriverSetupRequest) -> RequestUserInput | Set # TODO #9 externalize language texts # build user actions, based on available devices + selected_action_index = 0 dropdown_actions = [ { "id": "add", @@ -181,6 +182,8 @@ async def handle_driver_setup(msg: DriverSetupRequest) -> RequestUserInput | Set # add remove & reset actions if there's at least one configured device if dropdown_devices: + # pre-select configure action if at least one device exists + selected_action_index = 1 dropdown_actions.append( { "id": "configure", @@ -237,7 +240,7 @@ async def handle_driver_setup(msg: DriverSetupRequest) -> RequestUserInput | Set { "field": { "dropdown": { - "value": dropdown_actions[0]["id"], + "value": dropdown_actions[selected_action_index]["id"], "items": dropdown_actions, } }, From 2c6153e204e4e5ec84ba13c560893ced009e91a6 Mon Sep 17 00:00:00 2001 From: Markus Zehnder Date: Tue, 13 May 2025 14:33:25 +0200 Subject: [PATCH 4/4] chore: missing German text --- src/setup_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/setup_flow.py b/src/setup_flow.py index c9cbe11..7defe1c 100644 --- a/src/setup_flow.py +++ b/src/setup_flow.py @@ -313,6 +313,7 @@ async def handle_configuration_mode( return RequestUserInput( { "en": "Configure your Android TV", + "de": "Konfiguriere deinen Android TV", "fr": "Configurez votre Android TV", }, [