Skip to content

Commit 7f15814

Browse files
albaintorAlbaintor
andauthored
feat: configurable volume step and fixed replaced volume commands (#71)
* Added configurable volume step * Removed duplicate volume/mute features * Fixed volume control replaced by chromecast volume commands even if not activated * Added configurable option for volume through chromecast Fixes #72 --------- Co-authored-by: Albaintor <albaintor@github.com>
1 parent 6c5945a commit 7f15814

5 files changed

Lines changed: 119 additions & 33 deletions

File tree

driver.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"driver_id": "androidtv",
3-
"version": "0.7.2",
3+
"version": "0.7.3",
44
"min_core_api": "0.20.0",
55
"name": { "en": "Android TV" },
66
"icon": "uc:integration",
@@ -41,5 +41,5 @@
4141
}
4242
]
4343
},
44-
"release_date": "2025-04-27"
44+
"release_date": "2025-05-08"
4545
}

src/config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ class AtvDevice:
3838
"""Enable External Metadata."""
3939
use_chromecast: bool = False
4040
"""Enable Chromecast features."""
41+
use_chromecast_volume: bool = False
42+
"""Enable volume driven by Chromecast protocol."""
43+
volume_step: float = 10
44+
"""Volume step (1 to 100)."""
4145

4246

4347
class _EnhancedJSONEncoder(json.JSONEncoder):
@@ -133,6 +137,8 @@ def update(self, atv: AtvDevice) -> bool:
133137
item.auth_error = atv.auth_error
134138
item.use_external_metadata = atv.use_external_metadata
135139
item.use_chromecast = atv.use_chromecast
140+
item.use_chromecast_volume = atv.use_chromecast_volume
141+
item.volume_step = atv.volume_step if atv.volume_step else 10
136142
return self.store()
137143
return False
138144

@@ -236,6 +242,8 @@ def load(self) -> bool:
236242
item.get("auth_error", False),
237243
item.get("use_external_metadata", False),
238244
item.get("use_chromecast", False),
245+
item.get("use_chromecast_volume", False),
246+
item.get("volume_step", 10),
239247
)
240248
self._config.append(atv)
241249
return True

src/profiles.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,6 @@ def __init__(self):
189189
media_player.Features.SUBTITLE,
190190
media_player.Features.RECORD,
191191
media_player.Features.STOP,
192-
media_player.Features.VOLUME,
193-
media_player.Features.VOLUME_UP_DOWN,
194-
media_player.Features.MUTE_TOGGLE,
195192
],
196193
[],
197194
{},

src/setup_flow.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ class SetupSteps(IntEnum):
4848
_use_external_metadata: bool = False
4949
_reconfigured_device: AtvDevice | None = None
5050
_use_chromecast: bool = False
51+
_use_chromecast_volume: bool = False
52+
_volume_step: float = 10
5153

5254
# TODO #9 externalize language texts
5355
_user_input_discovery = RequestUserInput(
@@ -297,9 +299,13 @@ async def handle_configuration_mode(
297299
_setup_step = SetupSteps.RECONFIGURE
298300
_reconfigured_device = selected_device
299301
use_chromecast = selected_device.use_chromecast if selected_device.use_chromecast else False
302+
use_chromecast_volume = (
303+
selected_device.use_chromecast_volume if selected_device.use_chromecast_volume else False
304+
)
300305
use_external_metadata = (
301306
selected_device.use_external_metadata if selected_device.use_external_metadata else False
302307
)
308+
volume_step = selected_device.volume_step if selected_device.volume_step else 10
303309

304310
return RequestUserInput(
305311
{
@@ -316,6 +322,14 @@ async def handle_configuration_mode(
316322
},
317323
"field": {"checkbox": {"value": use_chromecast}},
318324
},
325+
{
326+
"id": "chromecast_volume",
327+
"label": {
328+
"en": "Set volume through Chromecast",
329+
"fr": "Régler le volume par Chromecast",
330+
},
331+
"field": {"checkbox": {"value": use_chromecast_volume}},
332+
},
319333
{
320334
"id": "external_metadata",
321335
"label": {
@@ -325,6 +339,14 @@ async def handle_configuration_mode(
325339
},
326340
"field": {"checkbox": {"value": use_external_metadata}},
327341
},
342+
{
343+
"id": "volume_step",
344+
"label": {
345+
"en": "Volume step in percent (Chromecast only)",
346+
"fr": "Pallier de volume en pourcentage (Chromecast uniquement)",
347+
},
348+
"field": {"number": {"value": volume_step, "min": 1, "max": 50, "steps": 1, "decimals": 0}},
349+
},
328350
],
329351
)
330352
case "reset":
@@ -437,6 +459,14 @@ async def _handle_discovery(msg: UserDataResponse) -> RequestUserInput | SetupEr
437459
},
438460
"field": {"checkbox": {"value": False}},
439461
},
462+
{
463+
"id": "chromecast_volume",
464+
"label": {
465+
"en": "Set volume through Chromecast",
466+
"fr": "Régler le volume par Chromecast",
467+
},
468+
"field": {"checkbox": {"value": False}},
469+
},
440470
{
441471
"id": "external_metadata",
442472
"label": {
@@ -446,6 +476,14 @@ async def _handle_discovery(msg: UserDataResponse) -> RequestUserInput | SetupEr
446476
},
447477
"field": {"checkbox": {"value": False}},
448478
},
479+
{
480+
"id": "volume_step",
481+
"label": {
482+
"en": "Volume step in percent (Chromecast only)",
483+
"fr": "Pallier de volume en pourcent (Chromecast uniquement)",
484+
},
485+
"field": {"number": {"value": 10, "min": 1, "max": 50, "steps": 1, "decimals": 0}},
486+
},
449487
],
450488
)
451489

