From a2165ae51ad7d3cc6e59c41c18862f20f2b120be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Fri, 28 Feb 2025 15:02:53 +0100 Subject: [PATCH] airplay: Add setting for MRP tunnel It is now possible to force or disable set up of MRP over AirPlay tunnel over Airplay (or the default "auto" mode). If set up of remote control fails, that will now render an exception (just like with other protocols on an error) instead of just a log message. Relates to #2629 --- docs/api/pyatv.conf.html | 14 +- docs/api/pyatv.const.html | 8 +- docs/api/pyatv.convert.html | 6 + docs/api/pyatv.exceptions.html | 1 + docs/api/pyatv.helpers.html | 8 +- docs/api/pyatv.html | 9 +- docs/api/pyatv.interface.html | 260 ++++++++++++++++++----- docs/api/pyatv.settings.html | 143 ++++++++----- docs/api/pyatv.storage.file_storage.html | 1 + docs/api/pyatv.storage.html | 8 +- pyatv/protocols/airplay/__init__.py | 148 +++++++------ pyatv/scripts/atvremote.py | 1 + pyatv/settings.py | 20 ++ 13 files changed, 440 insertions(+), 187 deletions(-) diff --git a/docs/api/pyatv.conf.html b/docs/api/pyatv.conf.html index e3766fa09..ca2cec8b2 100644 --- a/docs/api/pyatv.conf.html +++ b/docs/api/pyatv.conf.html @@ -64,7 +64,7 @@

Classes

class AirPlayService -(identifier: Optional[str], port: int = 7000, credentials: Optional[str] = None, properties: Optional[Mapping[str, str]] = None) +(identifier: str | None, port: int = 7000, credentials: str | None = None, properties: Mapping[str, str] | None = None)

Representation of an AirPlay service.

@@ -97,7 +97,7 @@

Inherited members

class AppleTV -(address: ipaddress.IPv4Address, name: str, deep_sleep: bool = False, properties: Optional[Mapping[str, Mapping[str, str]]] = None, device_info: Optional[DeviceInfo] = None) +(address: ipaddress.IPv4Address, name: str, deep_sleep: bool = False, properties: Mapping[str, Mapping[str, str]] | None = None, device_info: DeviceInfo | None = None)

Representation of a device configuration.

@@ -135,7 +135,7 @@

Inherited members

class CompanionService -(port: int, credentials: Optional[str] = None, properties: Optional[Mapping[str, str]] = None) +(port: int, credentials: str | None = None, properties: Mapping[str, str] | None = None)

Representation of a Companion link service.

@@ -168,7 +168,7 @@

Inherited members

class DmapService -(identifier: Optional[str], credentials: Optional[str], port: int = 3689, properties: Optional[Mapping[str, str]] = None) +(identifier: str | None, credentials: str | None, port: int = 3689, properties: Mapping[str, str] | None = None)

Representation of a DMAP service.

@@ -201,7 +201,7 @@

Inherited members

class ManualService -(identifier: Optional[str], protocol: Protocol, port: int, properties: Optional[Mapping[str, str]], credentials: Optional[str] = None, password: Optional[str] = None, requires_password: bool = False, pairing_requirement: PairingRequirement = PairingRequirement.Unsupported, enabled: bool = True) +(identifier: str | None, protocol: Protocol, port: int, properties: Mapping[str, str] | None, credentials: str | None = None, password: str | None = None, requires_password: bool = False, pairing_requirement: PairingRequirement = PairingRequirement.Unsupported, enabled: bool = True)

Service used when manually creating and adding a service.

@@ -240,7 +240,7 @@

Inherited members

class MrpService -(identifier: Optional[str], port: int, credentials: Optional[str] = None, properties: Optional[Mapping[str, str]] = None) +(identifier: str | None, port: int, credentials: str | None = None, properties: Mapping[str, str] | None = None)

Representation of a MediaRemote Protocol (MRP) service.

@@ -273,7 +273,7 @@

Inherited members

class RaopService -(identifier: Optional[str], port: int = 7000, credentials: Optional[str] = None, password: Optional[str] = None, properties: Optional[Mapping[str, str]] = None) +(identifier: str | None, port: int = 7000, credentials: str | None = None, password: str | None = None, properties: Mapping[str, str] | None = None)

Representation of an RAOP service.

diff --git a/docs/api/pyatv.const.html b/docs/api/pyatv.const.html index 1fca6d199..d3b5afbaa 100644 --- a/docs/api/pyatv.const.html +++ b/docs/api/pyatv.const.html @@ -950,19 +950,19 @@

Class variables

var Click = 5
-
+

The type of the None singleton.

var Hold = 3
-
+

The type of the None singleton.

var Press = 1
-
+

The type of the None singleton.

var Release = 4
-
+

The type of the None singleton.

diff --git a/docs/api/pyatv.convert.html b/docs/api/pyatv.convert.html index cce1f6081..b37750e4d 100644 --- a/docs/api/pyatv.convert.html +++ b/docs/api/pyatv.convert.html @@ -49,6 +49,7 @@

Functions

Convert internal API device state to string.

+
@@ -57,6 +58,7 @@

Functions

Convert internal API media type to string.

+
@@ -65,6 +67,7 @@

Functions

Convert device model to string.

+
@@ -73,6 +76,7 @@

Functions

Convert internal API protocol to string.

+
@@ -81,6 +85,7 @@

Functions

Convert internal API repeat state to string.

+
@@ -89,6 +94,7 @@

Functions

Convert internal API shuffle state to string.

+
diff --git a/docs/api/pyatv.exceptions.html b/docs/api/pyatv.exceptions.html index a368b1ecc..674273c73 100644 --- a/docs/api/pyatv.exceptions.html +++ b/docs/api/pyatv.exceptions.html @@ -232,6 +232,7 @@

Instance variables

var status_code -> int

Return status code that triggered the error.

