Repository: postlund/pyatv
Pull request #2516 (2024-10-05 16:22:45)

Add InputAction support to Companion Protocol for Hid Commands
@postlund I was able to get set up with gitpod, so everything here has been run through chickn. Thanks for your help with that.

I added handling of double tap and also changed the delay for Hold to a parameter with a default of one second.

I added tests that touch each of the hid commands with each InputAction. I tried to implement similar checks to the ones used in MRP to verify the actions themselves, but I couldn't get the timing check to work for Hold, so it would always clock them as single taps. If you have any advice on how to better handle the tests, or other improvements, let me know. 

postlund: This looks very good, thank you! I will try to merge this and add a comment regarding action not being verified, to deal with that later.

Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index fb77af9b..bd221707 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -112,7 +112,7 @@ jobs:
           python-version: ${{ env.PYTHON_VERSION }}
 
       - name: Download pre-built packages
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4.1.7
         with:
           name: packages
           path: dist
diff --git a/docs/api/pyatv.const.html b/docs/api/pyatv.const.html
index 8852e706..7ae5c65f 100644
--- a/docs/api/pyatv.const.html
+++ b/docs/api/pyatv.const.html
@@ -48,7 +48,7 @@ link_group: api
 </li>
 <li>
 <h4><code><a title="pyatv.const.FeatureName" href="#pyatv.const.FeatureName">FeatureName</a></code></h4>
-<ul class="two-column">
+<ul class="">
 <li><code><a title="pyatv.const.FeatureName.AccountList" href="#pyatv.const.FeatureName.AccountList">AccountList</a></code></li>
 <li><code><a title="pyatv.const.FeatureName.Action" href="#pyatv.const.FeatureName.Action">Action</a></code></li>
 <li><code><a title="pyatv.const.FeatureName.AddOutputDevices" href="#pyatv.const.FeatureName.AddOutputDevices">AddOutputDevices</a></code></li>
@@ -114,6 +114,7 @@ link_group: api
 <li><code><a title="pyatv.const.FeatureName.VolumeDown" href="#pyatv.const.FeatureName.VolumeDown">VolumeDown</a></code></li>
 <li><code><a title="pyatv.const.FeatureName.VolumeUp" href="#pyatv.const.FeatureName.VolumeUp">VolumeUp</a></code></li>
 <li><code><a title="pyatv.const.FeatureName.WakeUp" href="#pyatv.const.FeatureName.WakeUp">WakeUp</a></code></li>
+<li><code><a title="pyatv.const.FeatureName.iTunesStoreIdentifier" href="#pyatv.const.FeatureName.iTunesStoreIdentifier">iTunesStoreIdentifier</a></code></li>
 </ul>
 </li>
 <li>
@@ -223,7 +224,7 @@ link_group: api
 </header>
 <section id="section-intro">
 <p>Constants used in the public API.</p>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/const.py#L1-L454" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/const.py#L1-L457" class="git-link">Browse git</a></div>
 </section>
 <section>
 </section>
@@ -348,7 +349,7 @@ future.</p></section>
 </code></dt>
 <dd>
 <section class="desc"><p>All supported features.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/const.py#L249-L445" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/const.py#L249-L448" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>enum.Enum</li>
@@ -615,6 +616,10 @@ future.</p></section>
 <dd>
 <section class="desc"><p>Wake up device (deprecated; use Power.turn_on).</p></section>
 </dd>
+<dt id="pyatv.const.FeatureName.iTunesStoreIdentifier"><code class="name">var <span class="ident">iTunesStoreIdentifier</span> = 50</code></dt>
+<dd>
+<section class="desc"><p>iTunes Store Identifier for Content</p></section>
+</dd>
 </dl>
 </dd>
 <dt id="pyatv.const.FeatureState"><code class="flex name class">
@@ -931,7 +936,7 @@ is Apple TV Software (pre-tvOS).</p></section>
 </code></dt>
 <dd>
 <section class="desc"><p>Touch action constants.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/const.py#L448-L454" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/const.py#L451-L457" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>enum.Enum</li>
diff --git a/docs/api/pyatv.interface.html b/docs/api/pyatv.interface.html
index a216b5ce..b32de95f 100644
--- a/docs/api/pyatv.interface.html
+++ b/docs/api/pyatv.interface.html
@@ -213,7 +213,7 @@ link_group: api
 </li>
 <li>
 <h4><code><a title="pyatv.interface.Playing" href="#pyatv.interface.Playing">Playing</a></code></h4>