@@ -461,12 +499,16 @@ async def handle_device_choice(msg: UserDataResponse) -> RequestUserInput | Setu
461499
"""
462500
global _pairing_android_tv
463501
global _use_chromecast
502+
global _use_chromecast_volume
464503
global _setup_step
465504
global _use_external_metadata
505+
global _volume_step
466506

467507
choice = msg.input_values["choice"]
468508
_use_external_metadata = msg.input_values.get("external_metadata", "false") == "true"
469509
_use_chromecast = msg.input_values.get("chromecast", "false") == "true"
510+
_use_chromecast_volume = msg.input_values.get("chromecast_volume", "false") == "true"
511+
_volume_step = float(msg.input_values.get("volume_step", 10))
470512
name = ""
471513

472514
for discovered_tv in _discovered_android_tvs:
@@ -484,6 +526,8 @@ async def handle_device_choice(msg: UserDataResponse) -> RequestUserInput | Setu
484526
id="",
485527
use_external_metadata=False,
486528
use_chromecast=False,
529+
use_chromecast_volume=_use_chromecast_volume,
530+
volume_step=_volume_step,
487531
),
488532
)
489533
_LOG.info("Chosen Android TV: %s. Start pairing process...", choice)
@@ -564,8 +608,10 @@ async def handle_user_data_pin(msg: UserDataResponse) -> SetupComplete | SetupEr
564608
address=_pairing_android_tv.address,
565609
use_external_metadata=_use_external_metadata,
566610
use_chromecast=_use_chromecast,
611+
use_chromecast_volume=_use_chromecast_volume,
567612
manufacturer=device_info.get("manufacturer", ""),
568613
model=device_info.get("model", ""),
614+
volume_step=_volume_step,
569615
)
570616

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

597643
use_chromecast = msg.input_values.get("chromecast", "false") == "true"
644+
use_chromecast_volume = msg.input_values.get("chromecast_volume", "false") == "true"
598645
use_external_metadata = msg.input_values.get("external_metadata", "false") == "true"
646+
volume_step = float(msg.input_values.get("volume_step", 10))
599647

600648
_LOG.debug("User has changed configuration")
601649
_reconfigured_device.use_chromecast = use_chromecast
650+
_reconfigured_device.use_chromecast_volume = use_chromecast_volume
602651
_reconfigured_device.use_external_metadata = use_external_metadata
652+
_reconfigured_device.volume_step = volume_step
603653

604654
config.devices.add_or_update(_reconfigured_device) # triggers ATV instance update
605655
await asyncio.sleep(1)
606-
_LOG.info("Setup successfully completed for %s", _reconfigured_device.name)
656+
_LOG.info(
657+
"Setup successfully completed for %s (chromecast %s, external metadata %s, volume step %s)",
658+
_reconfigured_device.name,
659+
_reconfigured_device.use_chromecast,
660+
_reconfigured_device.use_external_metadata,
661+
_reconfigured_device.volume_step,
662+
)
607663

608664
return SetupComplete()
609665

src/tv.py

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,11 @@ def is_on(self) -> bool | None:
346346
"""Whether the Android TV is on or off. Returns None if not connected."""
347347
return self._atv.is_on
348348

349+
@property
350+
def device_config(self) -> AtvDevice:
351+
"""Return current device configuration."""
352+
return self._device_config
353+
349354
@property
350355
def media_title(self) -> str | None:
351356
"""Return media title."""
@@ -961,46 +966,66 @@ async def media_seek(self, position: float) -> ucapi.StatusCodes:
961966

962967
async def volume_up(self) -> ucapi.StatusCodes:
963968
"""Change volume up."""
964-
if self._chromecast is None:
965-
return ucapi.StatusCodes.NOT_IMPLEMENTED
966-
try:
967-
self._chromecast.volume_up()
968-
return ucapi.StatusCodes.OK
969-
except PyChromecastError as ex:
970-
_LOG.error("[%s] Chromecast error sending command : %s", self.log_id, ex)
971-
return ucapi.StatusCodes.BAD_REQUEST
969+
if self.device_config.use_chromecast and self.device_config.use_chromecast_volume:
970+
if self._chromecast is None:
971+
return ucapi.StatusCodes.NOT_IMPLEMENTED
972+
try:
973+
_LOG.debug(
974+
"[%s] Volume up : current %.2f + step %s",
975+
self.log_id,
976+
self._chromecast.status.volume_level,
977+
self._device_config.volume_step / 100,
978+
)
979+
self._chromecast.volume_up(delta=float(self._device_config.volume_step / 100))
980+
return ucapi.StatusCodes.OK
981+
except PyChromecastError as ex:
982+
_LOG.error("[%s] Chromecast error sending command : %s", self.log_id, ex)
983+
return ucapi.StatusCodes.BAD_REQUEST
984+
return await self.send_media_player_command(media_player.Commands.VOLUME_UP)
972985

973986
async def volume_down(self) -> ucapi.StatusCodes:
974987
"""Change volume down."""
975-
if self._chromecast is None:
976-
return ucapi.StatusCodes.NOT_IMPLEMENTED
977-
try:
978-
self._chromecast.volume_down()
979-
return ucapi.StatusCodes.OK
980-
except PyChromecastError as ex:
981-
_LOG.error("[%s] Chromecast error sending command : %s", self.log_id, ex)
982-
return ucapi.StatusCodes.BAD_REQUEST
988+
if self.device_config.use_chromecast and self.device_config.use_chromecast_volume:
989+
if self._chromecast is None:
990+
return ucapi.StatusCodes.NOT_IMPLEMENTED
991+
try:
992+
_LOG.debug(
993+
"[%s] Volume down : current %.2f - step %s",
994+
self.log_id,
995+
self._chromecast.status.volume_level,
996+
self._device_config.volume_step / 100,
997+
)
998+
self._chromecast.volume_down(delta=float(self._device_config.volume_step / 100))
999+
return ucapi.StatusCodes.OK
1000+
except PyChromecastError as ex:
1001+
_LOG.error("[%s] Chromecast error sending command : %s", self.log_id, ex)
1002+
return ucapi.StatusCodes.BAD_REQUEST
1003+
return await self.send_media_player_command(media_player.Commands.VOLUME_DOWN)
9831004

9841005
async def volume_mute_toggle(self) -> ucapi.StatusCodes:
9851006
"""Mute toggle."""
986-
if self._chromecast is None:
987-
return ucapi.StatusCodes.NOT_IMPLEMENTED
988-
try:
989-
self._muted = not self._muted
990-
self._chromecast.set_volume_muted(self._muted)
991-
return ucapi.StatusCodes.OK
992-
except PyChromecastError as ex:
993-
_LOG.error("[%s] Chromecast error sending command : %s", self.log_id, ex)
994-
return ucapi.StatusCodes.BAD_REQUEST
1007+
if self.device_config.use_chromecast and self.device_config.use_chromecast_volume:
1008+
if self._chromecast is None:
1009+
return ucapi.StatusCodes.NOT_IMPLEMENTED
1010+
try:
1011+
self._muted = not self._muted
1012+
_LOG.debug("[%s] Mute toggle : %s", self.log_id, self._muted)
1013+
self._chromecast.set_volume_muted(self._muted)
1014+
return ucapi.StatusCodes.OK
1015+
except PyChromecastError as ex:
1016+
_LOG.error("[%s] Chromecast error sending command : %s", self.log_id, ex)
1017+
return ucapi.StatusCodes.BAD_REQUEST
1018+
return await self.send_media_player_command(media_player.Commands.MUTE_TOGGLE)
9951019

996-
async def volume_set(self, volume: float | None) -> ucapi.StatusCodes:
1020+
async def volume_set(self, volume: int | None) -> ucapi.StatusCodes:
9971021
"""Set volume."""
9981022
if self._chromecast is None:
9991023
return ucapi.StatusCodes.NOT_IMPLEMENTED
10001024
if volume is None:
10011025
return ucapi.StatusCodes.BAD_REQUEST
10021026
try:
1003-
await self._chromecast.set_volume(volume / 100)
1027+
_LOG.debug("[%s] Set volume : %.2f", self.log_id, float(volume) / 100)
1028+
self._chromecast.set_volume(float(volume) / 100)
10041029
return ucapi.StatusCodes.OK
10051030
except PyChromecastError as ex:
10061031
_LOG.error("[%s] Chromecast error sending command : %s", self.log_id, ex)

0 commit comments

Comments
 (0)