+
diff --git a/docs/api/pyatv.helpers.html b/docs/api/pyatv.helpers.html index b70e1cd17..efa98cfdc 100644 --- a/docs/api/pyatv.helpers.html +++ b/docs/api/pyatv.helpers.html @@ -42,7 +42,7 @@

Functions

-async def auto_connect(handler: Callable[[AppleTV], None], timeout: int = 5, not_found: Optional[Callable[[], None]] = None, loop: Optional[asyncio.events.AbstractEventLoop] = None) -> None +async def auto_connect(handler: Callable[[AppleTV], None], timeout: int = 5, not_found: Callable[[], None] | None = None, loop: asyncio.events.AbstractEventLoop | None = None) -> None
@@ -52,10 +52,11 @@

Functions

optional error handler can be provided that is called when no device was found. Very inflexible in many cases, but can be handys sometimes when trying things.

Note: both handler and not_found must be coroutines

+
-def get_unique_id(service_type: str, service_name: str, properties: Mapping[str, str]) -> Optional[str] +def get_unique_id(service_type: str, service_name: str, properties: Mapping[str, str]) -> str | None
@@ -64,6 +65,7 @@

Functions

service_name name of the service (e.g. Office or Living Room) and properties all key-value properties belonging to the service.

The unique identifier is returned if available, otherwise None is returned.

+
@@ -76,6 +78,7 @@

Functions

PairingRequirement.Unsupported or PairingRequirement.Disabled. In all other cases it will return True. Do note that even if this method returns True, pairing (or that existing credentials are provided) might still be needed.

+
@@ -87,6 +90,7 @@

Functions

This method will return if the file format of the given file is supported and streamable by pyatv. It will never raise an exception, e.g. because the file is missing or lack of permissions.

+
diff --git a/docs/api/pyatv.html b/docs/api/pyatv.html index 3a7d18ac8..01bd41786 100644 --- a/docs/api/pyatv.html +++ b/docs/api/pyatv.html @@ -83,29 +83,32 @@

Functions

-async def connect(config: BaseConfig, loop: asyncio.events.AbstractEventLoop, protocol: Optional[Protocol] = None, session: Optional[aiohttp.client.ClientSession] = None, storage: Optional[Storage] = None) -> AppleTV +async def connect(config: BaseConfig, loop: asyncio.events.AbstractEventLoop, protocol: Protocol | None = None, session: aiohttp.client.ClientSession | None = None, storage: Storage | None = None) -> AppleTV

Connect to a device based on a configuration.

+
-async def pair(config: BaseConfig, protocol: Protocol, loop: asyncio.events.AbstractEventLoop, session: aiohttp.client.ClientSession = None, storage: Optional[Storage] = None, **kwargs) -> PairingHandler +async def pair(config: BaseConfig, protocol: Protocol, loop: asyncio.events.AbstractEventLoop, session: aiohttp.client.ClientSession = None, storage: Storage | None = None, **kwargs) -> PairingHandler

Pair a protocol for an Apple TV.

+
-async def scan(loop: asyncio.events.AbstractEventLoop, timeout: int = 5, identifier: Union[str, Set[str], ForwardRef(None)] = None, protocol: Union[Protocol, Set[Protocol], ForwardRef(None)] = None, hosts: Optional[List[str]] = None, aiozc: Optional[zeroconf.asyncio.AsyncZeroconf] = None, storage: Optional[Storage] = None) -> List[BaseConfig] +async def scan(loop: asyncio.events.AbstractEventLoop, timeout: int = 5, identifier: str | Set[str] | None = None, protocol: Protocol | Set[Protocol] | None = None, hosts: List[str] | None = None, aiozc: zeroconf.asyncio.AsyncZeroconf | None = None, storage: Storage | None = None) -> List[BaseConfig]

Scan for Apple TVs on network and return their configurations.

When passing in an aiozc instance, a ServiceBrowser must be running for all the types in the protocols that being scanned for.

+
diff --git a/docs/api/pyatv.interface.html b/docs/api/pyatv.interface.html index b32de95f6..20780e1f4 100644 --- a/docs/api/pyatv.interface.html +++ b/docs/api/pyatv.interface.html @@ -362,6 +362,7 @@

Functions

Retrieve all commands and help texts from an API object.

+
@@ -370,7 +371,7 @@

Classes

class App -(name: Optional[str], identifier: str) +(name: str | None, identifier: str)

Information about an app.

@@ -381,10 +382,12 @@

Instance variables

var identifier -> str

Return a unique bundle id for the app.

+
-
var name -> Optional[str]
+
var name -> str | None

User friendly name of app.

+
@@ -412,58 +415,72 @@

Instance variables

var apps -> Apps

Return apps interface.

+
var audio -> Audio

Return audio interface.

+
var device_info -> DeviceInfo

Return API for device information.

+
var features -> Features

Return features interface.

+
var keyboard -> Keyboard

Return keyboard interface.

+
var metadata -> Metadata

Return API for retrieving metadata from the Apple TV.

+
var power -> Power

Return API for power management.

+
var push_updater -> PushUpdater

Return API for handling push update from the Apple TV.

+
var remote_control -> RemoteControl

Return API for controlling the Apple TV.

+
var service -> BaseService

Return service used to connect to the Apple TV.

+
var settings -> Settings

Return device settings used by pyatv.

+
var stream -> Stream

Return API for streaming media.

+
var touch -> TouchGestures

Return touch gestures interface.

+
var user_accounts -> UserAccounts

Return user accounts interface.

+

Methods

@@ -475,6 +492,7 @@

Methods

Close connection and release allocated resources.

+
@@ -484,6 +502,7 @@

Methods

Initiate connection to device.

No need to call it yourself, it's done automatically.

+
@@ -511,6 +530,7 @@

Methods

Supported by: Protocol.Companion

Fetch a list of apps that can be launched.

+
@@ -523,6 +543,7 @@

Methods

