Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)."""


Expand Down
136 changes: 64 additions & 72 deletions src/setup_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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,
}
},
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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),
],
)

Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}},
}
45 changes: 21 additions & 24 deletions src/tv.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -999,34 +998,32 @@ 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)
self._chromecast.set_volume_muted(self._muted)
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)
self._chromecast.set_volume(float(volume) / 100)
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