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
4 changes: 2 additions & 2 deletions driver.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"driver_id": "androidtv",
"version": "0.7.2",
"version": "0.7.3",
"min_core_api": "0.20.0",
"name": { "en": "Android TV" },
"icon": "uc:integration",
Expand Down Expand Up @@ -41,5 +41,5 @@
}
]
},
"release_date": "2025-04-27"
"release_date": "2025-05-08"
}
8 changes: 8 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ class AtvDevice:
"""Enable External Metadata."""
use_chromecast: bool = False
"""Enable Chromecast features."""
use_chromecast_volume: bool = False
"""Enable volume driven by Chromecast protocol."""
volume_step: float = 10
"""Volume step (1 to 100)."""


class _EnhancedJSONEncoder(json.JSONEncoder):
Expand Down Expand Up @@ -133,6 +137,8 @@ def update(self, atv: AtvDevice) -> bool:
item.auth_error = atv.auth_error
item.use_external_metadata = atv.use_external_metadata
item.use_chromecast = atv.use_chromecast
item.use_chromecast_volume = atv.use_chromecast_volume
item.volume_step = atv.volume_step if atv.volume_step else 10
return self.store()
return False

Expand Down Expand Up @@ -236,6 +242,8 @@ def load(self) -> bool:
item.get("auth_error", False),
item.get("use_external_metadata", False),
item.get("use_chromecast", False),
item.get("use_chromecast_volume", False),
item.get("volume_step", 10),
)
self._config.append(atv)
return True
Expand Down
3 changes: 0 additions & 3 deletions src/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,6 @@ def __init__(self):
media_player.Features.SUBTITLE,
media_player.Features.RECORD,
media_player.Features.STOP,
media_player.Features.VOLUME,
media_player.Features.VOLUME_UP_DOWN,
media_player.Features.MUTE_TOGGLE,
],
[],
{},
Expand Down
58 changes: 57 additions & 1 deletion src/setup_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class SetupSteps(IntEnum):
_use_external_metadata: bool = False
_reconfigured_device: AtvDevice | None = None
_use_chromecast: bool = False
_use_chromecast_volume: bool = False
_volume_step: float = 10

# TODO #9 externalize language texts
_user_input_discovery = RequestUserInput(
Expand Down Expand Up @@ -297,9 +299,13 @@ async def handle_configuration_mode(
_setup_step = SetupSteps.RECONFIGURE
_reconfigured_device = selected_device
use_chromecast = selected_device.use_chromecast if selected_device.use_chromecast else False
use_chromecast_volume = (
selected_device.use_chromecast_volume if selected_device.use_chromecast_volume else False
)
use_external_metadata = (
selected_device.use_external_metadata if selected_device.use_external_metadata else False
)
volume_step = selected_device.volume_step if selected_device.volume_step else 10

return RequestUserInput(
{
Expand All @@ -316,6 +322,14 @@ async def handle_configuration_mode(
},
"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": {
Expand All @@ -325,6 +339,14 @@ async def handle_configuration_mode(
},
"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}},
},
],
)
case "reset":
Expand Down Expand Up @@ -437,6 +459,14 @@ async def _handle_discovery(msg: UserDataResponse) -> RequestUserInput | SetupEr
},
"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": {
Expand All @@ -446,6 +476,14 @@ async def _handle_discovery(msg: UserDataResponse) -> RequestUserInput | SetupEr
},
"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}},
},
],
)