Supported by: Protocol.Companion

Launch an app based on bundle ID or URL.

+ @@ -542,18 +563,22 @@

Instance variables

var bytes -> bytes

Alias for field number 0

+
var height -> int

Alias for field number 3

+
var mimetype -> str

Alias for field number 1

+
var width -> int

Alias for field number 2

+
@@ -586,11 +611,13 @@

Instance variables

var output_devices -> List[OutputDevice]

Return current list of output device IDs.

+
var volume -> float

Return current volume level.

Range is in percent, i.e. [0.0-100.0].

+

Methods

@@ -606,6 +633,7 @@

Methods

Supported by: Protocol.MRP

Add output devices.

+
@@ -618,6 +646,7 @@

Methods

Supported by: Protocol.MRP

Remove output devices.

+
@@ -630,6 +659,7 @@

Methods

Supported by: Protocol.MRP

Set output devices.

+
@@ -643,6 +673,7 @@

Methods

Change current volume level.

Range is in percent, i.e. [0.0-100.0].

+
@@ -659,6 +690,7 @@

Methods

range. It is not necessarily linear.

Call will block until volume change has been acknowledged by the device (when possible and supported).

+
@@ -675,6 +707,7 @@

Methods

range. It is not necessarily linear.

Call will block until volume change has been acknowledged by the device (when possible and supported).

+ @@ -701,6 +734,7 @@

Methods

Output devices were updated.

+
@@ -709,6 +743,7 @@

Methods

Device volume was updated.

+
@@ -736,38 +771,47 @@

Instance variables

var address -> ipaddress.IPv4Address

IP address of device.

+
var all_identifiers -> List[str]

Return all unique identifiers for this device.

+
var deep_sleep -> bool

If device is in deep sleep.

+
var device_info -> DeviceInfo

Return general device information.

+
-
var identifier -> Optional[str]
+
var identifier -> str | None

Return the main identifier associated with this device.

+
var name -> str

Name of device.

+
var properties -> Mapping[str, Mapping[str, str]]

Return Zeroconf properties.

+
var ready -> bool

Return if configuration is ready, (at least one service with identifier).

+
var services -> List[BaseService]

Return all supported services.

+

Methods

@@ -780,6 +824,7 @@

Methods

Add a new service.

If the service already exists, it will be merged.

+
@@ -788,24 +833,27 @@

Methods

Apply settings to configuration.

+
-def get_service(self, protocol: Protocol) -> Optional[BaseService] +def get_service(self, protocol: Protocol) -> BaseService | None

Look up a service based on protocol.

If a service with the specified protocol is not available, None is returned.

+
-def main_service(self, protocol: Optional[Protocol] = None) -> BaseService +def main_service(self, protocol: Protocol | None = None) -> BaseService

Return suggested service used to establish connection.

+
@@ -814,12 +862,13 @@

Methods

Set credentials for a protocol if it exists.

+
class BaseService -(identifier: Optional[str], protocol: Protocol, port: int, properties: Optional[Mapping[str, str]], credentials: Optional[str] = None, password: Optional[str] = None, enabled: bool = True) +(identifier: str | None, protocol: Protocol, port: int, properties: Mapping[str, str] | None, credentials: str | None = None, password: str | None = None, enabled: bool = True)

Base class for protocol services.

@@ -839,30 +888,37 @@

Instance variables

var enabled -> bool

Return True if service is enabled.

+
-
var identifier -> Optional[str]
+
var identifier -> str | None

Return unique identifier associated with this service.

+
var pairing -> PairingRequirement

Return if pairing is required by service.

+
var port -> int

Return service port number.

+
var properties -> Mapping[str, str]

Return service Zeroconf properties.

+
var protocol -> Protocol

Return protocol type.

+
var requires_password -> bool

Return if a password is required to access service.

+

Methods

@@ -877,6 +933,7 @@

Methods

Expects the same format as returned by settings() method. Unknown properties are silently ignored. Settings with a None value are also ignore (keeps original value).

+
@@ -886,6 +943,7 @@

Methods

Merge with other service of same type.

Merge will only include credentials, password and properties.

+
@@ -894,6 +952,7 @@

Methods

Return settings and their values.

+
@@ -909,46 +968,54 @@

Class variables

var OUTPUT_DEVICE_ID
-
+

The type of the None singleton.

Instance variables

-
var build_number -> Optional[str]
+
var build_number -> str | None

Operating system build number, e.g. 17K795.

+
-
var mac -> Optional[str]
+
var mac -> str | None

Device MAC address.

+
var model -> DeviceModel

Hardware model name, e.g. 3, 4 or 4K.

+
var model_str -> str

Return model name as string.

This property will return the model name as a string and fallback to raw_model if it is not available.

+
var operating_system -> OperatingSystem

Operating system running on device.

+
-
var output_device_id -> Optional[str]
+
var output_device_id -> str | None

Output device identifier.

+
-
var raw_model -> Optional[str]
+
var raw_model -> str | None

Return raw model description.

If DeviceInfo.model returns DeviceModel.Unknown then this property contains the raw model string (if any is available).

+
-
var version -> Optional[str]
+
var version -> str | None

Operating system version.

+
@@ -976,6 +1043,7 @@

Methods

Device connection was (intentionally) closed.

+
@@ -984,12 +1052,13 @@

Methods

Device was unexpectedly disconnected.

+
class FeatureInfo -(state: FeatureState, options: Optional[Dict[str, object]] = {}) +(state: FeatureState, options: Dict[str, object] | None = {})

Feature state and options.

@@ -1000,13 +1069,15 @@

Ancestors

Instance variables

-
var options -> Optional[Dict[str, object]]
+
var options -> Dict[str, object] | None

Alias for field number 1

+
var state -> FeatureState

Alias for field number 0

+
@@ -1034,6 +1105,7 @@

Methods

Return state of all features.

+
@@ -1042,10 +1114,11 @@