-<ul class="two-column">
+<ul class="">
 <li><code><a title="pyatv.interface.Playing.album" href="#pyatv.interface.Playing.album">album</a></code></li>
 <li><code><a title="pyatv.interface.Playing.artist" href="#pyatv.interface.Playing.artist">artist</a></code></li>
 <li><code><a title="pyatv.interface.Playing.content_identifier" href="#pyatv.interface.Playing.content_identifier">content_identifier</a></code></li>
@@ -221,6 +221,7 @@ link_group: api
 <li><code><a title="pyatv.interface.Playing.episode_number" href="#pyatv.interface.Playing.episode_number">episode_number</a></code></li>
 <li><code><a title="pyatv.interface.Playing.genre" href="#pyatv.interface.Playing.genre">genre</a></code></li>
 <li><code><a title="pyatv.interface.Playing.hash" href="#pyatv.interface.Playing.hash">hash</a></code></li>
+<li><code><a title="pyatv.interface.Playing.itunes_store_identifier" href="#pyatv.interface.Playing.itunes_store_identifier">itunes_store_identifier</a></code></li>
 <li><code><a title="pyatv.interface.Playing.media_type" href="#pyatv.interface.Playing.media_type">media_type</a></code></li>
 <li><code><a title="pyatv.interface.Playing.position" href="#pyatv.interface.Playing.position">position</a></code></li>
 <li><code><a title="pyatv.interface.Playing.repeat" href="#pyatv.interface.Playing.repeat">repeat</a></code></li>
@@ -345,7 +346,7 @@ link_group: api
 <p>Public interface exposed by library.</p>
 <p>This module contains all the interfaces that represents a generic Apple TV device and
 all its features.</p>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1-L1573" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1-L1586" class="git-link">Browse git</a></div>
 </section>
 <section>
 </section>
@@ -374,7 +375,7 @@ all its features.</p>
 <dd>
 <section class="desc"><p>Information about an app.</p>
 <p>Initialize a new App instance.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L680-L706" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L693-L719" class="git-link">Browse git</a></div>
 <h3>Instance variables</h3>
 <dl>
 <dt id="pyatv.interface.App.identifier"><code class="name">var <span class="ident">identifier</span> -> str</code></dt>
@@ -395,7 +396,7 @@ all its features.</p>
 <section class="desc"><p>Base class representing an Apple TV.</p>
 <p>Listener interface: <code>pyatv.interfaces.DeviceListener</code></p>
 <p>Initialize a new StateProducer instance.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1488-L1573" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1501-L1586" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -491,7 +492,7 @@ all its features.</p>
 </code></dt>
 <dd>
 <section class="desc"><p>Base class for app handling.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L709-L720" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L722-L733" class="git-link">Browse git</a></div>
 <h3>Subclasses</h3>
 <ul class="hlist">
 <li>pyatv.core.facade.FacadeApps</li>
@@ -565,7 +566,7 @@ all its features.</p>
 <p>Volume level is managed in percent where 0 is muted and 100 is max volume.</p>
 <p>Listener interface: <code>pyatv.interfaces.AudioListener</code></p>
 <p>Initialize a new StateProducer instance.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1138-L1207" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1151-L1220" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -682,7 +683,7 @@ possible and supported).</p></section>
 </code></dt>
 <dd>
 <section class="desc"><p>Listener interface for audio updates.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1122-L1135" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1135-L1148" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -721,7 +722,7 @@ possible and supported).</p></section>
 several services depending on the protocols it supports, e.g. DMAP or
 AirPlay.</p>
 <p>Initialize a new BaseConfig instance.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1294-L1441" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1307-L1454" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -903,7 +904,7 @@ original value).</p></section>
 <dd>
 <section class="desc"><p>General information about device.</p>
 <p>Initialize a new DeviceInfo instance.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L929-L1055" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L942-L1068" class="git-link">Browse git</a></div>
 <h3>Class variables</h3>
 <dl>
 <dt id="pyatv.interface.DeviceInfo.OUTPUT_DEVICE_ID"><code class="name">var <span class="ident">OUTPUT_DEVICE_ID</span></code></dt>