Expand All @@ -461,12 +499,16 @@ async def handle_device_choice(msg: UserDataResponse) -> RequestUserInput | Setu
"""
global _pairing_android_tv
global _use_chromecast
global _use_chromecast_volume
global _setup_step
global _use_external_metadata
global _volume_step

choice = msg.input_values["choice"]
_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))
name = ""

for discovered_tv in _discovered_android_tvs:
Expand All @@ -484,6 +526,8 @@ async def handle_device_choice(msg: UserDataResponse) -> RequestUserInput | Setu
id="",
use_external_metadata=False,
use_chromecast=False,
use_chromecast_volume=_use_chromecast_volume,
volume_step=_volume_step,
),
)
_LOG.info("Chosen Android TV: %s. Start pairing process...", choice)
Expand Down Expand Up @@ -564,8 +608,10 @@ async def handle_user_data_pin(msg: UserDataResponse) -> SetupComplete | SetupEr
address=_pairing_android_tv.address,
use_external_metadata=_use_external_metadata,
use_chromecast=_use_chromecast,
use_chromecast_volume=_use_chromecast_volume,
manufacturer=device_info.get("manufacturer", ""),
model=device_info.get("model", ""),
volume_step=_volume_step,
)

config.devices.add_or_update(device) # triggers AndroidTv instance creation
Expand Down Expand Up @@ -595,15 +641,25 @@ async def _handle_device_reconfigure(
return SetupError()

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))

_LOG.debug("User has changed configuration")
_reconfigured_device.use_chromecast = use_chromecast
_reconfigured_device.use_chromecast_volume = use_chromecast_volume
_reconfigured_device.use_external_metadata = use_external_metadata
_reconfigured_device.volume_step = volume_step

config.devices.add_or_update(_reconfigured_device) # triggers ATV instance update
await asyncio.sleep(1)
_LOG.info("Setup successfully completed for %s", _reconfigured_device.name)
_LOG.info(
"Setup successfully completed for %s (chromecast %s, external metadata %s, volume step %s)",
_reconfigured_device.name,
_reconfigured_device.use_chromecast,
_reconfigured_device.use_external_metadata,
_reconfigured_device.volume_step,
)

return SetupComplete()

Expand Down
79 changes: 52 additions & 27 deletions src/tv.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,11 @@ def is_on(self) -> bool | None:
"""Whether the Android TV is on or off. Returns None if not connected."""
return self._atv.is_on

@property
def device_config(self) -> AtvDevice:
"""Return current device configuration."""
return self._device_config

@property
def media_title(self) -> str | None:
"""Return media title."""
Expand Down Expand Up @@ -961,46 +966,66 @@ async def media_seek(self, position: float) -> ucapi.StatusCodes:

async def volume_up(self) -> ucapi.StatusCodes:
"""Change volume up."""
if self._chromecast is None:
return ucapi.StatusCodes.NOT_IMPLEMENTED
try:
self._chromecast.volume_up()
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
if self.device_config.use_chromecast and self.device_config.use_chromecast_volume:
if self._chromecast is None:
return ucapi.StatusCodes.NOT_IMPLEMENTED
try:
_LOG.debug(
"[%s] Volume up : current %.2f + step %s",
self.log_id,
self._chromecast.status.volume_level,
self._device_config.volume_step / 100,
)
self._chromecast.volume_up(delta=float(self._device_config.volume_step / 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 await self.send_media_player_command(media_player.Commands.VOLUME_UP)

async def volume_down(self) -> ucapi.StatusCodes:
"""Change volume down."""
if self._chromecast is None:
return ucapi.StatusCodes.NOT_IMPLEMENTED
try:
self._chromecast.volume_down()
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
if self.device_config.use_chromecast and self.device_config.use_chromecast_volume:
if self._chromecast is None:
return ucapi.StatusCodes.NOT_IMPLEMENTED
try:
_LOG.debug(
"[%s] Volume down : current %.2f - step %s",
self.log_id,
self._chromecast.status.volume_level,
self._device_config.volume_step / 100,
)
self._chromecast.volume_down(delta=float(self._device_config.volume_step / 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 await self.send_media_player_command(media_player.Commands.VOLUME_DOWN)

async def volume_mute_toggle(self) -> ucapi.StatusCodes:
"""Mute toggle."""
if self._chromecast is None:
return ucapi.StatusCodes.NOT_IMPLEMENTED
try:
self._muted = not 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
if self.device_config.use_chromecast and self.device_config.use_chromecast_volume:
if self._chromecast is None:
return ucapi.StatusCodes.NOT_IMPLEMENTED
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 await self.send_media_player_command(media_player.Commands.MUTE_TOGGLE)

async def volume_set(self, volume: float | None) -> ucapi.StatusCodes:
async def volume_set(self, volume: int | None) -> ucapi.StatusCodes:
"""Set volume."""
if self._chromecast is None:
return ucapi.StatusCodes.NOT_IMPLEMENTED
if volume is None:
return ucapi.StatusCodes.BAD_REQUEST
try:
await self._chromecast.set_volume(volume / 100)
_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)
Expand Down