Repository: postlund/pyatv
Pull request #2206 (2023-09-21 20:09:21)

raop: Add protocol_version setting
The setting `protocols.raop.protocol_version` can be set to auto, 1 or 2 to force a specific AirPlay version instead of always falling back to automatic mode.

Relates to #2204

<a href="https://gitpod.io/#https://github.com/postlund/pyatv/pull/2206"><img src="https://gitpod.io/button/open-in-gitpod.svg"/></a>



codecov[bot]: ## [Codecov](https://app.codecov.io/gh/postlund/pyatv/pull/2206?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) Report
Patch coverage: **`96.66%`** and project coverage change: **`-0.05%`** :warning:
> Comparison is base [(`fd7135b`)](https://app.codecov.io/gh/postlund/pyatv/commit/fd7135b588d290e508aa638a3a97e21c73d13451?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) 88.97% compared to head [(`6ab367c`)](https://app.codecov.io/gh/postlund/pyatv/pull/2206?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) 88.93%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #2206      +/-   ##
==========================================
- Coverage   88.97%   88.93%   -0.05%     
==========================================
  Files         167      167              
  Lines       11343    11347       +4     
==========================================
- Hits        10092    10091       -1     
- Misses       1251     1256       +5     
```


| [Files Changed](https://app.codecov.io/gh/postlund/pyatv/pull/2206?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) | Coverage Δ | |
|---|---|---|
| [pyatv/protocols/raop/\_\_init\_\_.py](https://app.codecov.io/gh/postlund/pyatv/pull/2206?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvcHJvdG9jb2xzL3Jhb3AvX19pbml0X18ucHk=) | `97.01% <66.66%> (ø)` | |
| [pyatv/protocols/airplay/\_\_init\_\_.py](https://app.codecov.io/gh/postlund/pyatv/pull/2206?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvcHJvdG9jb2xzL2FpcnBsYXkvX19pbml0X18ucHk=) | `72.94% <100.00%> (-0.16%)` | :arrow_down: |
| [pyatv/protocols/airplay/pairing.py](https://app.codecov.io/gh/postlund/pyatv/pull/2206?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvcHJvdG9jb2xzL2FpcnBsYXkvcGFpcmluZy5weQ==) | `94.11% <100.00%> (-0.71%)` | :arrow_down: |
| [pyatv/protocols/airplay/utils.py](https://app.codecov.io/gh/postlund/pyatv/pull/2206?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvcHJvdG9jb2xzL2FpcnBsYXkvdXRpbHMucHk=) | `88.27% <100.00%> (+0.41%)` | :arrow_up: |
| [pyatv/settings.py](https://app.codecov.io/gh/postlund/pyatv/pull/2206?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvc2V0dGluZ3MucHk=) | `100.00% <100.00%> (ø)` | |

... and [2 files with indirect coverage changes](https://app.codecov.io/gh/postlund/pyatv/pull/2206/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/postlund/pyatv/pull/2206?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl).


Patch:
diff --git a/docs/api/pyatv.settings.html b/docs/api/pyatv.settings.html
index 59ce9513..fea91a6b 100644
--- a/docs/api/pyatv.settings.html
+++ b/docs/api/pyatv.settings.html
@@ -28,6 +28,14 @@ This module has additional documentation
 </ul>
 </li>
 <li>
+<h4><code><a title="pyatv.settings.AirPlayVersion" href="#pyatv.settings.AirPlayVersion">AirPlayVersion</a></code></h4>
+<ul class="">
+<li><code><a title="pyatv.settings.AirPlayVersion.Auto" href="#pyatv.settings.AirPlayVersion.Auto">Auto</a></code></li>
+<li><code><a title="pyatv.settings.AirPlayVersion.V1" href="#pyatv.settings.AirPlayVersion.V1">V1</a></code></li>
+<li><code><a title="pyatv.settings.AirPlayVersion.V2" href="#pyatv.settings.AirPlayVersion.V2">V2</a></code></li>
+</ul>
+</li>
+<li>
 <h4><code><a title="pyatv.settings.CompanionSettings" href="#pyatv.settings.CompanionSettings">CompanionSettings</a></code></h4>
 <ul class="">
 <li><code><a title="pyatv.settings.CompanionSettings.credentials" href="#pyatv.settings.CompanionSettings.credentials">credentials</a></code></li>
@@ -72,11 +80,12 @@ This module has additional documentation
 </li>
 <li>
 <h4><code><a title="pyatv.settings.RaopSettings" href="#pyatv.settings.RaopSettings">RaopSettings</a></code></h4>
-<ul class="">
+<ul class="two-column">
 <li><code><a title="pyatv.settings.RaopSettings.control_port" href="#pyatv.settings.RaopSettings.control_port">control_port</a></code></li>
 <li><code><a title="pyatv.settings.RaopSettings.credentials" href="#pyatv.settings.RaopSettings.credentials">credentials</a></code></li>
 <li><code><a title="pyatv.settings.RaopSettings.identifier" href="#pyatv.settings.RaopSettings.identifier">identifier</a></code></li>
 <li><code><a title="pyatv.settings.RaopSettings.password" href="#pyatv.settings.RaopSettings.password">password</a></code></li>
+<li><code><a title="pyatv.settings.RaopSettings.protocol_version" href="#pyatv.settings.RaopSettings.protocol_version">protocol_version</a></code></li>
 <li><code><a title="pyatv.settings.RaopSettings.timing_port" href="#pyatv.settings.RaopSettings.timing_port">timing_port</a></code></li>
 </ul>
 </li>
@@ -97,7 +106,7 @@ This module has additional documentation
 </header>
 <section id="section-intro">
 <p>Settings for configuring pyatv.</p>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L1-L113" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L1-L134" class="git-link">Browse git</a></div>
 </section>
 <section>
 </section>
@@ -119,7 +128,7 @@ This module has additional documentation
 validated to form a valid model.</p>
 <p><code>__init__</code> uses <code>__pydantic_self__</code> instead of the more common <code>self</code> for the first arg to
 allow <code>self</code> as a field name.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L50-L55" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L64-L69" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>pydantic.main.BaseModel</li>
@@ -140,6 +149,34 @@ allow <code>self</code> as a field name.</p></section>
 </dd>
 </dl>
 </dd>
+<dt id="pyatv.settings.AirPlayVersion"><code class="flex name class">
+<span>class <span class="ident">AirPlayVersion</span></span>
+<span>(</span><span>value, names=None, *, module=None, qualname=None, type=None, start=1)</span>
+</code></dt>
+<dd>
+<section class="desc"><p>AirPlay version to use.</p></section>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L41-L46" class="git-link">Browse git</a></div>
+<h3>Ancestors</h3>
+<ul class="hlist">
+<li>builtins.str</li>
+<li>enum.Enum</li>
+</ul>
+<h3>Class variables</h3>
+<dl>
+<dt id="pyatv.settings.AirPlayVersion.Auto"><code class="name">var <span class="ident">Auto</span> = auto</code></dt>
+<dd>
+<section class="desc"></section>
+</dd>
+<dt id="pyatv.settings.AirPlayVersion.V1"><code class="name">var <span class="ident">V1</span> = 1</code></dt>
+<dd>
+<section class="desc"></section>
+</dd>
+<dt id="pyatv.settings.AirPlayVersion.V2"><code class="name">var <span class="ident">V2</span> = 2</code></dt>
+<dd>
+<section class="desc"></section>
+</dd>
+</dl>
+</dd>
 <dt id="pyatv.settings.CompanionSettings"><code class="flex name class">
 <span>class <span class="ident">CompanionSettings</span></span>
 <span>(</span><span>**data: Any)</span>
@@ -151,7 +188,7 @@ allow <code>self</code> as a field name.</p></section>
 validated to form a valid model.</p>
 <p><code>__init__</code> uses <code>__pydantic_self__</code> instead of the more common <code>self</code> for the first arg to
 allow <code>self</code> as a field name.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L58-L62" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L72-L76" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>pydantic.main.BaseModel</li>
@@ -179,7 +216,7 @@ allow <code>self</code> as a field name.</p></section>
 validated to form a valid model.</p>
 <p><code>__init__</code> uses <code>__pydantic_self__</code> instead of the more common <code>self</code> for the first arg to
 allow <code>self</code> as a field name.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L65-L69" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L79-L83" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>pydantic.main.BaseModel</li>
@@ -207,7 +244,7 @@ allow <code>self</code> as a field name.</p></section>
 validated to form a valid model.</p>
 <p><code>__init__</code> uses <code>__pydantic_self__</code> instead of the more common <code>self</code> for the first arg to
 allow <code>self</code> as a field name.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L38-L47" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L52-L61" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>pydantic.main.BaseModel</li>
@@ -255,7 +292,7 @@ allow <code>self</code> as a field name.</p></section>
 validated to form a valid model.</p>
 <p><code>__init__</code> uses <code>__pydantic_self__</code> instead of the more common <code>self</code> for the first arg to
 allow <code>self</code> as a field name.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L72-L76" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L86-L90" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>pydantic.main.BaseModel</li>
@@ -283,7 +320,7 @@ allow <code>self</code> as a field name.</p></section>
 validated to form a valid model.</p>
 <p><code>__init__</code> uses <code>__pydantic_self__</code> instead of the more common <code>self</code> for the first arg to
 allow <code>self</code> as a field name.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L99-L106" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L120-L127" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>pydantic.main.BaseModel</li>
@@ -323,7 +360,7 @@ allow <code>self</code> as a field name.</p></section>
 validated to form a valid model.</p>
 <p><code>__init__</code> uses <code>__pydantic_self__</code> instead of the more common <code>self</code> for the first arg to
 allow <code>self</code> as a field name.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L79-L96" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L93-L117" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>pydantic.main.BaseModel</li>
@@ -347,6 +384,12 @@ allow <code>self</code> as a field name.</p></section>
 <dd>
 <section class="desc"></section>
 </dd>
+<dt id="pyatv.settings.RaopSettings.protocol_version"><code class="name">var <span class="ident">protocol_version</span> -> <a title="pyatv.settings.AirPlayVersion" href="#pyatv.settings.AirPlayVersion">AirPlayVersion</a> = auto</code></dt>
+<dd>
+<section class="desc"><p>Protocol version used.</p>
+<p>In reality this corresponds to the AirPlay version used. Set to 0 for automatic
+mode (recommended), or 1 or 2 for AirPlay 1 or 2 respectively.</p></section>
+</dd>
 <dt id="pyatv.settings.RaopSettings.timing_port"><code class="name">var <span class="ident">timing_port</span> -> int = 0</code></dt>
 <dd>
 <section class="desc"><p>Server side (UDP) port used by timing server.</p>
@@ -365,7 +408,7 @@ allow <code>self</code> as a field name.</p></section>
 validated to form a valid model.</p>
 <p><code>__init__</code> uses <code>__pydantic_self__</code> instead of the more common <code>self</code> for the first arg to
 allow <code>self</code> as a field name.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L109-L113" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L130-L134" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>pydantic_settings.main.BaseSettings</li>
diff --git a/docs/documentation/atvremote.md b/docs/documentation/atvremote.md
index 768f0a19..870d7456 100644
--- a/docs/documentation/atvremote.md
+++ b/docs/documentation/atvremote.md
@@ -389,6 +389,35 @@ like this:
 $ atvremote -s 10.0.10.84 unset_setting=protocols.raop.password
 ```
 
+As `atvremote` tries to interpolate the correct data type of input (e.g. it
+will try to interpret "1" as an integer), you might end up with issues if a
+setting expects a number as a string. One example is this:
+
+```raw
+protocols.raop.protocol_version = auto (AirPlayVersion)
+```
+
+`AirPlayVersion` can be either auto, 1 or 2. Trying to change to 1 yields
+an error:
+
+```raw
+$ atvremote -s 10.0.10.84 change_setting=protocols.raop.protocol_version,1
+Traceback (most recent call last):
+...
+protocol_version
+  Input should be a valid string [type=string_type, input_value=1, input_type=int]
+    For further information visit https://errors.pydantic.dev/2.1/v/string_type
+```
+
+It expects a string but `atvremote` automatically converts 1 to an integer. To
+circumvent this, you can force an argument to be treated as a string like this:
+
+```raw
+$ atvremote -s 10.0.10.81 'change_setting=protocols.raop.protocol_version,"1"'
+```
+
+
+
 ## Removing Settings
 
 To remove all settings for a device (reverting to defaults), run:
diff --git a/pyatv/protocols/airplay/__init__.py b/pyatv/protocols/airplay/__init__.py
index d7ab2dfb..4f225a3a 100644
--- a/pyatv/protocols/airplay/__init__.py
+++ b/pyatv/protocols/airplay/__init__.py
@@ -29,10 +29,7 @@ from pyatv.protocols import mrp
 from pyatv.protocols.airplay.ap2_session import AP2Session
 from pyatv.protocols.airplay.auth import extract_credentials
 from pyatv.protocols.airplay.mrp_connection import AirPlayMrpConnection
-from pyatv.protocols.airplay.pairing import (
-    AirPlayPairingHandler,
-    get_preferred_auth_type,
-)
+from pyatv.protocols.airplay.pairing import AirPlayPairingHandler
 from pyatv.protocols.airplay.player import AirPlayPlayer
 from pyatv.protocols.airplay.utils import (
     AirPlayFlags,
@@ -132,7 +129,7 @@ class AirPlayStream(Stream):  # pylint: disable=too-few-public-methods
                 str(self.config.address), self.service.port
             )
             rtsp = RtspSession(self._connection)
-            stream_protocol = AirPlayStream.create_airplay_protocol(self.service, rtsp)
+            stream_protocol = self.create_airplay_protocol(self.service, rtsp)
             player = AirPlayPlayer(rtsp, stream_protocol)
             position = int(kwargs.get("position", 0))
             self._play_task = asyncio.ensure_future(player.play_url(url, position))
@@ -147,9 +144,8 @@ class AirPlayStream(Stream):  # pylint: disable=too-few-public-methods
                 await server.close()
 
     # Should be included in a "common" module with shared AirPlay code
-    @staticmethod
     def create_airplay_protocol(
-        service: BaseService, rtsp: RtspSession
+        self, service: BaseService, rtsp: RtspSession
     ) -> StreamProtocol:
         """Create AirPlay protocol implementation based on supported version."""
         if service.protocol != Protocol.AirPlay:
@@ -158,7 +154,12 @@ class AirPlayStream(Stream):  # pylint: disable=too-few-public-methods
         context = StreamContext()
         context.credentials = parse_credentials(service.credentials)
 
-        if get_protocol_version(service) == AirPlayMajorVersion.AirPlayV1:
+        if (
+            get_protocol_version(
+                service, self.core.settings.protocols.raop.protocol_version
+            )
+            == AirPlayMajorVersion.AirPlayV1
+        ):
             return airplayv1.AirPlayV1(context, rtsp)
         return airplayv2.AirPlayV2(context, rtsp)
 
@@ -379,4 +380,10 @@ def setup(  # pylint: disable=too-many-locals
 
 def pair(core: Core, **kwargs) -> PairingHandler:
     """Return pairing handler for protocol."""
-    return AirPlayPairingHandler(core, get_preferred_auth_type(core.service), **kwargs)
+    return AirPlayPairingHandler(
+        core,
+        get_protocol_version(
+            core.service, core.settings.protocols.raop.protocol_version
+        ),
+        **kwargs,
+    )
diff --git a/pyatv/protocols/airplay/pairing.py b/pyatv/protocols/airplay/pairing.py
index 4aab0761..87e6fe2f 100644
--- a/pyatv/protocols/airplay/pairing.py
+++ b/pyatv/protocols/airplay/pairing.py
@@ -6,33 +6,25 @@ from pyatv import exceptions
 from pyatv.auth.hap_pairing import PairSetupProcedure
 from pyatv.const import Protocol
 from pyatv.core import Core
-from pyatv.interface import BaseService, PairingHandler
+from pyatv.interface import PairingHandler
 from pyatv.protocols.airplay.auth import AuthenticationType, pair_setup
-from pyatv.protocols.airplay.utils import AirPlayFlags, parse_features
+from pyatv.protocols.airplay.utils import AirPlayMajorVersion
 from pyatv.support import error_handler
 from pyatv.support.http import HttpConnection, http_connect
 
 _LOGGER = logging.getLogger(__name__)
 
 
-def get_preferred_auth_type(service: BaseService) -> AuthenticationType:
-    """Return the preferred authentication type depending on what is supported."""
-    features_string = service.properties.get("features", service.properties.get("ft"))
-    if features_string:
-        features = parse_features(features_string)
-        if AirPlayFlags.SupportsCoreUtilsPairingAndEncryption in features:
-            return AuthenticationType.HAP
-    return AuthenticationType.Legacy
-
-
 class AirPlayPairingHandler(PairingHandler):
     """Base class for API used to pair with an Apple TV."""
 
-    def __init__(self, core: Core, auth_type: AuthenticationType, **kwargs) -> None:
+    def __init__(
+        self, core: Core, airplay_version: AirPlayMajorVersion, **kwargs
+    ) -> None:
         """Initialize a new MrpPairingHandler."""
         super().__init__(core.session_manager, core.service)
         self._name: str = kwargs.get("name", core.settings.info.name)
-        self.auth_type = auth_type
+        self.airplay_version = airplay_version
         self.http: Optional[HttpConnection] = None
         self.address: str = str(core.config.address)
         self.pairing_procedure: Optional[PairSetupProcedure] = None
@@ -54,7 +46,12 @@ class AirPlayPairingHandler(PairingHandler):
     async def begin(self) -> None:
         """Start pairing process."""
         self.http = await http_connect(self.address, self.service.port)
-        self.pairing_procedure = pair_setup(self.auth_type, self.http)
+        self.pairing_procedure = pair_setup(
+            AuthenticationType.HAP
+            if self.airplay_version == AirPlayMajorVersion.AirPlayV2
+            else AuthenticationType.Legacy,
+            self.http,
+        )
         self._has_paired = False
         return await error_handler(
             self.pairing_procedure.start_pairing, exceptions.PairingError
diff --git a/pyatv/protocols/airplay/utils.py b/pyatv/protocols/airplay/utils.py
index 54c8e617..2167e5b8 100644
--- a/pyatv/protocols/airplay/utils.py
+++ b/pyatv/protocols/airplay/utils.py
@@ -14,6 +14,7 @@ from pyatv.auth.hap_pairing import (
 from pyatv.const import PairingRequirement
 from pyatv.core import MutableService
 from pyatv.interface import BaseService
+from pyatv.settings import AirPlayVersion
 from pyatv.support import map_range
 from pyatv.support.http import HttpRequest, HttpResponse
 
@@ -236,17 +237,23 @@ def log_response(logger, response: HttpResponse, message_prefix="") -> None:
 # so I'm guessing until I know better. The basic idea here is simple: the service
 # should have the "features" flag (either "features" or "ft") and either bit 38 or
 # bit 48 should be present.
-def get_protocol_version(service: BaseService) -> AirPlayMajorVersion:
+def get_protocol_version(
+    service: BaseService, preferred_version: AirPlayVersion
+) -> AirPlayMajorVersion:
     """Return major AirPlay version supported by a service."""
-    features = service.properties.get("ft")
-    if not features:
-        features = service.properties.get("features", "0x0")
-
-    parsed_features = parse_features(features)
-    if (
-        AirPlayFlags.SupportsUnifiedMediaControl in parsed_features
-        or AirPlayFlags.SupportsCoreUtilsPairingAndEncryption in parsed_features
-    ):
+    if preferred_version == AirPlayVersion.Auto:
+        features = service.properties.get("ft")
+        if not features:
+            features = service.properties.get("features", "0x0")
+
+        parsed_features = parse_features(features)
+        if (
+            AirPlayFlags.SupportsUnifiedMediaControl in parsed_features
+            or AirPlayFlags.SupportsCoreUtilsPairingAndEncryption in parsed_features
+        ):
+            return AirPlayMajorVersion.AirPlayV2
+        return AirPlayMajorVersion.AirPlayV1
+    if preferred_version == AirPlayVersion.V2:
         return AirPlayMajorVersion.AirPlayV2
     return AirPlayMajorVersion.AirPlayV1
 
diff --git a/pyatv/protocols/raop/__init__.py b/pyatv/protocols/raop/__init__.py
index 7cce3a50..bcde634f 100644
--- a/pyatv/protocols/raop/__init__.py
+++ b/pyatv/protocols/raop/__init__.py
@@ -44,10 +44,7 @@ from pyatv.interface import (
     Stream,
 )
 from pyatv.protocols.airplay.auth import extract_credentials
-from pyatv.protocols.airplay.pairing import (
-    AirPlayPairingHandler,
-    get_preferred_auth_type,
-)
+from pyatv.protocols.airplay.pairing import AirPlayPairingHandler
 from pyatv.protocols.airplay.utils import (
     AirPlayMajorVersion,
     dbfs_to_pct,
@@ -147,7 +144,9 @@ class RaopPlaybackManager:
         )
         self._rtsp = RtspSession(self._connection)
 
-        protocol_version = get_protocol_version(service)
+        protocol_version = get_protocol_version(
+            service, self.core.settings.protocols.raop.protocol_version
+        )
         _LOGGER.debug("Using AirPlay version %s", protocol_version)
 
         protocol_class = (
@@ -592,4 +591,10 @@ def setup(  # pylint: disable=too-many-locals
 
 def pair(core: Core, **kwargs) -> PairingHandler:
     """Return pairing handler for protocol."""
-    return AirPlayPairingHandler(core, get_preferred_auth_type(core.service), **kwargs)
+    return AirPlayPairingHandler(
+        core,
+        get_protocol_version(
+            core.service, core.settings.protocols.raop.protocol_version
+        ),
+        **kwargs,
+    )
diff --git a/pyatv/scripts/atvremote.py b/pyatv/scripts/atvremote.py
index cb808d5c..ff6db429 100644
--- a/pyatv/scripts/atvremote.py
+++ b/pyatv/scripts/atvremote.py
@@ -814,6 +814,10 @@ def _extract_command_with_args(cmd):
     """
 
     def _typeparse(value):
+        # Special case where input is forced to be treated as a string by quoting
+        if isinstance(value, str) and value.startswith('"') and value.endswith('"'):
+            return value[1:-1]
+
         try:
             return int(value)
         except ValueError:
diff --git a/pyatv/settings.py b/pyatv/settings.py
index 8fce3e52..a5f288ec 100644
--- a/pyatv/settings.py
+++ b/pyatv/settings.py
@@ -1,4 +1,5 @@
 """Settings for configuring pyatv."""
+from enum import Enum
 from typing import Optional
 
 from pydantic import BaseModel, Field
@@ -34,6 +35,19 @@ DEFAULT_OS_NAME = "iPhone OS"
 DEFAULT_OS_BUILD = "18G82"
 DEFAULT_OS_VERSION = "14.7.1"
 
+# pylint: disable=invalid-name
+
+
+class AirPlayVersion(str, Enum):
+    """AirPlay version to use."""
+
+    Auto = "auto"
+    V1 = "1"
+    V2 = "2"
+
+
+# pylint: enable=invalid-name
+
 
 class InfoSettings(BaseModel, extra="ignore"):  # type: ignore[call-arg]
     """Information related settings."""
@@ -83,6 +97,13 @@ class RaopSettings(BaseModel, extra="ignore"):  # type: ignore[call-arg]
     credentials: Optional[str] = None
     password: Optional[str] = None
 
+    protocol_version: AirPlayVersion = AirPlayVersion.Auto
+    """Protocol version used.
+
+    In reality this corresponds to the AirPlay version used. Set to 0 for automatic
+    mode (recommended), or 1 or 2 for AirPlay 1 or 2 respectively.
+    """
+
     timing_port: int = 0
     """Server side (UDP) port used by timing server.
 


Test patch:
diff --git a/tests/protocols/airplay/test_utils.py b/tests/protocols/airplay/test_utils.py
index 2e179d97..ceb3e557 100644
--- a/tests/protocols/airplay/test_utils.py
+++ b/tests/protocols/airplay/test_utils.py
@@ -17,6 +17,7 @@ from pyatv.protocols.airplay.utils import (
     is_remote_control_supported,
     parse_features,
 )
+from pyatv.settings import AirPlayVersion
 
 # These are not really valid credentials but parse_credentials accepts them (for now)
 HAP_CREDS = parse_credentials("aa:bb:cc:dd")
@@ -118,24 +119,46 @@ def test_is_remote_control_supported(props, credentials, expected_supported):
 
 
 @pytest.mark.parametrize(
-    "props, expected_version",
+    "props, preferred_version, expected_version",
     [
         # Fallback
-        ({}, AirPlayMajorVersion.AirPlayV1),
+        ({}, AirPlayVersion.Auto, AirPlayMajorVersion.AirPlayV1),
         # Used by RAOP
-        ({"ft": "0x5A7FFFF7,0xE"}, AirPlayMajorVersion.AirPlayV1),  # Apple TV 3
+        (
+            {"ft": "0x5A7FFFF7,0xE"},
+            AirPlayVersion.Auto,
+            AirPlayMajorVersion.AirPlayV1,
+        ),  # Apple TV 3
         (
             {"ft": "0x4A7FCA00,0xBC354BD0"},
+            AirPlayVersion.Auto,
             AirPlayMajorVersion.AirPlayV2,
         ),  # HomePod Mini
         # Used by AirPlay
-        ({"features": "0x5A7FFFF7,0xE"}, AirPlayMajorVersion.AirPlayV1),  # Apple TV 3
+        (
+            {"features": "0x5A7FFFF7,0xE"},
+            AirPlayVersion.Auto,
+            AirPlayMajorVersion.AirPlayV1,
+        ),  # Apple TV 3
+        # Should yield v1 but overridden with v2
+        (
+            {"features": "0x5A7FFFF7,0xE"},
+            AirPlayVersion.V2,
+            AirPlayMajorVersion.AirPlayV2,
+        ),
         (
             {"features": "0x4A7FCA00,0xBC354BD0"},
+            AirPlayVersion.Auto,
             AirPlayMajorVersion.AirPlayV2,
         ),  # HomePod Mini
+        # Should yield v2 both overridden with v1
+        (
+            {"features": "0x4A7FCA00,0xBC354BD0"},
+            AirPlayVersion.V1,
+            AirPlayMajorVersion.AirPlayV1,
+        ),
     ],
 )
-def test_get_protocol_version(props, expected_version):
+def test_get_protocol_version(props, preferred_version, expected_version):
     service = MutableService("id", Protocol.AirPlay, 0, props)
-    assert get_protocol_version(service) == expected_version
+    assert get_protocol_version(service, preferred_version) == expected_version