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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,4 @@ config.json
licenses.json
tools/licenses.md
/data/
*.wav
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ development.

- [Requirements and setting](docs/settings.md).
- Multiple Android TV devices are supported with version 0.5.0 and newer.
- A [media player entity](https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_media_player.md)
- A [media-player entity](https://unfoldedcircle.github.io/core-api/entities/entity_media_player.html)
is exposed per Android TV device to the Remote.
- A [voice-assistant entity](https://unfoldedcircle.github.io/core-api/entities/entity_voice_assistant.html)
is exposed per Android TV device to the Remote if the voice-command feature is enabled in the device configuration and
the Android TV device supports voice commands.
- Device profiles allow device-specific support and custom key bindings, for example, double-click or long-press actions.
See [command mappings](docs/command_mapping.md) for more information.

Preview features:
- Optional external metadata lookup using the Google Play Store for friendly application name and icon.
- Google Cast support to retrieve media-playing information.
- Google Cast volume control with configurable volume step.
- Google voice commands. Requires a voice-capable device.

The preview features are not enabled by default. They can be enabled in the device configuration of the setup flow.

Expand Down Expand Up @@ -69,7 +73,7 @@ After some tests, turns out Python stuff on embedded is a nightmare. So we're be
that has everything in it, including the Python runtime and all required modules and native libraries.

To do that, we use [PyInstaller](https://pyinstaller.org/), but it needs to run on the target architecture as
`PyInstaller` does not support cross compilation.
`PyInstaller` does not support cross-compilation.

The `--onefile` option to create a one-file bundled executable should be avoided:
- Higher startup cost, since the wrapper binary must first extract the archive.
Expand Down
12 changes: 6 additions & 6 deletions docs/command_mapping.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Command Mapping

The following feature set is defined for the exposed
[Remote Two media-player entity](https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_media_player.md)
[Remote Two/3 media-player entity](https://github.com/unfoldedcircle/core-api/blob/main/doc/entities/entity_media_player.md)
in the default device profile:

| Feature | Command(s) | Android remote keycode(s) | Comments |
Expand Down Expand Up @@ -46,7 +46,7 @@ in the default device profile:

## Device Profiles

Unfortunately the keycode support of Android TV devices is very limited and device- (and probably even app-) specific.
Unfortunately, the keycode support of Android TV devices is very limited and device- (and probably even app-) specific.

Device profiles allow better support of the available features and different key-mappings:

Expand All @@ -66,18 +66,18 @@ Pull requests for additional devices are greatly appreciated.

### Device Profile Matching

Profiles are matched based on manufacturer name and device model, which is returned in the
Profiles are matched based on the manufacturer name and device model, which is returned in the
[device information](https://github.com/tronikos/androidtvremote2/blob/v0.0.14/src/androidtvremote2/androidtv_remote.py#L101)
from the androidtvremote2 library.

Each device profile is defined in a separate json file in [config/profiles](../config/profiles). All profiles are read
Each device profile is defined in a separate JSON file in [config/profiles](../config/profiles). All profiles are read
during driver startup and sorted alphabetically.

- Manufacturer and model fields in the profile file are treated as prefixes.
- Matching against the returned information from the device is performed case-insensitive.
- Manufacturer is mandatory, model is optional (empty field).

For example: profile `manufacturer = "foo"`, `model = "bar"` will match the following device information:
For example, profile `manufacturer = "foo"`, `model = "bar"` will match the following device information:

- foo / bar
- FOO / Bart
Expand All @@ -87,7 +87,7 @@ For example: profile `manufacturer = "foo"`, `model = "bar"` will match the foll
## Testing Keycodes

To simplify testing and identifying working keycodes, the [available keycodes](https://github.com/tronikos/androidtvremote2/blob/v0.0.14/src/androidtvremote2/remotemessage.proto#L90)
can be sent directly with the Core-API to the Remote Two or Core Simulator.
can be sent directly with the Core-API to the Remote Two/3 or Core Simulator.
Either as name (including the `KEYCODE_` prefix), or as numeric value string.

Example:
Expand Down
6 changes: 5 additions & 1 deletion docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

## Limitations and Known Issues

- During the setup process, you have to enter a PIN code that is shown on your Android TV.
- During the setup process, you have to enter a PIN code shown on your Android TV.
- Please make sure that your Android TV is powered on and that no old pairing request is shown.
- If pairing continuously fails, reboot your Android TV device and try again.
- If sending commands doesn't work after pairing or the integration is repeatedly disconnected, try rebooting the
Expand All @@ -30,6 +30,10 @@
- The shown apps in the input selection list are a pre-defined list of common applications.
- Some devices, like TCL, become unavailable after they are turned off, unless you activate the `Screenless service`.
- Activate it under: Settings, System, Power and energy: Screenless service
- Voice commands are a recently reverse-engineered feature and may not work on all devices.
- Streaming voice while speaking is currently disabled, since the detection becomes much more unreliable.
- The full voice command is buffered in the integration and sent after the voice stream stops (when releasing the
microphone button.

See also the known issues of [Home Assistant's Android TV Remote Integration](https://www.home-assistant.io/integrations/androidtv_remote/#limitations-and-known-issues),
since it is using the same communication library.
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
androidtvremote2==0.3.0
ucapi==0.4.0
ucapi==0.5.1
pyee~=13.0.0
google_play_scraper==1.2.7
pillow>=11.2.1
pillow>=11.3
requests>=2.32
pychromecast~=14.0.9
httpx~=0.28.1
Expand Down
29 changes: 29 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,36 @@
from dataclasses import dataclass
from typing import Iterator

from ucapi import EntityTypes

_LOG = logging.getLogger(__name__)

_CFG_FILENAME = "config.json"


def create_entity_id(device_id: str, entity_type: EntityTypes) -> str:
"""Create a unique entity identifier for the given device and entity type."""
# backwards compatibility for the initial media-player entity
if entity_type == EntityTypes.MEDIA_PLAYER:
return f"{device_id}"
return f"{entity_type.value}.{device_id}"


def device_from_entity_id(entity_id: str) -> str | None:
"""
Return the device_id suffix of an entity_id.

The prefix is the part before the first dot in the name and refers to the entity type (media-player or remote),
the suffix is the device identifier.

:param entity_id: the entity identifier
:return: the device suffix, the original entity_id if it doesn't contain a dot, or None if the entity_id is invalid
"""
parts = entity_id.split(".", 1)
device = parts[1] if len(parts) == 2 else parts[0]
return None if len(device) == 0 else device


@dataclass
class AtvDevice:
"""Android TV device configuration."""
Expand All @@ -42,6 +67,8 @@ class AtvDevice:
"""Enable volume driven by Chromecast protocol."""
volume_step: int = 10
"""Volume step (1 to 100)."""
use_voice: bool = False
"""Enable voice commands."""


class _EnhancedJSONEncoder(json.JSONEncoder):
Expand Down Expand Up @@ -139,6 +166,7 @@ def update(self, atv: AtvDevice) -> bool:
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
item.use_voice = atv.use_voice if atv.use_voice else False
return self.store()
return False

Expand Down Expand Up @@ -244,6 +272,7 @@ def load(self) -> bool:
item.get("use_chromecast", False),
item.get("use_chromecast_volume", False),
item.get("volume_step", 10),
item.get("use_voice", False),
)
self._config.append(atv)
return True
Expand Down
Loading