diff --git a/docs/api/pyatv.settings.html b/docs/api/pyatv.settings.html index 85ed5a641..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
@@ -142,6 +151,10 @@ Class variables
-
The type of the None singleton.
+var mrp_tunnel -> MrpTunnel
+-
+
The type of the None singleton.
+
var password -> str | None
-
The type of the None singleton.
@@ -154,7 +167,7 @@ Class variables
AirPlay version to use.
var Auto = auto
The type of the None singleton.
Automatically determine what version to use.
var V1 = 1
The type of the None singleton.
Use version 1 of AirPlay.
var V2 = 2
The type of the None singleton.
Use version 2 of AirPlay.
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.
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.
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.
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.
+class MrpTunnel
+(*args, **kwds)
+
How MRP tunneling over AirPlay is handled.
var Auto = auto
Automatically set up MRP tunnel if supported by remote device.
var Disable = disable
Fully disable set up of MRP tunnel.
var Force = force
Force set up of MRP tunnel even if remote device does not supports it.
class ProtocolSettings
(**data: Any)
@@ -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
@@ -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
@@ -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
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."""