Repository: postlund/pyatv
Pull request #2721 (2025-06-30 05:12:36)

build(deps): Bump mypy from 1.15.0 to 1.16.1 in /requirements
Bumps [mypy](https://github.com/python/mypy) from 1.15.0 to 1.16.1.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/python/mypy/blob/master/CHANGELOG.md">mypy's changelog</a>.</em></p>
<blockquote>
<h1>Mypy Release Notes</h1>
<h2>Next Release</h2>
<h3>Remove Support for targeting Python 3.8</h3>
<p>Mypy now requires <code>--python-version 3.9</code> or greater. Support for only Python 3.8 is
fully removed now. Given an unsupported version, mypy will default to the oldest
supported one, currently 3.9.</p>
<p>This change is necessary because typeshed stopped supporting Python 3.8 after it
reached its End of Life in October 2024.</p>
<p>Contributed by Marc Mueller
(PR <a href="https://redirect.github.com/python/mypy/pull/19157">19157</a>, PR <a href="https://redirect.github.com/python/mypy/pull/19162">19162</a>).</p>
<h3>Initial Support for Python 3.14</h3>
<p>Mypy is now tested on 3.14 and mypyc works with 3.14.0b3 and later.
Mypyc compiled wheels of mypy itself will be available for new versions after 3.14.0rc1 is released.</p>
<p>Note that not all new features might be supported just yet.</p>
<p>Contributed by Marc Mueller (PR <a href="https://redirect.github.com/python/mypy/pull/19164">19164</a>)</p>
<h3>Deprecated Flag: --force-uppercase-builtins</h3>
<p>Mypy only supports Python 3.9+. The --force-uppercase-builtins flag is now deprecated and a no-op. It will be removed in a future version.</p>
<p>Contributed by Marc Mueller (PR <a href="https://redirect.github.com/python/mypy/pull/19176">19176</a>)</p>
<h2>Mypy 1.16</h2>
<p>We’ve just uploaded mypy 1.16 to the Python Package Index (<a href="https://pypi.org/project/mypy/">PyPI</a>).
Mypy is a static type checker for Python. This release includes new features and bug fixes.
You can install it as follows:</p>
<pre><code>python3 -m pip install -U mypy
</code></pre>
<p>You can read the full documentation for this release on <a href="http://mypy.readthedocs.io">Read the Docs</a>.</p>
<h3>Different Property Getter and Setter Types</h3>
<p>Mypy now supports using different types for a property getter and setter:</p>
<pre lang="python"><code>class A:
    _value: int
<pre><code>@property
</code></pre>
<p>&lt;/tr&gt;&lt;/table&gt;
</code></pre></p>
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/python/mypy/commit/68b8fa097d080c92d30a429bc74de8acd56caf85"><code>68b8fa0</code></a> Bump version to 1.16.1</li>
<li><a href="https://github.com/python/mypy/commit/e253eded9c887630f3f5404c4b9f73f13570476a"><code>e253ede</code></a> Single underscore is not a sunder (<a href="https://redirect.github.com/python/mypy/issues/19273">#19273</a>)</li>
<li><a href="https://github.com/python/mypy/commit/9fb5ff66c51bd971d7a6b1260cc0ec9f1b82cc06"><code>9fb5ff6</code></a> Fix properties with setters after deleters (<a href="https://redirect.github.com/python/mypy/issues/19248">#19248</a>)</li>
<li><a href="https://github.com/python/mypy/commit/c20fd7838338cd65d6c7c6e252eda85996cfc98e"><code>c20fd78</code></a> Handle assignment of bound methods in class bodies (<a href="https://redirect.github.com/python/mypy/issues/19233">#19233</a>)</li>
<li><a href="https://github.com/python/mypy/commit/c86480ce51e4bb6db21f4b3f0b3ec8833aafc8ce"><code>c86480c</code></a> Tighten metaclass <strong>call</strong> handling in protocols (<a href="https://redirect.github.com/python/mypy/issues/19191">#19191</a>)</li>
<li><a href="https://github.com/python/mypy/commit/cb3c6ec6a7aaa96a0e26768a946ac63ea14115f2"><code>cb3c6ec</code></a> Fix crash on partial type used as context (<a href="https://redirect.github.com/python/mypy/issues/19216">#19216</a>)</li>
<li><a href="https://github.com/python/mypy/commit/c39f5e73c47182e51c5d8d488f7cc7301257c974"><code>c39f5e7</code></a> [mypyc] Fixing condition for handling user-defined <strong>del</strong> (<a href="https://redirect.github.com/python/mypy/issues/19188">#19188</a>)</li>
<li><a href="https://github.com/python/mypy/commit/0a4f28431faa18e59d35bc269cb0ea6c00810653"><code>0a4f284</code></a> Fix crash on invalid property inside its own body (<a href="https://redirect.github.com/python/mypy/issues/19208">#19208</a>)</li>
<li><a href="https://github.com/python/mypy/commit/9b079f6592740a51c0e629728eeb0324ad85126f"><code>9b079f6</code></a> Bump version to 1.16.1+dev</li>
<li><a href="https://github.com/python/mypy/commit/9e72e9601f4c2fb6866cfec98fc40a31c91ccdb0"><code>9e72e96</code></a> Update version to 1.16.0</li>
<li>Additional commits viewable in <a href="https://github.com/python/mypy/compare/v1.15.0...v1.16.1">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=mypy&package-manager=pip&previous-version=1.15.0&new-version=1.16.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/docs/api/pyatv.settings.html b/docs/api/pyatv.settings.html
index 941800a5..9a6dd245 100644
--- a/docs/api/pyatv.settings.html
+++ b/docs/api/pyatv.settings.html
@@ -54,6 +54,7 @@ This module has additional documentation
 <h4><code><a title="pyatv.settings.InfoSettings" href="#pyatv.settings.InfoSettings">InfoSettings</a></code></h4>
 <ul class="two-column">
 <li><code><a title="pyatv.settings.InfoSettings.device_id" href="#pyatv.settings.InfoSettings.device_id">device_id</a></code></li>
+<li><code><a title="pyatv.settings.InfoSettings.fill_missing_rp_id" href="#pyatv.settings.InfoSettings.fill_missing_rp_id">fill_missing_rp_id</a></code></li>
 <li><code><a title="pyatv.settings.InfoSettings.mac" href="#pyatv.settings.InfoSettings.mac">mac</a></code></li>
 <li><code><a title="pyatv.settings.InfoSettings.mac_validator" href="#pyatv.settings.InfoSettings.mac_validator">mac_validator</a></code></li>
 <li><code><a title="pyatv.settings.InfoSettings.model" href="#pyatv.settings.InfoSettings.model">model</a></code></li>
@@ -61,6 +62,7 @@ This module has additional documentation
 <li><code><a title="pyatv.settings.InfoSettings.os_build" href="#pyatv.settings.InfoSettings.os_build">os_build</a></code></li>
 <li><code><a title="pyatv.settings.InfoSettings.os_name" href="#pyatv.settings.InfoSettings.os_name">os_name</a></code></li>
 <li><code><a title="pyatv.settings.InfoSettings.os_version" href="#pyatv.settings.InfoSettings.os_version">os_version</a></code></li>
+<li><code><a title="pyatv.settings.InfoSettings.rp_id" href="#pyatv.settings.InfoSettings.rp_id">rp_id</a></code></li>
 </ul>
 </li>
 <li>
@@ -116,7 +118,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-L168" 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-L179" class="git-link">Browse git</a></div>
 </section>
 <section>
 </section>
@@ -135,7 +137,7 @@ This module has additional documentation
 <section class="desc"><p>Settings related to AirPlay.</p>
 <p>Create a new model by parsing and validating input data from keyword arguments.</p>
 <p>Raises ValidationError if the input data cannot be parsed to form a valid model.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L96-L103" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L107-L114" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>pydantic.v1.main.BaseModel</li>
@@ -167,7 +169,7 @@ This module has additional documentation
 </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#L47-L57" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L49-L59" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>builtins.str</li>
@@ -197,7 +199,7 @@ This module has additional documentation
 <section class="desc"><p>Settings related to Companion.</p>
 <p>Create a new model by parsing and validating input data from keyword arguments.</p>
 <p>Raises ValidationError if the input data cannot be parsed to form a valid model.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L106-L110" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L117-L121" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>pydantic.v1.main.BaseModel</li>
@@ -223,7 +225,7 @@ This module has additional documentation
 <section class="desc"><p>Settings related to DMAP.</p>
 <p>Create a new model by parsing and validating input data from keyword arguments.</p>
 <p>Raises ValidationError if the input data cannot be parsed to form a valid model.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L113-L117" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L124-L128" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>pydantic.v1.main.BaseModel</li>
@@ -249,7 +251,7 @@ This module has additional documentation
 <section class="desc"><p>Information related settings.</p>
 <p>Create a new model by parsing and validating input data from keyword arguments.</p>
 <p>Raises ValidationError if the input data cannot be parsed to form a valid model.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L76-L93" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L78-L104" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>pydantic.v1.main.BaseModel</li>
@@ -285,9 +287,21 @@ This module has additional documentation
 <dd>
 <section class="desc"><p>The type of the None singleton.</p></section>
 </dd>
+<dt id="pyatv.settings.InfoSettings.rp_id"><code class="name">var <span class="ident">rp_id</span> -> str | None</code></dt>
+<dd>
+<section class="desc"><p>The type of the None singleton.</p></section>
+</dd>
 </dl>
 <h3>Static methods</h3>
 <dl>
+<dt id="pyatv.settings.InfoSettings.fill_missing_rp_id">
+<code class="name flex">
+<span>def <span class="ident">fill_missing_rp_id</span></span>(<span>v)</span>
+</code>
+</dt>
+<dd>
+<section class="desc"><p>Generate a new random rp_id if it is missing.</p></section>
+</dd>
 <dt id="pyatv.settings.InfoSettings.mac_validator">
 <code class="name flex">
 <span>def <span class="ident">mac_validator</span></span>(<span>mac: str) -> str</span>
@@ -306,7 +320,7 @@ This module has additional documentation
 <section class="desc"><p>Settings related to MRP.</p>
 <p>Create a new model by parsing and validating input data from keyword arguments.</p>
 <p>Raises ValidationError if the input data cannot be parsed to form a valid model.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L120-L124" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L131-L135" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>pydantic.v1.main.BaseModel</li>
@@ -330,7 +344,7 @@ This module has additional documentation
 </code></dt>
 <dd>
 <section class="desc"><p>How MRP tunneling over AirPlay is handled.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L60-L70" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L62-L72" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>builtins.str</li>
@@ -360,7 +374,7 @@ This module has additional documentation
 <section class="desc"><p>Container for protocol specific settings.</p>
 <p>Create a new model by parsing and validating input data from keyword arguments.</p>
 <p>Raises ValidationError if the input data cannot be parsed to form a valid model.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L154-L161" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L165-L172" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>pydantic.v1.main.BaseModel</li>
@@ -398,7 +412,7 @@ This module has additional documentation
 <section class="desc"><p>Settings related to RAOP.</p>
 <p>Create a new model by parsing and validating input data from keyword arguments.</p>
 <p>Raises ValidationError if the input data cannot be parsed to form a valid model.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L127-L151" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L138-L162" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>pydantic.v1.main.BaseModel</li>
@@ -444,7 +458,7 @@ mode (recommended), or 1 or 2 for AirPlay 1 or 2 respectively.</p></section>
 <section class="desc"><p>Settings container class.</p>
 <p>Create a new model by parsing and validating input data from keyword arguments.</p>
 <p>Raises ValidationError if the input data cannot be parsed to form a valid model.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L164-L168" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/settings.py#L175-L179" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>pydantic.v1.main.BaseModel</li>
diff --git a/docs/api/pyatv.storage.file_storage.html b/docs/api/pyatv.storage.file_storage.html
index 47bd205f..1c8b038e 100644
--- a/docs/api/pyatv.storage.file_storage.html
+++ b/docs/api/pyatv.storage.file_storage.html
@@ -35,7 +35,7 @@ This module has additional documentation
 </header>
 <section id="section-intro">
 <p>File based storage module.</p>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/file_storage.py#L1-L69" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/file_storage.py#L1-L70" class="git-link">Browse git</a></div>
 </section>
 <section>
 </section>
@@ -53,7 +53,7 @@ This module has additional documentation
 <dd>
 <section class="desc"><p>Storage module storing settings in a file.</p>
 <p>Initialize a new FileStorage instance.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/file_storage.py#L16-L69" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/file_storage.py#L16-L70" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li><a title="pyatv.storage.AbstractStorage" href="#pyatv.storage.AbstractStorage">AbstractStorage</a></li>
@@ -81,14 +81,14 @@ on Windows).</p></section>
 <ul class="hlist">
 <li><code><b><a title="pyatv.storage.AbstractStorage" href="#pyatv.storage.AbstractStorage">AbstractStorage</a></b></code>:
 <ul class="hlist">
-<li><code><a title="pyatv.storage.AbstractStorage.changed" href="#pyatv.storage.AbstractStorage.changed">changed</a></code></li>
 <li><code><a title="pyatv.storage.AbstractStorage.get_settings" href="#pyatv.storage.AbstractStorage.get_settings">get_settings</a></code></li>
+<li><code><a title="pyatv.storage.AbstractStorage.has_changed" href="#pyatv.storage.AbstractStorage.has_changed">has_changed</a></code></li>
 <li><code><a title="pyatv.storage.AbstractStorage.load" href="../../interface#pyatv.interface.Storage.load">load</a></code></li>
-<li><code><a title="pyatv.storage.AbstractStorage.mark_as_saved" href="#pyatv.storage.AbstractStorage.mark_as_saved">mark_as_saved</a></code></li>
 <li><code><a title="pyatv.storage.AbstractStorage.remove_settings" href="../../interface#pyatv.interface.Storage.remove_settings">remove_settings</a></code></li>
 <li><code><a title="pyatv.storage.AbstractStorage.save" href="../../interface#pyatv.interface.Storage.save">save</a></code></li>
 <li><code><a title="pyatv.storage.AbstractStorage.settings" href="../../interface#pyatv.interface.Storage.settings">settings</a></code></li>
 <li><code><a title="pyatv.storage.AbstractStorage.storage_model" href="#pyatv.storage.AbstractStorage.storage_model">storage_model</a></code></li>
+<li><code><a title="pyatv.storage.AbstractStorage.update_hash" href="#pyatv.storage.AbstractStorage.update_hash">update_hash</a></code></li>
 <li><code><a title="pyatv.storage.AbstractStorage.update_settings" href="../../interface#pyatv.interface.Storage.update_settings">update_settings</a></code></li>
 </ul>
 </li>
diff --git a/docs/api/pyatv.storage.html b/docs/api/pyatv.storage.html
index 74e062a9..7f7151a1 100644
--- a/docs/api/pyatv.storage.html
+++ b/docs/api/pyatv.storage.html
@@ -28,10 +28,10 @@ This module has additional documentation
 <li>
 <h4><code><a title="pyatv.storage.AbstractStorage" href="#pyatv.storage.AbstractStorage">AbstractStorage</a></code></h4>
 <ul class="">
-<li><code><a title="pyatv.storage.AbstractStorage.changed" href="#pyatv.storage.AbstractStorage.changed">changed</a></code></li>
 <li><code><a title="pyatv.storage.AbstractStorage.get_settings" href="#pyatv.storage.AbstractStorage.get_settings">get_settings</a></code></li>
-<li><code><a title="pyatv.storage.AbstractStorage.mark_as_saved" href="#pyatv.storage.AbstractStorage.mark_as_saved">mark_as_saved</a></code></li>
+<li><code><a title="pyatv.storage.AbstractStorage.has_changed" href="#pyatv.storage.AbstractStorage.has_changed">has_changed</a></code></li>
 <li><code><a title="pyatv.storage.AbstractStorage.storage_model" href="#pyatv.storage.AbstractStorage.storage_model">storage_model</a></code></li>
+<li><code><a title="pyatv.storage.AbstractStorage.update_hash" href="#pyatv.storage.AbstractStorage.update_hash">update_hash</a></code></li>
 </ul>
 </li>
 <li>
@@ -51,7 +51,7 @@ This module has additional documentation
 </header>
 <section id="section-intro">
 <p>Storage module.</p>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/__init__.py#L1-L175" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/__init__.py#L1-L177" class="git-link">Browse git</a></div>
 </section>
 <section>
 <h2 class="section-title" id="header-submodules">Sub-modules</h2>
@@ -81,7 +81,7 @@ This module has additional documentation
 <p>New storage modules should generally inherit from this class and implement save and
 load according to the underlying storage mechanism.</p>
 <p>Initialize a new AbstractStorage instance.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/__init__.py#L42-L175" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/__init__.py#L41-L177" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li><a title="pyatv.interface.Storage" href="../../interface#pyatv.interface.Storage">Storage</a></li>
@@ -94,18 +94,10 @@ load according to the underlying storage mechanism.</p>
 </ul>
 <h3>Instance variables</h3>
 <dl>
-<dt id="pyatv.storage.AbstractStorage.changed"><code class="name">var <span class="ident">changed</span> -> bool</code></dt>
-<dd>
-<section class="desc"><p>Return if anything has changed in the model since loading.</p>
-<p>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).</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/__init__.py#L54-L62" class="git-link">Browse git</a></div>
-</dd>
 <dt id="pyatv.storage.AbstractStorage.storage_model"><code class="name">var <span class="ident">storage_model</span> -> <a title="pyatv.storage.StorageModel" href="#pyatv.storage.StorageModel">StorageModel</a></code></dt>
 <dd>
 <section class="desc"><p>Return storage model representation.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/__init__.py#L69-L72" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/__init__.py#L66-L69" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 <h3>Methods</h3>
@@ -124,18 +116,27 @@ automatically and returned. If the configuration does not contain any valid
 identitiers, DeviceIdMissingError will be raised.</p>
 <p>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.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/__init__.py#L89-L122" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/__init__.py#L82-L115" class="git-link">Browse git</a></div>
+</dd>
+<dt id="pyatv.storage.AbstractStorage.has_changed">
+<code class="name flex">
+<span>def <span class="ident">has_changed</span></span>(<span>self, data: dict) -> bool</span>
+</code>
+</dt>
+<dd>
+<section class="desc"><p>Return if anything has changed in the model since loading.</p>
+<p>This method compares a hash of the saved data with the provided data to deduce
+if anything has changed.</p></section>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/__init__.py#L53-L59" class="git-link">Browse git</a></div>
 </dd>
-<dt id="pyatv.storage.AbstractStorage.mark_as_saved">
+<dt id="pyatv.storage.AbstractStorage.update_hash">
 <code class="name flex">
-<span>def <span class="ident">mark_as_saved</span></span>(<span>self) -> None</span>
+<span>def <span class="ident">update_hash</span></span>(<span>self, data: dict) -> None</span>
 </code>
 </dt>
 <dd>
-<section class="desc"><p>Call after saving to indicate settings have been saved.</p>
-<p>The changed property reflects whether something has been changed in the model
-or not based on calling this method.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/__init__.py#L81-L87" class="git-link">Browse git</a></div>
+<section class="desc"><p>Call after saving to indicate settings have been saved.</p></section>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/__init__.py#L78-L80" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 <h3>Inherited members</h3>
@@ -159,7 +160,7 @@ or not based on calling this method.</p></section>
 <section class="desc"><p>Storage model of data that is saved or restored to underlying storage.</p>
 <p>Create a new model by parsing and validating input data from keyword arguments.</p>
 <p>Raises ValidationError if the input data cannot be parsed to form a valid model.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/__init__.py#L35-L39" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/storage/__init__.py#L34-L38" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>pydantic.v1.main.BaseModel</li>
diff --git a/docs/api/pyatv.storage.memory_storage.html b/docs/api/pyatv.storage.memory_storage.html
index 3f55c58e..c9fb3da9 100644
--- a/docs/api/pyatv.storage.memory_storage.html
+++ b/docs/api/pyatv.storage.memory_storage.html
@@ -62,14 +62,14 @@ be forgotten when restarting the python interpreter.</p>
 <ul class="hlist">
 <li><code><b><a title="pyatv.storage.AbstractStorage" href="#pyatv.storage.AbstractStorage">AbstractStorage</a></b></code>:
 <ul class="hlist">
-<li><code><a title="pyatv.storage.AbstractStorage.changed" href="#pyatv.storage.AbstractStorage.changed">changed</a></code></li>
 <li><code><a title="pyatv.storage.AbstractStorage.get_settings" href="#pyatv.storage.AbstractStorage.get_settings">get_settings</a></code></li>
+<li><code><a title="pyatv.storage.AbstractStorage.has_changed" href="#pyatv.storage.AbstractStorage.has_changed">has_changed</a></code></li>
 <li><code><a title="pyatv.storage.AbstractStorage.load" href="../../interface#pyatv.interface.Storage.load">load</a></code></li>
-<li><code><a title="pyatv.storage.AbstractStorage.mark_as_saved" href="#pyatv.storage.AbstractStorage.mark_as_saved">mark_as_saved</a></code></li>
 <li><code><a title="pyatv.storage.AbstractStorage.remove_settings" href="../../interface#pyatv.interface.Storage.remove_settings">remove_settings</a></code></li>
 <li><code><a title="pyatv.storage.AbstractStorage.save" href="../../interface#pyatv.interface.Storage.save">save</a></code></li>
 <li><code><a title="pyatv.storage.AbstractStorage.settings" href="../../interface#pyatv.interface.Storage.settings">settings</a></code></li>
 <li><code><a title="pyatv.storage.AbstractStorage.storage_model" href="#pyatv.storage.AbstractStorage.storage_model">storage_model</a></code></li>
+<li><code><a title="pyatv.storage.AbstractStorage.update_hash" href="#pyatv.storage.AbstractStorage.update_hash">update_hash</a></code></li>
 <li><code><a title="pyatv.storage.AbstractStorage.update_settings" href="../../interface#pyatv.interface.Storage.update_settings">update_settings</a></code></li>
 </ul>
 </li>
diff --git a/pyatv/protocols/companion/api.py b/pyatv/protocols/companion/api.py
index 0adf6e27..d8cd3f0c 100644
--- a/pyatv/protocols/companion/api.py
+++ b/pyatv/protocols/companion/api.py
@@ -196,7 +196,7 @@ class CompanionAPI(
                 "_bf": 0,
                 "_cf": 512,
                 "_clFl": 128,
-                "_i": "cafecafecafe",  # TODO: Figure out what to put here
+                "_i": info.rp_id,
                 "_idsID": creds.client_id,
                 # Not really device id here, but better then anything...
                 "_pubID": info.device_id,
diff --git a/pyatv/settings.py b/pyatv/settings.py
index 999fd689..6acc8f00 100644
--- a/pyatv/settings.py
+++ b/pyatv/settings.py
@@ -1,6 +1,7 @@
 """Settings for configuring pyatv."""
 
 from enum import Enum
+import os
 import re
 from typing import Optional
 
@@ -36,6 +37,7 @@ _MAC_REGEX = r"[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}"
 DEFAULT_NAME = "pyatv"
 DEFUALT_MAC = "02:70:79:61:74:76"  # Locally administrated (02) + "pyatv" in hex
 DEFAULT_DEVICE_ID = "FF:70:79:61:74:76"  # 0xFF + "pyatv"
+DEFAULT_RP_ID = "cafecafecafe"
 DEFAULT_MODEL = "iPhone10,6"
 DEFAULT_OS_NAME = "iPhone OS"
 DEFAULT_OS_BUILD = "18G82"
@@ -80,6 +82,7 @@ class InfoSettings(BaseModel, extra="ignore"):  # type: ignore[call-arg]
     mac: str = DEFUALT_MAC
     model: str = DEFAULT_MODEL
     device_id: str = DEFAULT_DEVICE_ID
+    rp_id: Optional[str] = None
     os_name: str = DEFAULT_OS_NAME
     os_build: str = DEFAULT_OS_BUILD
     os_version: str = DEFAULT_OS_VERSION
@@ -92,6 +95,14 @@ class InfoSettings(BaseModel, extra="ignore"):  # type: ignore[call-arg]
             raise ValueError(f"{mac} is not a valid MAC address")
         return mac
 
+    @field_validator("rp_id", always=True)
+    @classmethod
+    def fill_missing_rp_id(cls, v):
+        """Generate a new random rp_id if it is missing."""
+        if v is None:
+            return os.urandom(6).hex()
+        return v
+
 
 class AirPlaySettings(BaseModel, extra="ignore"):  # type: ignore[call-arg]
     """Settings related to AirPlay."""
diff --git a/pyatv/storage/__init__.py b/pyatv/storage/__init__.py
index 741f86c5..2718ded1 100644
--- a/pyatv/storage/__init__.py
+++ b/pyatv/storage/__init__.py
@@ -1,7 +1,8 @@
 """Storage module."""
 
 from hashlib import sha256
-from typing import List, Sequence
+import json
+from typing import Any, Iterator, List, Sequence, Tuple
 
 from pyatv.const import Protocol
 from pyatv.exceptions import DeviceIdMissingError, SettingsError
@@ -19,14 +20,12 @@ __pdoc__ = {
 MODEL_VERSION = 1
 
 
-def _calculate_settings_hash(settings: List[Settings]) -> str:
+def _dict_hash(data: dict) -> str:
     # Calculate a hash of all settings by dumping content to JSON and hashing all
     # output using SHA256 (arbitrarily chosen for now). This is not very efficient
     # (especially when making multiple calls) but should be reliable and good enough.
     hasher = sha256()
-    for setting in settings:
-        setting_json = setting.json(exclude_defaults=True)
-        hasher.update(setting_json.encode("utf-8"))
+    hasher.update(json.dumps(data).encode("utf-8"))
     return hasher.hexdigest()
 
 
@@ -49,17 +48,15 @@ class AbstractStorage(Storage):
     def __init__(self) -> None:
         """Initialize a new AbstractStorage instance."""
         self._settings: List[Settings] = []
-        self._settings_hash: str = _calculate_settings_hash(self._settings)
+        self._hash: str = _dict_hash({})
 
-    @property
-    def changed(self) -> bool:
+    def has_changed(self, data: dict) -> bool:
         """Return if anything has changed in the model since loading.
 
-        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).
+        This method compares a hash of the saved data with the provided data to deduce
+        if anything has changed.
         """
-        return self._settings_hash != _calculate_settings_hash(self._settings)
+        return self._hash != _dict_hash(data)
 
     @property
     def settings(self) -> Sequence[Settings]:
@@ -78,13 +75,9 @@ class AbstractStorage(Storage):
             raise SettingsError(f"unsupported version: {other.version}")
         self._settings = other.devices
 
-    def mark_as_saved(self) -> None:
-        """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.
-        """
-        self._settings_hash = _calculate_settings_hash(self._settings)
+    def update_hash(self, data: dict) -> None:
+        """Call after saving to indicate settings have been saved."""
+        self._hash = _dict_hash(data)
 
     async def get_settings(self, config: BaseConfig) -> Settings:
         """Return settings for a specific configuration (device).
@@ -170,6 +163,15 @@ class AbstractStorage(Storage):
                 )
                 settings.protocols.raop.identifier = service.identifier
 
+    def __iter__(self) -> Iterator[Tuple[str, Any]]:
+        """Iterate over stored devices."""
+        # If settings are empty for a device (e.e. no settings overridden or credentials
+        # saved), then the output will just be an empty dict. To not pollute the output
+        # with those, we do some filtering here.
+        dumped = self.storage_model.dict(exclude_defaults=True)
+        dumped["devices"] = [device for device in dumped["devices"] if device != {}]
+        return iter(dumped.items())
+
     def __repr__(self) -> str:
         """Return representation of MemoryStorage."""
         return str(self)
diff --git a/pyatv/storage/file_storage.py b/pyatv/storage/file_storage.py
index 22c2fa1c..5fb05355 100644
--- a/pyatv/storage/file_storage.py
+++ b/pyatv/storage/file_storage.py
@@ -37,18 +37,13 @@ class FileStorage(AbstractStorage):
 
     async def save(self) -> None:
         """Save settings to active storage."""
-        if self.changed:
+        dumped = dict(self)
+        if self.has_changed(dumped):
             _LOGGER.debug("Saving settings to %s", self._filename)
-            await self._loop.run_in_executor(None, self._save_file)
-            self.mark_as_saved()
-
-    def _save_file(self) -> None:
-        # If settings are empty for a device (e.e. no settings overridden or credentials
-        # saved), then the output will just be an empty dict. To not pollute the output
-        # with those, we do some filtering here.
-        dumped = self.storage_model.dict(exclude_defaults=True)
-        dumped["devices"] = [device for device in dumped["devices"] if device != {}]
+            await self._loop.run_in_executor(None, self._save_file, dumped)
+            self.update_hash(dumped)
 
+    def _save_file(self, dumped: dict) -> None:
         with open(self._filename, "w", encoding="utf-8") as _fh:
             _fh.write(json.dumps(dumped) + "\n")
 
@@ -57,8 +52,14 @@ class FileStorage(AbstractStorage):
         if path.exists(self._filename):
             _LOGGER.debug("Loading settings from %s", self._filename)
             model_json = await self._loop.run_in_executor(None, self._read_file)
-            self.storage_model = StorageModel(**json.loads(model_json))
-            self.mark_as_saved()
+            raw_data = json.loads(model_json)
+            self.storage_model = StorageModel.parse_obj(raw_data)
+
+            # Update hash based on what we read from file rather than serializing the
+            # model. The reasonf for this is that pydantic might (because of
+            # validators) modify the data we read and in that case we want to ensure
+            # catch the update and actually save to file again when save is called.
+            self.update_hash(raw_data)
 
     def _read_file(self) -> str:
         with open(self._filename, "r", encoding="utf-8") as _fh:
diff --git a/pyatv/storage/memory_storage.py b/pyatv/storage/memory_storage.py
index ce67b2de..fbea86e9 100644
--- a/pyatv/storage/memory_storage.py
+++ b/pyatv/storage/memory_storage.py
@@ -14,7 +14,7 @@ class MemoryStorage(AbstractStorage):
 
     async def save(self) -> None:
         """Save settings to active storage."""
-        self.mark_as_saved()
+        self.update_hash(dict(self))
 
     async def load(self) -> None:
         """Load settings from active storage."""
diff --git a/pyproject.toml b/pyproject.toml
index 850b1cda..622e520e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,3 +24,6 @@ module = [
     "srptools",
 ]
 ignore_missing_imports = true
+
+[tool.pytest.ini_options]
+asyncio_default_fixture_loop_scope = "function"
\ No newline at end of file


Test patch:
diff --git a/requirements/requirements_test.txt b/requirements/requirements_test.txt
index d94a6fe4..dae8e896 100644
--- a/requirements/requirements_test.txt
+++ b/requirements/requirements_test.txt
@@ -13,8 +13,8 @@ pytest-aiohttp==1.0.5
 pytest_httpserver==1.1.3
 pytest-xdist==3.8.0
 pydocstyle==6.3.0
-mypy==1.15.0
+mypy==1.16.1
 mypy-protobuf==3.6.0
 types-protobuf==6.30.2.20250703
-types-requests==2.32.0.20250328
+types-requests==2.32.4.20250611
 types-tabulate==0.9.0.20241207
diff --git a/tests/storage/test_memory_storage.py b/tests/storage/test_memory_storage.py
index 585a5f5a..5e81a3e5 100644
--- a/tests/storage/test_memory_storage.py
+++ b/tests/storage/test_memory_storage.py
@@ -169,21 +169,21 @@ async def test_unsupported_version_raises(memory_storage):
 
 
 async def test_change_field_reflected_in_changed_property(memory_storage):
-    assert not memory_storage.changed
+    assert memory_storage.has_changed(dict(memory_storage))
+    memory_storage.update_hash(dict(memory_storage))
+    assert not memory_storage.has_changed(dict(memory_storage))
 
     atv = AppleTV(IPv4Address("127.0.0.1"), "test")
     atv.add_service(ManualService("id1", Protocol.DMAP, 1234, {}))
     settings = await memory_storage.get_settings(atv)
 
-    assert memory_storage.changed
-    memory_storage.mark_as_saved()
-    assert not memory_storage.changed
+    assert memory_storage.has_changed(dict(memory_storage))
+    memory_storage.update_hash(dict(memory_storage))
 
     settings.info.name = "test"
-
-    assert memory_storage.changed
-    memory_storage.mark_as_saved()
-    assert not memory_storage.changed
+    assert memory_storage.has_changed(dict(memory_storage))
+    memory_storage.update_hash(dict(memory_storage))
+    assert not memory_storage.has_changed(dict(memory_storage))
 
 
 async def test_set_model_reflects_changed_property(memory_storage):
@@ -192,13 +192,13 @@ async def test_set_model_reflects_changed_property(memory_storage):
     await memory_storage.get_settings(atv)
 
     new_storage = MemoryStorage()
-    assert not new_storage.changed
+    new_storage.update_hash(dict(new_storage))
 
     new_storage.storage_model = memory_storage.storage_model
-    assert new_storage.changed
+    assert new_storage.has_changed(dict(new_storage))
 
-    new_storage.mark_as_saved()
-    assert not new_storage.changed
+    new_storage.update_hash(dict(new_storage))
+    assert not new_storage.has_changed(dict(new_storage))
 
 
 async def test_save_updates_changed_property(memory_storage):
@@ -206,6 +206,6 @@ async def test_save_updates_changed_property(memory_storage):
     atv.add_service(ManualService("id1", Protocol.DMAP, 1234, {}))
     await memory_storage.get_settings(atv)
 
-    assert memory_storage.changed
+    assert memory_storage.has_changed(dict(memory_storage))
     await memory_storage.save()
-    assert not memory_storage.changed
+    assert not memory_storage.has_changed(dict(memory_storage))
diff --git a/tests/test_storage_functional.py b/tests/test_storage_functional.py
index 6e0e68d1..c8c8c7e0 100644
--- a/tests/test_storage_functional.py
+++ b/tests/test_storage_functional.py
@@ -5,7 +5,7 @@ Uses MemoryStorage as storage for simplicity.
 """
 
 import asyncio
-from unittest.mock import MagicMock, patch
+import os
 
 from deepdiff import DeepDiff
 import pytest
@@ -13,7 +13,6 @@ import pytest
 from pyatv import connect, pair
 from pyatv.const import Protocol
 from pyatv.interface import Storage
-from pyatv.protocols import ProtocolMethods
 from pyatv.storage.file_storage import FileStorage
 
 from tests.fake_udns import airplay_service
@@ -56,8 +55,13 @@ async def test_scan_inserts_into_storage(unicast_scan, mockfs):
 
 
 async def test_provides_storage_to_pairing_handler(
-    unicast_scan, session_manager, mockfs
+    unicast_scan, session_manager, monkeypatch
 ):
+    def fake_urandom(n):
+        return b"\x00" * n
+
+    monkeypatch.setattr(os, "urandom", fake_urandom)
+
     loop = asyncio.get_running_loop()
 
     storage = await new_storage(STORAGE_FILENAME, loop)