Methods

Return current state of a feature.

+
-def in_state(self, states: Union[List[FeatureState], FeatureState], *feature_names: FeatureName) -> bool +def in_state(self, states: List[FeatureState] | FeatureState, *feature_names: FeatureName) -> bool
@@ -1053,6 +1126,7 @@

Methods

This method will return True if all given features are in the state specified by "states". If "states" is a list of states, it is enough for the feature to be in one of the listed states.

+
@@ -1082,6 +1156,7 @@

Instance variables

var text_focus_state -> KeyboardFocusState

Return keyboard focus state.

+

Methods

@@ -1097,6 +1172,7 @@

Methods

Supported by: Protocol.Companion

Input text into virtual keyboard.

+
@@ -1109,10 +1185,11 @@

Methods

Supported by: Protocol.Companion

Clear virtual keyboard text.

+
-async def text_get(self) -> Optional[str] +async def text_get(self) -> str | None
@@ -1121,6 +1198,7 @@

Methods

Supported by: Protocol.Companion

Get current virtual keyboard text.

+
@@ -1133,6 +1211,7 @@

Methods

Supported by: Protocol.Companion

Replace text in virtual keyboard.

+ @@ -1159,37 +1238,38 @@

Methods

Keyboard focus state was updated.

+
class MediaMetadata -(title: Optional[str] = None, artist: Optional[str] = None, album: Optional[str] = None, artwork: Optional[bytes] = None, duration: Optional[float] = None) +(title: str | None = None, artist: str | None = None, album: str | None = None, artwork: bytes | None = None, duration: float | None = None)

Container for media (e.g. audio or video) metadata.

Class variables

-
var album -> Optional[str]
+
var album -> str | None
-
+

The type of the None singleton.

-
var artist -> Optional[str]
+
var artist -> str | None
-
+

The type of the None singleton.

-
var artwork -> Optional[bytes]
+
var artwork -> bytes | None
-
+

The type of the None singleton.

-
var duration -> Optional[float]
+
var duration -> float | None
-
+

The type of the None singleton.

-
var title -> Optional[str]
+
var title -> str | None
-
+

The type of the None singleton.

@@ -1208,27 +1288,30 @@

Subclasses

Instance variables

-
var app -> Optional[App]
+
var app -> App | None

Return information about current app playing something.

Do note that this property returns which app is currently playing something and not which app is currently active. If nothing is playing, the corresponding feature will be unavailable.

+
var artwork_id -> str

Return a unique identifier for current artwork.

+
-
var device_id -> Optional[str]
+
var device_id -> str | None

Return a unique identifier for current device.

+

Methods

-async def artwork(self, width: Optional[int] = 512, height: Optional[int] = None) -> Optional[ArtworkInfo] +async def artwork(self, width: int | None = 512, height: int | None = None) -> ArtworkInfo | None
@@ -1242,6 +1325,7 @@

Methods

return artwork of a different size. Set both parameters to None to request default size. Set one of them and let the other one be None to keep original aspect ratio.

+
@@ -1250,12 +1334,13 @@

Methods

Return what is currently playing.

+
class OutputDevice -(name: Optional[str], identifier: str) +(name: str | None, identifier: str)

Information about an output device.

@@ -1266,10 +1351,12 @@

Instance variables

var identifier -> str

Return a unique id for the output device.

+
-
var name -> Optional[str]
+
var name -> str | None

User friendly name of output device.

+
@@ -1297,15 +1384,18 @@

Instance variables

var device_provides_pin -> bool

Return True if remote device presents PIN code, else False.

+
var has_paired -> bool

If a successful pairing has been performed.

The value will be reset when stop() is called.

+
var service -> BaseService

Return service used for pairing.

+

Methods

@@ -1317,6 +1407,7 @@

Methods

Start pairing process.

+
@@ -1325,6 +1416,7 @@

Methods

Call to free allocated resources after pairing.

+
@@ -1333,6 +1425,7 @@

Methods

Stop pairing process.

+
@@ -1341,12 +1434,13 @@

Methods

Pin code used for pairing.

+
class Playing -(media_type: MediaType = MediaType.Unknown, device_state: DeviceState = DeviceState.Idle, title: Optional[str] = None, artist: Optional[str] = None, album: Optional[str] = None, genre: Optional[str] = None, total_time: Optional[int] = None, position: Optional[int] = None, shuffle: Optional[ShuffleState] = None, repeat: Optional[RepeatState] = None, hash: Optional[str] = None, series_name: Optional[str] = None, season_number: Optional[int] = None, episode_number: Optional[int] = None, content_identifier: Optional[str] = None, itunes_store_identifier: Optional[int] = None) +(media_type: MediaType = MediaType.Unknown, device_state: DeviceState = DeviceState.Idle, title: str | None = None, artist: str | None = None, album: str | None = None, genre: str | None = None, total_time: int | None = None, position: int | None = None, shuffle: ShuffleState | None = None, repeat: RepeatState | None = None, hash: str | None = None, series_name: str | None = None, season_number: int | None = None, episode_number: int | None = None, content_identifier: str | None = None, itunes_store_identifier: int | None = None)

Base class for retrieving what is currently playing.

@@ -1358,71 +1452,87 @@

Ancestors

Instance variables

-
var album -> Optional[str]
+
var album -> str | None

Album of the currently playing song.

+
-
var artist -> Optional[str]
+
var artist -> str | None

Artist of the currently playing song.

+
-
var content_identifier -> Optional[str]
+
var content_identifier -> str | None

Content identifier (app specific).

+
var device_state -> DeviceState

Device state, e.g. playing or paused.

+
-
var episode_number -> Optional[int]
+
var episode_number -> int | None

Episode number of TV series.

+
-
var genre -> Optional[str]
+
var genre -> str | None

Genre of the currently playing song.

+
var hash -> str

Create a unique hash for what is currently playing.