@@ -956,7 +957,7 @@ then this property contains the raw model string (if any is available).</p></sec
 </code></dt>
 <dd>
 <section class="desc"><p>Listener interface for generic device updates.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L881-L892" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L894-L905" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -1014,7 +1015,7 @@ then this property contains the raw model string (if any is available).</p></sec
 </code></dt>
 <dd>
 <section class="desc"><p>Base class for supported feature functionality.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1058-L1090" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1071-L1103" class="git-link">Browse git</a></div>
 <h3>Subclasses</h3>
 <ul class="hlist">
 <li>pyatv.core.facade.FacadeFeatures</li>
@@ -1064,7 +1065,7 @@ in one of the listed states.</p></section>
 <p>Listener
 interface: <code>pyatv.interfaces.KeyboardListener</code></p>
 <p>Initialize a new StateProducer instance.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1221-L1252" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1234-L1265" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -1140,7 +1141,7 @@ interface: <code>pyatv.interfaces.KeyboardListener</code></p>
 </code></dt>
 <dd>
 <section class="desc"><p>Listener interface for keyboard updates.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1210-L1218" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1223-L1231" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -1197,7 +1198,7 @@ interface: <code>pyatv.interfaces.KeyboardListener</code></p>
 </code></dt>
 <dd>
 <section class="desc"><p>Base class for retrieving metadata from an Apple TV.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L766-L806" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L779-L819" class="git-link">Browse git</a></div>
 <h3>Subclasses</h3>
 <ul class="hlist">
 <li>pyatv.core.facade.FacadeMetadata</li>
@@ -1259,7 +1260,7 @@ aspect ratio.</p></section>
 <dd>
 <section class="desc"><p>Information about an output device.</p>
 <p>Initialize a new OutputDevice instance.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1093-L1119" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1106-L1132" class="git-link">Browse git</a></div>
 <h3>Instance variables</h3>
 <dl>
 <dt id="pyatv.interface.OutputDevice.identifier"><code class="name">var <span class="ident">identifier</span> -> str</code></dt>
@@ -1345,12 +1346,12 @@ aspect ratio.</p></section>
 </dd>
 <dt id="pyatv.interface.Playing"><code class="flex name class">
 <span>class <span class="ident">Playing</span></span>
-<span>(</span><span>media_type: <a title="pyatv.const.MediaType" href="../const#pyatv.const.MediaType">MediaType</a> = MediaType.Unknown, device_state: <a title="pyatv.const.DeviceState" href="../const#pyatv.const.DeviceState">DeviceState</a> = 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[<a title="pyatv.const.ShuffleState" href="../const#pyatv.const.ShuffleState">ShuffleState</a>] = None, repeat: Optional[<a title="pyatv.const.RepeatState" href="../const#pyatv.const.RepeatState">RepeatState</a>] = 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)</span>
+<span>(</span><span>media_type: <a title="pyatv.const.MediaType" href="../const#pyatv.const.MediaType">MediaType</a> = MediaType.Unknown, device_state: <a title="pyatv.const.DeviceState" href="../const#pyatv.const.DeviceState">DeviceState</a> = 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[<a title="pyatv.const.ShuffleState" href="../const#pyatv.const.ShuffleState">ShuffleState</a>] = None, repeat: Optional[<a title="pyatv.const.RepeatState" href="../const#pyatv.const.RepeatState">RepeatState</a>] = 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)</span>
 </code></dt>
 <dd>
 <section class="desc"><p>Base class for retrieving what is currently playing.</p>
 <p>Initialize a new Playing instance.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L459-L677" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L459-L690" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -1387,6 +1388,10 @@ aspect ratio.</p></section>
 <p>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.</p></section>
 </dd>
+<dt id="pyatv.interface.Playing.itunes_store_identifier"><code class="name">var <span class="ident">itunes_store_identifier</span> -> Optional[int]</code></dt>
+<dd>
+<section class="desc"><p>Itunes Store identifier.</p></section>
+</dd>
 <dt id="pyatv.interface.Playing.media_type"><code class="name">var <span class="ident">media_type</span> -> <a title="pyatv.const.MediaType" href="../const#pyatv.const.MediaType">MediaType</a></code></dt>
 <dd>
 <section class="desc"><p>Type of media is currently playing, e.g. video, music.</p></section>
