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/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..7defe1c 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( @@ -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, } }, @@ -310,43 +313,14 @@ async def handle_configuration_mode( return RequestUserInput( { "en": "Configure your Android TV", + "de": "Konfiguriere deinen Android TV", "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 +424,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 +452,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 +587,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 +618,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}}, + } 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