The hash is based on title, artist, album and total time. It should always be the same for the same content, but it is not guaranteed.

+
-
var itunes_store_identifier -> Optional[int]
+
var itunes_store_identifier -> int | None

Itunes Store identifier.

+
var media_type -> MediaType

Type of media is currently playing, e.g. video, music.

+
-
var position -> Optional[int]
+
var position -> int | None

Position in the playing media (seconds).

+
-
var repeat -> Optional[RepeatState]
+
var repeat -> RepeatState | None

Repeat mode.

+
-
var season_number -> Optional[int]
+
var season_number -> int | None

Season number of TV series.

+
-
var series_name -> Optional[str]
+
var series_name -> str | None

Title of TV series.

+
-
var shuffle -> Optional[ShuffleState]
+
var shuffle -> ShuffleState | None

If shuffle is enabled or not.

+
-
var title -> Optional[str]
+
var title -> str | None

Title of the current media, e.g. movie or song name.

+
-
var total_time -> Optional[int]
+
var total_time -> int | None

Total play time in seconds.

+
@@ -1452,6 +1562,7 @@

Instance variables

var power_state -> PowerState

Return device power state.

+

Methods

@@ -1467,6 +1578,7 @@

Methods

Supported by: Protocol.Companion, Protocol.MRP

Turn device off.

+
@@ -1479,6 +1591,7 @@

Methods

Supported by: Protocol.Companion, Protocol.MRP

Turn device on.

+ @@ -1507,6 +1620,7 @@

Methods

Device power state was updated.

+
@@ -1535,6 +1649,7 @@

Methods

Inform about an error when updating play status.

+
@@ -1543,6 +1658,7 @@

Methods

Inform about changes to what is currently playing.

+
@@ -1573,6 +1689,7 @@

Instance variables

var active -> bool

Return if push updater has been started.

+

Methods

@@ -1589,6 +1706,7 @@

Methods

Begin to listen to updates.

If an error occurs, start must be called again.

+
@@ -1597,6 +1715,7 @@

Methods

No longer forward updates to listener.

+
@@ -1628,6 +1747,7 @@

Methods

Supported by: Protocol.Companion

Select previous channel.

+
@@ -1640,6 +1760,7 @@

Methods

Supported by: Protocol.Companion

Select next channel.

+
@@ -1652,6 +1773,7 @@

Methods

Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

Press key down.

+
@@ -1664,6 +1786,7 @@

Methods

Supported by: Protocol.Companion, Protocol.MRP

Press key home.

+
@@ -1676,6 +1799,7 @@

Methods

Supported by: Protocol.MRP

Hold key home.

+
@@ -1688,6 +1812,7 @@

Methods

Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

Press key left.

+
@@ -1700,6 +1825,7 @@

Methods

Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

Press key menu.

+
@@ -1712,6 +1838,7 @@

Methods

Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

Press key next.

+
@@ -1724,6 +1851,7 @@

Methods

Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP, Protocol.RAOP

Press key pause.

+
@@ -1736,6 +1864,7 @@

Methods

Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

Press key play.

+
@@ -1748,6 +1877,7 @@

Methods

Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

Toggle between play and pause.

+
@@ -1760,6 +1890,7 @@

Methods

Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

Press key previous.

+
@@ -1772,6 +1903,7 @@

Methods

Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

Press key right.

+
@@ -1784,6 +1916,7 @@

Methods

Supported by: Protocol.Companion

Activate screen saver..

+
@@ -1796,6 +1929,7 @@

Methods

Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

Press key select.

+
@@ -1808,6 +1942,7 @@

Methods

Supported by: Protocol.DMAP, Protocol.MRP

Seek in the current playing media.

+
@@ -1820,6 +1955,7 @@

Methods

Supported by: Protocol.DMAP, Protocol.MRP

Change repeat state.

+
@@ -1832,6 +1968,7 @@

Methods

Supported by: Protocol.DMAP, Protocol.MRP

Change shuffle mode to on or off.

+
@@ -1846,6 +1983,7 @@

Methods

Skip backward a time interval.

If time_interval is not positive or not present, a default or app-chosen time interval is used, which is typically 10, 15, 30, etc. seconds.

+
@@ -1860,6 +1998,7 @@

Methods

Skip forward a time interval.

If time_interval is not positive or not present, a default or app-chosen time interval is used, which is typically 10, 15, 30, etc. seconds.

+
@@ -1872,6 +2011,7 @@

Methods

Supported by: Protocol.AirPlay, Protocol.DMAP, Protocol.MRP, Protocol.RAOP

Press key stop.

+
@@ -1885,6 +2025,7 @@

Methods

Suspend the device.

DEPRECATED: Use Power.turn_off() instead.

+
@@ -1897,6 +2038,7 @@

Methods

Supported by: Protocol.DMAP, Protocol.MRP

Go to main menu (long press menu).

+
@@ -1909,6 +2051,7 @@

Methods

Supported by: Protocol.Companion, Protocol.DMAP, Protocol.MRP

Press key up.

+
@@ -1922,6 +2065,7 @@

Methods

Press key volume down.

DEPRECATED: Use Audio.volume_down() instead.

+
@@ -1935,6 +2079,7 @@

Methods

Press key volume up.

DEPRECATED: Use Audio.volume_up() instead.

+
@@ -1948,6 +2093,7 @@

Methods

Wake up the device.

DEPRECATED: Use Power.turn_on() instead.

+ @@ -1970,6 +2116,7 @@

Instance variables

var settings -> Sequence[Settings]

Return settings for all devices.

+

Methods

@@ -1986,6 +2133,7 @@

Methods

If no settings exists for the current configuration, new settings are created automatically and returned. If the configuration does not contain any valid identitiers, DeviceIdMissingError will be raised.

+
@@ -1994,6 +2142,7 @@

Methods

Load settings from active storage.

+
@@ -2003,6 +2152,7 @@

Methods

Remove settings from storage.