@@ -1429,7 +1434,7 @@ always be the same for the same content, but it is not guaranteed.</p></section>
 <section class="desc"><p>Base class for retrieving power state from an Apple TV.</p>
 <p>Listener interface: <code>pyatv.interfaces.PowerListener</code></p>
 <p>Initialize a new StateProducer instance.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L906-L926" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L919-L939" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -1482,7 +1487,7 @@ always be the same for the same content, but it is not guaranteed.</p></section>
 </code></dt>
 <dd>
 <section class="desc"><p>Listener interface for power updates.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L895-L903" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L908-L916" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -1510,7 +1515,7 @@ always be the same for the same content, but it is not guaranteed.</p></section>
 </code></dt>
 <dd>
 <section class="desc"><p>Listener interface for push updates.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L809-L818" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L822-L831" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -1551,7 +1556,7 @@ always be the same for the same content, but it is not guaranteed.</p></section>
 actually changes.</p>
 <p>Listener interface: <code><a title="pyatv.interface.PushListener" href="#pyatv.interface.PushListener">PushListener</a></code>.</p>
 <p>Initialize a new StateProducer instance.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L821-L848" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L834-L861" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -1951,7 +1956,7 @@ time interval is used, which is typically 10, 15, 30, etc. seconds.</p></section
 </code></dt>
 <dd>
 <section class="desc"><p>Base class for storage modules.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1444-L1485" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1457-L1498" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -2024,7 +2029,7 @@ the storage.</p></section>
 </code></dt>
 <dd>
 <section class="desc"><p>Base class for stream functionality.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L851-L878" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L864-L891" class="git-link">Browse git</a></div>
 <h3>Subclasses</h3>
 <ul class="hlist">
 <li>pyatv.core.facade.FacadeStream</li>
@@ -2074,7 +2079,7 @@ the storage.</p></section>
 </code></dt>
 <dd>
 <section class="desc"><p>Base class for touch gestures.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1255-L1291" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1268-L1304" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -2142,7 +2147,7 @@ in a given time (in milliseconds).</p>
 <dd>
 <section class="desc"><p>Information about a user account.</p>
 <p>Initialize a new UserAccount instance.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L723-L749" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L736-L762" class="git-link">Browse git</a></div>
 <h3>Instance variables</h3>
 <dl>
 <dt id="pyatv.interface.UserAccount.identifier"><code class="name">var <span class="ident">identifier</span> -> str</code></dt>
@@ -2160,7 +2165,7 @@ in a given time (in milliseconds).</p>
 </code></dt>
 <dd>
 <section class="desc"><p>Base class for account handling.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L752-L763" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L765-L776" class="git-link">Browse git</a></div>
 <h3>Subclasses</h3>
 <ul class="hlist">
 <li>pyatv.core.facade.FacadeUserAccounts</li>
diff --git a/pyatv/const.py b/pyatv/const.py
index e232fa3f..c10383c7 100644
--- a/pyatv/const.py
+++ b/pyatv/const.py
@@ -363,6 +363,9 @@ class FeatureName(Enum):
     ContentIdentifier = 47
     """Identifier for Content"""
 
+    iTunesStoreIdentifier = 50
+    """iTunes Store Identifier for Content"""
+
     AppList = 38
     """List of launchable apps."""
 
diff --git a/pyatv/interface.py b/pyatv/interface.py
index 336bf4a1..dae282b2 100644
--- a/pyatv/interface.py
+++ b/pyatv/interface.py
@@ -475,6 +475,7 @@ class Playing(ABC):
         "season_number",
         "episode_number",
         "content_identifier",
+        "itunes_store_identifier",
     ]
 
     def __init__(  # pylint: disable=too-many-locals
@@ -494,8 +495,10 @@ class Playing(ABC):
         season_number: Optional[int] = None,
         episode_number: Optional[int] = None,
         content_identifier: Optional[str] = None,
+        itunes_store_identifier: Optional[int] = None,
     ) -> None:
         """Initialize a new Playing instance."""
+        self._itunes_store_identifier = None
         self._media_type = media_type
         self._device_state = device_state
         self._title = title
@@ -511,6 +514,8 @@ class Playing(ABC):
         self._season_number = season_number
         self._episode_number = episode_number
         self._content_identifier = content_identifier
+        self._itunes_store_identifier = itunes_store_identifier
+
         self._post_process()
 
     def _post_process(self) -> None:
@@ -570,6 +575,8 @@ class Playing(ABC):
         if self.shuffle is not None:
             output.append(f"     Shuffle: {convert.shuffle_str(self.shuffle)}")
 
+        if self._itunes_store_identifier is not None:
+            output.append(f"iTunes Store Identifier: {self._itunes_store_identifier}")
         return "\n".join(output)
 
     def __eq__(self, other) -> bool:
@@ -676,6 +683,12 @@ class Playing(ABC):
         """Content identifier (app specific)."""
         return self._content_identifier
 
+    @property  # type: ignore
+    @feature(50, "iTunesStoreIdentifier", "iTunes Store identifier for Content")
+    def itunes_store_identifier(self) -> Optional[int]:
+        """Itunes Store identifier."""
+        return self._itunes_store_identifier
+
 
 class App:
     """Information about an app."""
diff --git a/pyatv/protocols/companion/__init__.py b/pyatv/protocols/companion/__init__.py
index 7d48dda7..1dd28140 100644
--- a/pyatv/protocols/companion/__init__.py
+++ b/pyatv/protocols/companion/__init__.py
@@ -378,12 +378,29 @@ class CompanionRemoteControl(RemoteControl):
         await self._press_button(HidCommand.Screensaver)
 
     async def _press_button(
-        self, command: HidCommand, action: InputAction = InputAction.SingleTap
+        self,
+        command: HidCommand,
+        action: InputAction = InputAction.SingleTap,
+        delay: float = 1,
     ) -> None:
-        if action != InputAction.SingleTap:
-            raise NotImplementedError(f"{action} not supported for {command} (yet)")
-        await self.api.hid_command(True, command)
-        await self.api.hid_command(False, command)
+        if action == InputAction.SingleTap:
+            await self.api.hid_command(True, command)
+            await self.api.hid_command(False, command)
+
+        elif action == InputAction.Hold:
+            await self.api.hid_command(True, command)
+            await asyncio.sleep(delay)
+            await self.api.hid_command(False, command)
+
+        elif action == InputAction.DoubleTap:
+            # First press
+            await self.api.hid_command(True, command)
+            await self.api.hid_command(False, command)
+            # Second press
+            await self.api.hid_command(True, command)
+            await self.api.hid_command(False, command)
+        else:
+            raise exceptions.NotSupportedError(f"unsupported input action: {action}")
 
 
 class CompanionAudio(Audio):
diff --git a/pyatv/protocols/mrp/__init__.py b/pyatv/protocols/mrp/__init__.py
index 685d2dea..750f5c61 100644
--- a/pyatv/protocols/mrp/__init__.py
+++ b/pyatv/protocols/mrp/__init__.py
@@ -142,6 +142,7 @@ _FIELD_FEATURES: Dict[FeatureName, str] = {
     FeatureName.SeasonNumber: "seasonNumber",
     FeatureName.EpisodeNumber: "episodeNumber",
     FeatureName.ContentIdentifier: "contentIdentifier",
+    FeatureName.iTunesStoreIdentifier: "iTunesStoreIdentifier",
 }
 
 DELAY_BETWEEN_COMMANDS = 0.1