Returns True if settings were removed, otherwise False.

+
@@ -2011,6 +2161,7 @@

Methods

Save settings to active storage.

+
@@ -2021,6 +2172,7 @@

Methods

Update settings based on config.

This method extracts settings from a configuration and writes them back to the storage.

+ @@ -2045,6 +2197,7 @@

Methods

Close connection and release allocated resources.

+
@@ -2057,10 +2210,11 @@

Methods

Supported by: Protocol.AirPlay

Play media from an URL on the device.

+
-async def stream_file(self, file: Union[str, io.BufferedIOBase, asyncio.streams.StreamReader], /, metadata: Optional[MediaMetadata] = None, override_missing_metadata: bool = False, **kwargs) -> None +async def stream_file(self, file: str | io.BufferedIOBase | asyncio.streams.StreamReader, /, metadata: MediaMetadata | None = None, override_missing_metadata: bool = False, **kwargs) -> None
@@ -2071,6 +2225,7 @@

Methods

Stream local or remote file to device.

Supports either local file paths or a HTTP(s) address.

INCUBATING METHOD - MIGHT CHANGE IN THE FUTURE!

+
@@ -2105,6 +2260,7 @@

Methods

:param x: x coordinate :param y: y coordinate :param mode: touch mode (1: press, 3: hold, 4: release)

+
@@ -2118,6 +2274,7 @@

Methods

Send a touch click.

:param action: action mode single tap (0), double tap (1), or hold (2)

+
@@ -2137,6 +2294,7 @@

Methods

:param end_x: End x coordinate :param end_y: Endi x coordinate :param duration_ms: Time in milliseconds to reach the end coordinates

+ @@ -2153,10 +2311,12 @@

Instance variables

var identifier -> str

Return a unique id for the account.

+
-
var name -> Optional[str]
+
var name -> str | None

User name.

+
@@ -2184,6 +2344,7 @@

Methods

Supported by: Protocol.Companion

Fetch a list of user accounts that can be switched.

+
@@ -2196,6 +2357,7 @@

Methods

Supported by: Protocol.Companion

Switch user account by account ID.