@@ -267,6 +268,10 @@ def build_playing_instance(  # pylint: disable=too-many-locals
         """Content identifier."""
         return state.metadata_field("contentIdentifier")
 
+    def itunes_store_identifier() -> int:
+        """Itunes Store identifier."""
+        return state.metadata_field("iTunesStoreIdentifier")
+
     return Playing(
         media_type=media_type(),
         device_state=device_state(),
@@ -283,6 +288,7 @@ def build_playing_instance(  # pylint: disable=too-many-locals
         season_number=season_number(),
         episode_number=episode_number(),
         content_identifier=content_identifier(),
+        itunes_store_identifier=itunes_store_identifier(),
     )
 
 
diff --git a/pyatv/scripts/atvremote.py b/pyatv/scripts/atvremote.py
index 977c38c4..15f1a1b5 100644
--- a/pyatv/scripts/atvremote.py
+++ b/pyatv/scripts/atvremote.py
@@ -840,12 +840,12 @@ def _extract_command_with_args(cmd):
             "select",
             "menu",
             "home",
-            "touch_click",
+            "click",
         ]:
             return [InputAction(args[0])]
         if cmd == "set_volume":
             return [float(args[0])]
-        if cmd == "touch_action":
+        if cmd == "action":
             return [args[0], args[1], TouchAction(args[2])]
         return args
 
diff --git a/pyatv/support/opack.py b/pyatv/support/opack.py
index e177cfd2..6b4b13eb 100644
--- a/pyatv/support/opack.py
+++ b/pyatv/support/opack.py
@@ -86,17 +86,17 @@ def _pack(data, object_list):
             packed_bytes = (
                 bytes([0x91]) + len(data).to_bytes(1, byteorder="little") + data
             )
-        elif len(data) <= 0xFFFF:
+        elif len(data) <= 0xFFFF:  # 2^16-1
             packed_bytes = (
                 bytes([0x92]) + len(data).to_bytes(2, byteorder="little") + data
             )
-        elif len(data) <= 0xFFFFFF:
+        elif len(data) <= 0xFFFF_FFFF:  # 2^32-1
             packed_bytes = (
-                bytes([0x93]) + len(data).to_bytes(3, byteorder="little") + data
+                bytes([0x93]) + len(data).to_bytes(4, byteorder="little") + data
             )
-        elif len(data) <= 0xFFFFFFFF:
+        elif len(data) <= 0xFFFF_FFFF_FFFF_FFFF:  # 2^64-1
             packed_bytes = (
-                bytes([0x94]) + len(data).to_bytes(4, byteorder="little") + data
+                bytes([0x94]) + len(data).to_bytes(8, byteorder="little") + data
             )
     elif isinstance(data, list):
         packed_bytes = bytes([0xD0 + min(len(data), 0xF)]) + b"".join(
@@ -184,8 +184,8 @@ def _unpack(data, object_list):
     elif 0x70 <= data[0] <= 0x90:
         length = data[0] - 0x70
         value, remaining = data[1 : 1 + length], data[1 + length :]
-    elif 0x90 < data[0] <= 0x94:
-        noof_bytes = data[0] & 0xF
+    elif 0x91 <= data[0] <= 0x94:
+        noof_bytes = 1 << ((data[0] & 0xF) - 1)
         length = int.from_bytes(data[1 : 1 + noof_bytes], byteorder="little")
         value, remaining = (
             data[1 + noof_bytes : 1 + noof_bytes + length],


Test patch:
diff --git a/requirements/requirements_test.txt b/requirements/requirements_test.txt
index 5628eb87..74a29f69 100644
--- a/requirements/requirements_test.txt
+++ b/requirements/requirements_test.txt
@@ -5,7 +5,7 @@ isort==5.13.2
 mutagen==1.47.0
 pyfakefs==5.6.0
 pylint==3.2.6
-pytest==8.3.2
+pytest==8.3.3
 pytest-asyncio==0.24.0
 pytest-cov==5.0.0
 pytest-timeout==2.3.1
diff --git a/tests/fake_device/mrp.py b/tests/fake_device/mrp.py
index 5458a266..7eef03e9 100644
--- a/tests/fake_device/mrp.py
+++ b/tests/fake_device/mrp.py
@@ -118,6 +118,8 @@ def _fill_item(item, metadata):
         md.episodeNumber = metadata.episode_number
     if metadata.content_identifier:
         md.contentIdentifier = metadata.content_identifier
+    if metadata.itunes_store_identifier:
+        md.iTunesStoreIdentifier = metadata.itunes_store_identifier
 
 
 def _set_state_message(metadata, identifier):
@@ -194,6 +196,7 @@ class PlayingState:
         self.skip_time = kwargs.get("skip_time")
         self.app_name = kwargs.get("app_name")
         self.content_identifier = kwargs.get("content_identifier")
+        self.itunes_store_identifier = kwargs.get("itunes_store_identifier")
 
 
 class FakeMrpState:
diff --git a/tests/protocols/companion/test_companion_functional.py b/tests/protocols/companion/test_companion_functional.py
index a15d34f0..7b1a2763 100644
--- a/tests/protocols/companion/test_companion_functional.py
+++ b/tests/protocols/companion/test_companion_functional.py
@@ -218,6 +218,30 @@ async def test_remote_control_buttons(companion_client, companion_state, button)
     assert companion_state.latest_button == button
 
 
+@pytest.mark.parametrize(
+    "button,action",
+    [
+        ("up", InputAction.DoubleTap),
+        ("up", InputAction.Hold),
+        ("down", InputAction.DoubleTap),
+        ("down", InputAction.Hold),
+        ("left", InputAction.DoubleTap),
+        ("left", InputAction.Hold),
+        ("right", InputAction.DoubleTap),
+        ("right", InputAction.Hold),
+        ("select", InputAction.DoubleTap),
+        ("select", InputAction.Hold),
+        ("menu", InputAction.DoubleTap),
+        ("menu", InputAction.Hold),
+        ("home", InputAction.DoubleTap),
+        ("home", InputAction.Hold),
+    ],
+)
+async def test_button_actions(companion_client, companion_state, button, action):
+    await getattr(companion_client.remote_control, button)(action=action)
+    assert companion_state.latest_button == button
+
+
 async def test_remote_control_skip_forward_backward(companion_client, companion_state):
     duration = companion_state.duration
     await companion_client.remote_control.skip_forward()
diff --git a/tests/protocols/mrp/test_mrp_functional.py b/tests/protocols/mrp/test_mrp_functional.py
index d337a9f2..9082a96b 100644
--- a/tests/protocols/mrp/test_mrp_functional.py
+++ b/tests/protocols/mrp/test_mrp_functional.py
@@ -542,6 +542,7 @@ class MRPFunctionalTest(common_functional_tests.CommonFunctionalTests):
             season_number=12,
             episode_number=4,
             content_identifier="identifier",
+            itunes_store_identifier=123456789,
         )
 
         with faketime("pyatv", 0):
@@ -554,6 +555,7 @@ class MRPFunctionalTest(common_functional_tests.CommonFunctionalTests):
             self.assertEqual(playing.season_number, 12)
             self.assertEqual(playing.episode_number, 4)
             self.assertEqual(playing.content_identifier, "identifier")
+            self.assertEqual(playing.itunes_store_identifier, 123456789)
 
         self.assertFeatures(
             FeatureState.Available,
diff --git a/tests/scripts/test_atvscript.py b/tests/scripts/test_atvscript.py
index d083783c..f2be15ef 100644
--- a/tests/scripts/test_atvscript.py
+++ b/tests/scripts/test_atvscript.py
@@ -103,6 +103,7 @@ async def test_mrp_idle(scriptenv):
             "season_number": None,
             "episode_number": None,
             "content_identifier": None,
+            "itunes_store_identifier": None,
         },
     )
     assert exit_code == 0
diff --git a/tests/support/test_opack.py b/tests/support/test_opack.py
index e337d855..6ffe6b6b 100644
--- a/tests/support/test_opack.py
+++ b/tests/support/test_opack.py
@@ -84,6 +84,7 @@ def test_pack_short_raw_bytes():
 def test_pack_longer_raw_bytes():
     assert pack(33 * b"\x61") == b"\x91\x21" + (33 * b"\x61")
     assert pack(256 * b"\x61") == b"\x92\x00\x01" + (256 * b"\x61")
+    assert pack(65536 * b"\x61") == b"\x93\x00\x00\x01\x00" + (65536 * b"\x61")
 
 
 def test_pack_array():
@@ -275,6 +276,7 @@ def test_unpack_short_raw_bytes():
 def test_unpack_longer_raw_bytes():
     assert unpack(b"\x91\x21" + (33 * b"\x61")) == (33 * b"\x61", b"")
     assert unpack(b"\x92\x00\x01" + (256 * b"\x61")) == (256 * b"\x61", b"")
+    assert unpack(b"\x93\x00\x00\x01\x00" + (65536 * b"\x61")) == (65536 * b"\x61", b"")
 
 
 def test_unpack_array():
diff --git a/tests/test_interface.py b/tests/test_interface.py
index 3be339c7..a99d12d6 100644
--- a/tests/test_interface.py
+++ b/tests/test_interface.py
@@ -128,6 +128,7 @@ def test_playing_basic_fields():
             season_number=1245,
             episode_number=2468,
             content_identifier="content_id",
+            itunes_store_identifier=123456789,
         )
     )
     assert "mytitle" in out
@@ -138,6 +139,7 @@ def test_playing_basic_fields():
     assert "1245" in out
     assert "2468" in out
     assert "content_id" in out
+    assert "123456789" in out
 
 
 @pytest.mark.parametrize(
@@ -197,7 +199,7 @@ def test_playing_custom_hash():
 def test_playing_eq_ensure_member_count():
     # Fail if a property is added or removed to interface, just as a reminder to
     # update equality comparison
-    assert len(Playing().__dict__) == 15
+    assert len(Playing().__dict__) == 16
 
 
 @pytest.mark.parametrize(