+ diff --git a/docs/api/pyatv.settings.html b/docs/api/pyatv.settings.html index 36a548132..941800a57 100644 --- a/docs/api/pyatv.settings.html +++ b/docs/api/pyatv.settings.html @@ -24,6 +24,7 @@

  • credentials
  • identifier
  • +
  • mrp_tunnel
  • password
  • @@ -70,6 +71,14 @@

    MrpTunnel

    + + +
  • ProtocolSettings

    • airplay
    • @@ -107,7 +116,7 @@

      Module pyatv.settings

      Settings for configuring pyatv.

      - +
      @@ -126,7 +135,7 @@

      Classes

      Settings related to AirPlay.

      Create a new model by parsing and validating input data from keyword arguments.

      Raises ValidationError if the input data cannot be parsed to form a valid model.

      - +

      Ancestors

      • pydantic.v1.main.BaseModel
      • @@ -134,17 +143,21 @@

        Ancestors

      Class variables

      -
      var credentials -> Optional[str]
      +
      var credentials -> str | None
      +
      +

      The type of the None singleton.

      +
      +
      var identifier -> str | None
      -
      +

      The type of the None singleton.

      -
      var identifier -> Optional[str]
      +
      var mrp_tunnel -> MrpTunnel
      -
      +

      The type of the None singleton.

      -
      var password -> Optional[str]
      +
      var password -> str | None
      -
      +

      The type of the None singleton.

      @@ -154,7 +167,7 @@

      Class variables

  • AirPlay version to use.

    - +

    Ancestors

    • builtins.str
    • @@ -164,15 +177,15 @@

      Class variables

      var Auto = auto
      -
      +

      Automatically determine what version to use.

      var V1 = 1
      -
      +

      Use version 1 of AirPlay.

      var V2 = 2
      -
      +

      Use version 2 of AirPlay.

    @@ -184,7 +197,7 @@

    Class variables

    Settings related to Companion.

    Create a new model by parsing and validating input data from keyword arguments.

    Raises ValidationError if the input data cannot be parsed to form a valid model.

    - +

    Ancestors

    • pydantic.v1.main.BaseModel
    • @@ -192,13 +205,13 @@

      Ancestors

    Class variables

    -
    var credentials -> Optional[str]
    +
    var credentials -> str | None
    -
    +

    The type of the None singleton.

    -
    var identifier -> Optional[str]
    +
    var identifier -> str | None
    -
    +

    The type of the None singleton.

    @@ -210,7 +223,7 @@

    Class variables

    Settings related to DMAP.

    Create a new model by parsing and validating input data from keyword arguments.

    Raises ValidationError if the input data cannot be parsed to form a valid model.

    - +

    Ancestors

    • pydantic.v1.main.BaseModel
    • @@ -218,13 +231,13 @@

      Ancestors

    Class variables

    -
    var credentials -> Optional[str]
    +
    var credentials -> str | None
    -
    +

    The type of the None singleton.

    -
    var identifier -> Optional[str]
    +
    var identifier -> str | None
    -
    +

    The type of the None singleton.

    @@ -236,7 +249,7 @@

    Class variables

    Information related settings.

    Create a new model by parsing and validating input data from keyword arguments.

    Raises ValidationError if the input data cannot be parsed to form a valid model.

    - +

    Ancestors

    • pydantic.v1.main.BaseModel
    • @@ -246,31 +259,31 @@

      Class variables

      var device_id -> str
      -
      +

      The type of the None singleton.

      var mac -> str
      -
      +

      The type of the None singleton.

      var model -> str
      -
      +

      The type of the None singleton.

      var name -> str
      -
      +

      The type of the None singleton.

      var os_build -> str
      -
      +

      The type of the None singleton.

      var os_name -> str
      -
      +

      The type of the None singleton.

      var os_version -> str
      -
      +

      The type of the None singleton.

      Static methods

      @@ -293,7 +306,7 @@

      Static methods

      Settings related to MRP.

      Create a new model by parsing and validating input data from keyword arguments.

      Raises ValidationError if the input data cannot be parsed to form a valid model.

      - +

      Ancestors

      • pydantic.v1.main.BaseModel
      • @@ -301,13 +314,41 @@

        Ancestors

      Class variables

      -
      var credentials -> Optional[str]
      +
      var credentials -> str | None
      +
      +

      The type of the None singleton.

      +
      +
      var identifier -> str | None
      +
      +

      The type of the None singleton.

      +
      +
      + +
      +class MrpTunnel +(*args, **kwds) +
      +
      +

      How MRP tunneling over AirPlay is handled.

      + +

      Ancestors

      +
        +
      • builtins.str
      • +
      • enum.Enum
      • +
      +

      Class variables

      +
      +
      var Auto = auto
      +
      +

      Automatically set up MRP tunnel if supported by remote device.

      +
      +
      var Disable = disable
      -
      +

      Fully disable set up of MRP tunnel.

      -
      var identifier -> Optional[str]
      +
      var Force = force
      -
      +

      Force set up of MRP tunnel even if remote device does not supports it.

      @@ -319,7 +360,7 @@

      Class variables

      Container for protocol specific settings.

      Create a new model by parsing and validating input data from keyword arguments.

      Raises ValidationError if the input data cannot be parsed to form a valid model.

      - +

      Ancestors

      • pydantic.v1.main.BaseModel
      • @@ -329,23 +370,23 @@

        Class variables

        var airplay -> AirPlaySettings
        -
        +

        The type of the None singleton.

        var companion -> CompanionSettings
        -
        +

        The type of the None singleton.

        var dmap -> DmapSettings
        -
        +

        The type of the None singleton.

        var mrp -> MrpSettings
        -
        +

        The type of the None singleton.

        var raop -> RaopSettings
        -
        +

        The type of the None singleton.

        @@ -357,7 +398,7 @@

        Class variables

        Settings related to RAOP.

        Create a new model by parsing and validating input data from keyword arguments.

        Raises ValidationError if the input data cannot be parsed to form a valid model.

        - +

        Ancestors

        • pydantic.v1.main.BaseModel
        • @@ -370,17 +411,17 @@

          Class variables

          Server side (UDP) port used by control server.

          Set to 0 to use random free port.

          -
          var credentials -> Optional[str]
          +
          var credentials -> str | None
          -
          +

          The type of the None singleton.

          -
          var identifier -> Optional[str]
          +
          var identifier -> str | None
          -
          +

          The type of the None singleton.

          -
          var password -> Optional[str]
          +
          var password -> str | None
          -
          +

          The type of the None singleton.

          var protocol_version -> AirPlayVersion
          @@ -403,7 +444,7 @@

          Class variables

          Settings container class.

          Create a new model by parsing and validating input data from keyword arguments.

          Raises ValidationError if the input data cannot be parsed to form a valid model.

          - +

          Ancestors

          • pydantic.v1.main.BaseModel
          • @@ -413,11 +454,11 @@

            Class variables

            var info -> InfoSettings
            -
            +

            The type of the None singleton.

            var protocols -> ProtocolSettings
            -
            +

            The type of the None singleton.

          diff --git a/docs/api/pyatv.storage.file_storage.html b/docs/api/pyatv.storage.file_storage.html index 2809345e4..47bd205fe 100644 --- a/docs/api/pyatv.storage.file_storage.html +++ b/docs/api/pyatv.storage.file_storage.html @@ -74,6 +74,7 @@

          Static methods

          applications.

          The path used for this file is $HOME/.pyatv.conf (C:\Users\.pyatv.conf on Windows).

          +

          Inherited members

          diff --git a/docs/api/pyatv.storage.html b/docs/api/pyatv.storage.html index e9b08cd83..74e062a9e 100644 --- a/docs/api/pyatv.storage.html +++ b/docs/api/pyatv.storage.html @@ -100,10 +100,12 @@

          Instance variables

          This property will return True if a any setting has been changed. It is reset when data is loaded into storage (by calling load) or manually by calling mark_as_solved (typically done by save).

          +
          var storage_model -> StorageModel

          Return storage model representation.

          +

          Methods

          @@ -122,6 +124,7 @@

          Methods

          identitiers, DeviceIdMissingError will be raised.

          If settings exists for a configuration but mismatch, they will be automatically updated in the storage. Set ignore_update to False to not update storage.

          +
          @@ -132,6 +135,7 @@

          Methods

          Call after saving to indicate settings have been saved.

          The changed property reflects whether something has been changed in the model or not based on calling this method.

          +

          Inherited members

          @@ -165,11 +169,11 @@

          Class variables

          var devices -> List[Settings]
          -
          +

          The type of the None singleton.

          var version -> int
          -
          +

          The type of the None singleton.

          diff --git a/pyatv/protocols/airplay/__init__.py b/pyatv/protocols/airplay/__init__.py index 52477a761..0bff2ea03 100644 --- a/pyatv/protocols/airplay/__init__.py +++ b/pyatv/protocols/airplay/__init__.py @@ -46,6 +46,7 @@ airplayv1, airplayv2, ) +from pyatv.settings import MrpTunnel from pyatv.support import net from pyatv.support.device_info import lookup_model, lookup_os from pyatv.support.http import HttpConnection, StaticFileWebServer, http_connect @@ -230,6 +231,75 @@ async def service_info( update_service_details(service) +def _create_mrp_tunnel_data(core: Core, credentials: HapCredentials): + session = AP2Session( + str(core.config.address), core.service.port, credentials, core.settings.info + ) + + # A protocol requires its corresponding service to function, so add a + # dummy one if we don't have one yet + mrp_service = core.config.get_service(Protocol.MRP) + if mrp_service is None: + mrp_service = MutableService(None, Protocol.MRP, core.service.port, {}) + core.config.add_service(mrp_service) + + ( + _, + mrp_connect, + mrp_close, + mrp_device_info, + mrp_interfaces, + mrp_features, + ) = mrp.create_with_connection( + Core( + core.loop, + core.config, + mrp_service, + core.settings, + core.device_listener, + core.session_manager, + core.takeover, + core.state_dispatcher.create_copy(Protocol.MRP), + ), + AirPlayMrpConnection(session, core.device_listener), + requires_heatbeat=False, # Already have heartbeat on control channel + ) + + async def _connect_rc() -> bool: + try: + await session.connect() + await session.setup_remote_control() + session.start_keep_alive(core.device_listener) + except exceptions.HttpError as ex: + if ex.status_code == 470: + raise exceptions.InvalidCredentialsError( + "invalid or missing credentials" + ) from ex + raise + except Exception as ex: + raise exceptions.ProtocolError( + "Failed to set up remote control channel" + ) from ex + + await mrp_connect() + return True + + def _close_rc() -> Set[asyncio.Task]: + tasks = set() + tasks.update(mrp_close()) + tasks.update(session.stop()) + return tasks + + return SetupData( + Protocol.MRP, + _connect_rc, + _close_rc, + mrp_device_info, + mrp_interfaces, + mrp_features, + ) + + def setup( # pylint: disable=too-many-locals core: Core, ) -> Generator[SetupData, None, None]: @@ -301,80 +371,20 @@ def _device_info() -> Dict[str, Any]: yield from raop_setup(raop_core) - # Set up remote control channel if it is supported - if not is_remote_control_supported(core.service, credentials): + mrp_tunnel = core.settings.protocols.airplay.mrp_tunnel + + if mrp_tunnel == MrpTunnel.Disable: + _LOGGER.debug("Remote control tunnel disabled by setting") + elif mrp_tunnel == MrpTunnel.Force: + _LOGGER.debug("Remote control channel is supported (forced)") + yield _create_mrp_tunnel_data(core, credentials) + elif not is_remote_control_supported(core.service, credentials): _LOGGER.debug("Remote control not supported by device") elif credentials.type not in [AuthenticationType.HAP, AuthenticationType.Transient]: _LOGGER.debug("%s not supported by remote control channel", credentials.type) else: _LOGGER.debug("Remote control channel is supported") - - session = AP2Session( - str(core.config.address), core.service.port, credentials, core.settings.info - ) - - # A protocol requires its corresponding service to function, so add a - # dummy one if we don't have one yet - mrp_service = core.config.get_service(Protocol.MRP) - if mrp_service is None: - mrp_service = MutableService(None, Protocol.MRP, core.service.port, {}) - core.config.add_service(mrp_service) - - ( - _, - mrp_connect, - mrp_close, - mrp_device_info, - mrp_interfaces, - mrp_features, - ) = mrp.create_with_connection( - Core( - core.loop, - core.config, - mrp_service, - core.settings, - core.device_listener, - core.session_manager, - core.takeover, - core.state_dispatcher.create_copy(Protocol.MRP), - ), - AirPlayMrpConnection(session, core.device_listener), - requires_heatbeat=False, # Already have heartbeat on control channel - ) - - async def _connect_rc() -> bool: - try: - await session.connect() - await session.setup_remote_control() - session.start_keep_alive(core.device_listener) - except exceptions.HttpError as ex: - if ex.status_code == 470: - _LOGGER.debug( - "Remote control authorization failed, missing credentials" - ) - else: - _LOGGER.exception("Failed to set up remote control channel") - except Exception: - _LOGGER.exception("Failed to set up remote control channel") - else: - await mrp_connect() - return True - return False - - def _close_rc() -> Set[asyncio.Task]: - tasks = set() - tasks.update(mrp_close()) - tasks.update(session.stop()) - return tasks - - yield SetupData( - Protocol.MRP, - _connect_rc, - _close_rc, - mrp_device_info, - mrp_interfaces, - mrp_features, - ) + yield _create_mrp_tunnel_data(core, credentials) def pair(core: Core, **kwargs) -> PairingHandler: diff --git a/pyatv/scripts/atvremote.py b/pyatv/scripts/atvremote.py index e6b557699..8fd7ae7a0 100644 --- a/pyatv/scripts/atvremote.py +++ b/pyatv/scripts/atvremote.py @@ -106,6 +106,7 @@ async def commands(self): _print_commands("User Accounts", interface.UserAccounts) _print_commands("Global", self.__class__) _print_commands("Touch", interface.TouchGestures) + _print_commands("Settings", SettingsCommands) return 0 diff --git a/pyatv/settings.py b/pyatv/settings.py index 624104409..999fd689c 100644 --- a/pyatv/settings.py +++ b/pyatv/settings.py @@ -48,8 +48,26 @@ class AirPlayVersion(str, Enum): """AirPlay version to use.""" Auto = "auto" + """Automatically determine what version to use.""" + V1 = "1" + """Use version 1 of AirPlay.""" + V2 = "2" + """Use version 2 of AirPlay.""" + + +class MrpTunnel(str, Enum): + """How MRP tunneling over AirPlay is handled.""" + + Auto = "auto" + """Automatically set up MRP tunnel if supported by remote device.""" + + Force = "force" + """Force set up of MRP tunnel even if remote device does not supports it.""" + + Disable = "disable" + """Fully disable set up of MRP tunnel.""" # pylint: enable=invalid-name @@ -82,6 +100,8 @@ class AirPlaySettings(BaseModel, extra="ignore"): # type: ignore[call-arg] credentials: Optional[str] = None password: Optional[str] = None + mrp_tunnel: MrpTunnel = MrpTunnel.Auto + class CompanionSettings(BaseModel, extra="ignore"): # type: ignore[call-arg] """Settings related to Companion."""