Repository: postlund/pyatv

Issue:
Device and power state are not aligned
### Describe the bug

Device and power states are managed independently by pyatv. Unfortunately, the power state is a bit unreliable and it sometimes happens that it reports `Off` even though something is playing. One such case is when using the Apple TV as an AirPlay receiver, only to receive and play audio but not use any screen as output. The power state more or less mirrors if there is a monitor on or not (which is not totally true either because `turn_on` with Companion will cause the power state to be reported on `On` even if no screen is on). So it's a pickle...

My suggestion is to at least change power state to on when something is playing as that makes more sense. It will potentially trigger lots of changes between on and off whenever media plays or not, so that's the flipside of this.

### Error log

_No response_

### How to reproduce the bug?

Stream audio to an Apple TV without having the TV turned on: pyatv shall report `Off` as `power_state` but still provide a playing state.

### What is expected behavior?

Power state should be reported as `On`.

### Operating System

Any

### Python

Other

### pyatv

any

### Device

Apple TV 4 and later

### Additional context

Reported here: https://github.com/postlund/pyatv/issues/1333#issuecomment-961279579

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/postlund/pyatv/pull/1519?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) Report
> Merging [#1519](https://codecov.io/gh/postlund/pyatv/pull/1519?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) (37c4a7d) into [master](https://codecov.io/gh/postlund/pyatv/commit/f0c4c33d1c4abb8d3ba69b13fa90996f5c1a97e6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) (f0c4c33) will **decrease** coverage by `0.29%`.
> The diff coverage is `98.95%`.

[![Impacted file tree graph](https://codecov.io/gh/postlund/pyatv/pull/1519/graphs/tree.svg?width=650&height=150&src=pr&token=wb6azJjRSY&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl)](https://codecov.io/gh/postlund/pyatv/pull/1519?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl)

```diff
@@            Coverage Diff             @@
##           master    #1519      +/-   ##
==========================================
- Coverage   92.34%   92.04%   -0.30%     
==========================================
  Files         150      150              
  Lines        9734     9695      -39     
==========================================
- Hits         8989     8924      -65     
- Misses        745      771      +26     
```


| [Impacted Files](https://codecov.io/gh/postlund/pyatv/pull/1519?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/interface.py](https://codecov.io/gh/postlund/pyatv/pull/1519/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvaW50ZXJmYWNlLnB5) | `99.25% <ø> (-0.20%)` | :arrow_down: |
| [pyatv/core/\_\_init\_\_.py](https://codecov.io/gh/postlund/pyatv/pull/1519/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvY29yZS9fX2luaXRfXy5weQ==) | `98.50% <97.14%> (-1.50%)` | :arrow_down: |
| [pyatv/\_\_init\_\_.py](https://codecov.io/gh/postlund/pyatv/pull/1519/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvX19pbml0X18ucHk=) | `93.15% <100.00%> (-1.22%)` | :arrow_down: |
| [pyatv/core/facade.py](https://codecov.io/gh/postlund/pyatv/pull/1519/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvY29yZS9mYWNhZGUucHk=) | `98.97% <100.00%> (+0.05%)` | :arrow_up: |
| [pyatv/core/protocol.py](https://codecov.io/gh/postlund/pyatv/pull/1519/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvY29yZS9wcm90b2NvbC5weQ==) | `93.44% <100.00%> (+0.98%)` | :arrow_up: |
| [pyatv/core/relayer.py](https://codecov.io/gh/postlund/pyatv/pull/1519/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvY29yZS9yZWxheWVyLnB5) | `100.00% <100.00%> (ø)` | |
| [pyatv/protocols/\_\_init\_\_.py](https://codecov.io/gh/postlund/pyatv/pull/1519/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvcHJvdG9jb2xzL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [pyatv/protocols/airplay/\_\_init\_\_.py](https://codecov.io/gh/postlund/pyatv/pull/1519/diff?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=) | `75.93% <100.00%> (ø)` | |
| [pyatv/protocols/companion/\_\_init\_\_.py](https://codecov.io/gh/postlund/pyatv/pull/1519/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvcHJvdG9jb2xzL2NvbXBhbmlvbi9fX2luaXRfXy5weQ==) | `97.63% <100.00%> (ø)` | |
| [pyatv/protocols/dmap/\_\_init\_\_.py](https://codecov.io/gh/postlund/pyatv/pull/1519/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl#diff-cHlhdHYvcHJvdG9jb2xzL2RtYXAvX19pbml0X18ucHk=) | `97.41% <100.00%> (ø)` | |
| ... and [13 more](https://codecov.io/gh/postlund/pyatv/pull/1519/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/postlund/pyatv/pull/1519?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/postlund/pyatv/pull/1519?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl). Last update [f0c4c33...37c4a7d](https://codecov.io/gh/postlund/pyatv/pull/1519?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Pierre+St%C3%A5hl). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?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.html b/docs/api/pyatv.html
index 41b9e00f..0802c79a 100644
--- a/docs/api/pyatv.html
+++ b/docs/api/pyatv.html
@@ -35,7 +35,7 @@ link_group: api
 </header>
 <section id="section-intro">
 <p>Main routines for interacting with an Apple TV.</p>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/__init__.py#L1-L131" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/__init__.py#L1-L139" class="git-link">Browse git</a></div>
 </section>
 <section>
 <h2 class="section-title" id="header-submodules">Sub-modules</h2>
@@ -78,7 +78,7 @@ link_group: api
 </dt>
 <dd>
 <section class="desc"><p>Connect to a device based on a configuration.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/__init__.py#L70-L107" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/__init__.py#L71-L115" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.pair">
 <code class="name flex">
@@ -87,7 +87,7 @@ link_group: api
 </dt>
 <dd>
 <section class="desc"><p>Pair a protocol for an Apple TV.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/__init__.py#L110-L131" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/__init__.py#L118-L139" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.scan">
 <code class="name flex">
@@ -96,7 +96,7 @@ link_group: api
 </dt>
 <dd>
 <section class="desc"><p>Scan for Apple TVs on network and return their configurations.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/__init__.py#L23-L67" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/__init__.py#L24-L68" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 </section>
diff --git a/docs/api/pyatv/interface.html b/docs/api/pyatv/interface.html
index 535404ff..70bf4b59 100644
--- a/docs/api/pyatv/interface.html
+++ b/docs/api/pyatv/interface.html
@@ -202,7 +202,6 @@ link_group: api
 <h4><code><a title="pyatv.interface.PushUpdater" href="#pyatv.interface.PushUpdater">PushUpdater</a></code></h4>
 <ul class="">
 <li><code><a title="pyatv.interface.PushUpdater.active" href="#pyatv.interface.PushUpdater.active">active</a></code></li>
-<li><code><a title="pyatv.interface.PushUpdater.post_update" href="#pyatv.interface.PushUpdater.post_update">post_update</a></code></li>
 <li><code><a title="pyatv.interface.PushUpdater.start" href="#pyatv.interface.PushUpdater.start">start</a></code></li>
 <li><code><a title="pyatv.interface.PushUpdater.stop" href="#pyatv.interface.PushUpdater.stop">stop</a></code></li>
 </ul>
@@ -256,7 +255,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-L1231" 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-L1221" class="git-link">Browse git</a></div>
 </section>
 <section>
 </section>
@@ -309,7 +308,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#L1166-L1231" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1156-L1221" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -325,52 +324,52 @@ all its features.</p>
 <dt id="pyatv.interface.AppleTV.apps"><code class="name">var <span class="ident">apps</span> -> <a title="pyatv.interface.Apps" href="#pyatv.interface.Apps">Apps</a></code></dt>
 <dd>
 <section class="desc"><p>Return apps interface.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1223-L1226" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1213-L1216" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.AppleTV.audio"><code class="name">var <span class="ident">audio</span> -> <a title="pyatv.interface.Audio" href="#pyatv.interface.Audio">Audio</a></code></dt>
 <dd>
 <section class="desc"><p>Return audio interface.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1228-L1231" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1218-L1221" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.AppleTV.device_info"><code class="name">var <span class="ident">device_info</span> -> <a title="pyatv.interface.DeviceInfo" href="#pyatv.interface.DeviceInfo">DeviceInfo</a></code></dt>
 <dd>
 <section class="desc"><p>Return API for device information.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1183-L1186" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1173-L1176" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.AppleTV.features"><code class="name">var <span class="ident">features</span> -> <a title="pyatv.interface.Features" href="#pyatv.interface.Features">Features</a></code></dt>
 <dd>
 <section class="desc"><p>Return features interface.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1218-L1221" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1208-L1211" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.AppleTV.metadata"><code class="name">var <span class="ident">metadata</span> -> <a title="pyatv.interface.Metadata" href="#pyatv.interface.Metadata">Metadata</a></code></dt>
 <dd>
 <section class="desc"><p>Return API for retrieving metadata from the Apple TV.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1198-L1201" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1188-L1191" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.AppleTV.power"><code class="name">var <span class="ident">power</span> -> <a title="pyatv.interface.Power" href="#pyatv.interface.Power">Power</a></code></dt>
 <dd>
 <section class="desc"><p>Return API for power management.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1213-L1216" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1203-L1206" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.AppleTV.push_updater"><code class="name">var <span class="ident">push_updater</span> -> <a title="pyatv.interface.PushUpdater" href="#pyatv.interface.PushUpdater">PushUpdater</a></code></dt>
 <dd>
 <section class="desc"><p>Return API for handling push update from the Apple TV.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1203-L1206" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1193-L1196" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.AppleTV.remote_control"><code class="name">var <span class="ident">remote_control</span> -> <a title="pyatv.interface.RemoteControl" href="#pyatv.interface.RemoteControl">RemoteControl</a></code></dt>
 <dd>
 <section class="desc"><p>Return API for controlling the Apple TV.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1193-L1196" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1183-L1186" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.AppleTV.service"><code class="name">var <span class="ident">service</span> -> <a title="pyatv.interface.BaseService" href="#pyatv.interface.BaseService">BaseService</a></code></dt>
 <dd>
 <section class="desc"><p>Return service used to connect to the Apple TV.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1188-L1191" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1178-L1181" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.AppleTV.stream"><code class="name">var <span class="ident">stream</span> -> <a title="pyatv.interface.Stream" href="#pyatv.interface.Stream">Stream</a></code></dt>
 <dd>
 <section class="desc"><p>Return API for streaming media.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1208-L1211" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1198-L1201" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 <h3>Methods</h3>
@@ -382,7 +381,7 @@ all its features.</p>
 </dt>
 <dd>
 <section class="desc"><p>Close connection and release allocated resources.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1179-L1181" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1169-L1171" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.AppleTV.connect">
 <code class="name flex">
@@ -392,7 +391,7 @@ all its features.</p>
 <dd>
 <section class="desc"><p>Initiate connection to device.</p>
 <p>No need to call it yourself, it's done automatically.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1172-L1177" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1162-L1167" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 </dd>
@@ -474,7 +473,7 @@ all its features.</p>
 <dd>
 <section class="desc"><p>Base class for audio functionality.</p>
 <p>Volume level is managed in percent where 0 is muted and 100 is max volume.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L988-L1033" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L978-L1023" class="git-link">Browse git</a></div>
 <h3>Subclasses</h3>
 <ul class="hlist">
 <li>pyatv.core.facade.FacadeAudio</li>
@@ -493,7 +492,7 @@ all its features.</p>
 </div>
 <section class="desc"><p>Return current volume level.</p>
 <p>Range is in percent, i.e. [0.0-100.0].</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L994-L1001" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L984-L991" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 <h3>Methods</h3>
@@ -510,7 +509,7 @@ all its features.</p>
 </div>
 <section class="desc"><p>Change current volume level.</p>
 <p>Range is in percent, i.e. [0.0-100.0].</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1003-L1009" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L993-L999" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.Audio.volume_down">
 <code class="name flex">
@@ -527,7 +526,7 @@ all its features.</p>
 range. It is not necessarily linear.</p>
 <p>Call will block until volume change has been acknowledged by the device (when
 possible and supported).</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1023-L1033" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1013-L1023" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.Audio.volume_up">
 <code class="name flex">
@@ -544,7 +543,7 @@ possible and supported).</p></section>
 range. It is not necessarily linear.</p>
 <p>Call will block until volume change has been acknowledged by the device (when
 possible and supported).</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1011-L1021" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1001-L1011" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 </dd>
@@ -558,7 +557,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#L1036-L1163" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1026-L1153" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -572,47 +571,47 @@ AirPlay.</p>
 <dt id="pyatv.interface.BaseConfig.address"><code class="name">var <span class="ident">address</span> -> ipaddress.IPv4Address</code></dt>
 <dd>
 <section class="desc"><p>IP address of device.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1048-L1051" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1038-L1041" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.BaseConfig.all_identifiers"><code class="name">var <span class="ident">all_identifiers</span> -> List[str]</code></dt>
 <dd>
 <section class="desc"><p>Return all unique identifiers for this device.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1110-L1113" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1100-L1103" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.BaseConfig.deep_sleep"><code class="name">var <span class="ident">deep_sleep</span> -> bool</code></dt>
 <dd>
 <section class="desc"><p>If device is in deep sleep.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1058-L1061" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1048-L1051" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.BaseConfig.device_info"><code class="name">var <span class="ident">device_info</span> -> <a title="pyatv.interface.DeviceInfo" href="#pyatv.interface.DeviceInfo">DeviceInfo</a></code></dt>
 <dd>
 <section class="desc"><p>Return general device information.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1068-L1071" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1058-L1061" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.BaseConfig.identifier"><code class="name">var <span class="ident">identifier</span> -> Optional[str]</code></dt>
 <dd>
 <section class="desc"><p>Return the main identifier associated with this device.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1101-L1108" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1091-L1098" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.BaseConfig.name"><code class="name">var <span class="ident">name</span> -> str</code></dt>
 <dd>
 <section class="desc"><p>Name of device.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1053-L1056" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1043-L1046" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.BaseConfig.properties"><code class="name">var <span class="ident">properties</span> -> Mapping[str, Mapping[str, str]]</code></dt>
 <dd>
 <section class="desc"><p>Return Zeroconf properties.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1088-L1091" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1078-L1081" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.BaseConfig.ready"><code class="name">var <span class="ident">ready</span> -> bool</code></dt>
 <dd>
 <section class="desc"><p>Return if configuration is ready, (at least one service with identifier).</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1093-L1099" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1083-L1089" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.BaseConfig.services"><code class="name">var <span class="ident">services</span> -> List[<a title="pyatv.interface.BaseService" href="#pyatv.interface.BaseService">BaseService</a>]</code></dt>
 <dd>
 <section class="desc"><p>Return all supported services.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1063-L1066" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1053-L1056" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 <h3>Methods</h3>
@@ -625,7 +624,7 @@ AirPlay.</p>
 <dd>
 <section class="desc"><p>Add a new service.</p>
 <p>If the service already exists, it will be merged.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1073-L1078" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1063-L1068" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.BaseConfig.get_service">
 <code class="name flex">
@@ -636,7 +635,7 @@ AirPlay.</p>
 <section class="desc"><p>Look up a service based on protocol.</p>
 <p>If a service with the specified protocol is not available, None is
 returned.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1080-L1086" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1070-L1076" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.BaseConfig.main_service">
 <code class="name flex">
@@ -645,7 +644,7 @@ returned.</p></section>
 </dt>
 <dd>
 <section class="desc"><p>Return suggested service used to establish connection.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1115-L1128" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1105-L1118" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.BaseConfig.set_credentials">
 <code class="name flex">
@@ -654,7 +653,7 @@ returned.</p></section>
 </dt>
 <dd>
 <section class="desc"><p>Set credentials for a protocol if it exists.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1130-L1136" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L1120-L1126" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 </dd>
@@ -734,40 +733,40 @@ returned.</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#L839-L950" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L829-L940" class="git-link">Browse git</a></div>
 <h3>Instance variables</h3>
 <dl>
 <dt id="pyatv.interface.DeviceInfo.build_number"><code class="name">var <span class="ident">build_number</span> -> Optional[str]</code></dt>
 <dd>
 <section class="desc"><p>Operating system build number, e.g. 17K795.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L901-L904" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L891-L894" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.DeviceInfo.mac"><code class="name">var <span class="ident">mac</span> -> Optional[str]</code></dt>
 <dd>
 <section class="desc"><p>Device MAC address.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L920-L923" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L910-L913" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.DeviceInfo.model"><code class="name">var <span class="ident">model</span> -> <a title="pyatv.const.DeviceModel" href="const#pyatv.const.DeviceModel">DeviceModel</a></code></dt>
 <dd>
 <section class="desc"><p>Hardware model name, e.g. 3, 4 or 4K.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L906-L909" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L896-L899" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.DeviceInfo.operating_system"><code class="name">var <span class="ident">operating_system</span> -> <a title="pyatv.const.OperatingSystem" href="const#pyatv.const.OperatingSystem">OperatingSystem</a></code></dt>
 <dd>
 <section class="desc"><p>Operating system running on device.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L868-L887" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L858-L877" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.DeviceInfo.raw_model"><code class="name">var <span class="ident">raw_model</span> -> Optional[str]</code></dt>
 <dd>
 <section class="desc"><p>Return raw model description.</p>
 <p>If <code><a title="pyatv.interface.DeviceInfo.model" href="#pyatv.interface.DeviceInfo.model">DeviceInfo.model</a></code> returns <code><a title="pyatv.const.DeviceModel.Unknown" href="const#pyatv.const.DeviceModel.Unknown">DeviceModel.Unknown</a></code>
 then this property contains the raw model string (if any is available).</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L911-L918" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L901-L908" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.DeviceInfo.version"><code class="name">var <span class="ident">version</span> -> Optional[str]</code></dt>
 <dd>
 <section class="desc"><p>Operating system version.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L889-L899" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L879-L889" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 </dd>
@@ -776,7 +775,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#L791-L802" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L781-L792" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -795,7 +794,7 @@ then this property contains the raw model string (if any is available).</p></sec
 </dt>
 <dd>
 <section class="desc"><p>Device connection was (intentionally) closed.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L799-L802" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L789-L792" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.DeviceListener.connection_lost">
 <code class="name flex">
@@ -804,7 +803,7 @@ then this property contains the raw model string (if any is available).</p></sec
 </dt>
 <dd>
 <section class="desc"><p>Device was unexpectedly disconnected.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L794-L797" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L784-L787" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 </dd>
@@ -836,7 +835,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#L953-L985" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L943-L975" class="git-link">Browse git</a></div>
 <h3>Subclasses</h3>
 <ul class="hlist">
 <li>pyatv.core.facade.FacadeFeatures</li>
@@ -855,7 +854,7 @@ then this property contains the raw model string (if any is available).</p></sec
 </dt>
 <dd>
 <section class="desc"><p>Return state of all features.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L960-L967" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L950-L957" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.Features.get_feature">
 <code class="name flex">
@@ -864,7 +863,7 @@ then this property contains the raw model string (if any is available).</p></sec
 </dt>
 <dd>
 <section class="desc"><p>Return current state of a feature.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L956-L958" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L946-L948" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.Features.in_state">
 <code class="name flex">
@@ -876,7 +875,7 @@ then this property contains the raw model string (if any is available).</p></sec
 <p>This method will return True if all given features are in the state specified
 by "states". If "states" is a list of states, it is enough for the feature to be
 in one of the listed states.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L969-L985" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L959-L975" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 </dd>
@@ -1176,7 +1175,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#L816-L836" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L806-L826" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -1198,7 +1197,7 @@ always be the same for the same content, but it is not guaranteed.</p></section>
 <span>Supported by: <a title="pyatv.const.Protocol.MRP" href="const#pyatv.const.Protocol.MRP">Protocol.MRP</a></span>
 </div>
 <section class="desc"><p>Return device power state.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L822-L826" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L812-L816" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 <h3>Methods</h3>
@@ -1214,7 +1213,7 @@ always be the same for the same content, but it is not guaranteed.</p></section>
 <span>Supported by: <a title="pyatv.const.Protocol.Companion" href="const#pyatv.const.Protocol.Companion">Protocol.Companion</a>, <a title="pyatv.const.Protocol.MRP" href="const#pyatv.const.Protocol.MRP">Protocol.MRP</a></span>
 </div>
 <section class="desc"><p>Turn device off.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L833-L836" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L823-L826" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.Power.turn_on">
 <code class="name flex">
@@ -1227,7 +1226,7 @@ always be the same for the same content, but it is not guaranteed.</p></section>
 <span>Supported by: <a title="pyatv.const.Protocol.Companion" href="const#pyatv.const.Protocol.Companion">Protocol.Companion</a>, <a title="pyatv.const.Protocol.MRP" href="const#pyatv.const.Protocol.MRP">Protocol.MRP</a></span>
 </div>
 <section class="desc"><p>Turn device on.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L828-L831" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L818-L821" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 </dd>
@@ -1236,7 +1235,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#L805-L813" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L795-L803" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -1255,7 +1254,7 @@ always be the same for the same content, but it is not guaranteed.</p></section>
 </dt>
 <dd>
 <section class="desc"><p>Device power state was updated.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L808-L813" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L798-L803" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 </dd>
@@ -1299,13 +1298,15 @@ always be the same for the same content, but it is not guaranteed.</p></section>
 </dd>
 <dt id="pyatv.interface.PushUpdater"><code class="flex name class">
 <span>class <span class="ident">PushUpdater</span></span>
-<span>(</span><span>loop: asyncio.events.AbstractEventLoop)</span>
+<span>(</span><span>max_calls: int = 0)</span>
 </code></dt>
 <dd>
 <section class="desc"><p>Base class for push/async updates from an Apple TV.</p>
-<p>Listener interface: <code><a title="pyatv.interface.PushListener" href="#pyatv.interface.PushListener">PushListener</a></code></p>
-<p>Initialize a new PushUpdater.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L728-L765" class="git-link">Browse git</a></div>
+<p>A <code><a title="pyatv.interface.PushUpdater" href="#pyatv.interface.PushUpdater">PushUpdater</a></code> shall only publish update in case the state
+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#L728-L755" class="git-link">Browse git</a></div>
 <h3>Ancestors</h3>
 <ul class="hlist">
 <li>abc.ABC</li>
@@ -1314,30 +1315,19 @@ always be the same for the same content, but it is not guaranteed.</p></section>
 </ul>
 <h3>Subclasses</h3>
 <ul class="hlist">
+<li>pyatv.core.AbstractPushUpdater</li>
 <li>pyatv.core.facade.FacadePushUpdater</li>
-<li>pyatv.protocols.dmap.DmapPushUpdater</li>
-<li>pyatv.protocols.mrp.MrpPushUpdater</li>
-<li>pyatv.protocols.raop.RaopPushUpdater</li>
 </ul>
 <h3>Instance variables</h3>
 <dl>
 <dt id="pyatv.interface.PushUpdater.active"><code class="name">var <span class="ident">active</span> -> bool</code></dt>
 <dd>
 <section class="desc"><p>Return if push updater has been started.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L740-L744" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L737-L741" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 <h3>Methods</h3>
 <dl>
-<dt id="pyatv.interface.PushUpdater.post_update">
-<code class="name flex">
-<span>def <span class="ident">post_update</span></span>(<span>self, playing: <a title="pyatv.interface.Playing" href="#pyatv.interface.Playing">Playing</a>) -> None</span>
-</code>
-</dt>
-<dd>
-<section class="desc"><p>Post an update to listener.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L760-L765" class="git-link">Browse git</a></div>
-</dd>
 <dt id="pyatv.interface.PushUpdater.start">
 <code class="name flex">
 <span>def <span class="ident">start</span></span>(<span>self, initial_delay: int = 0) -> None</span>
@@ -1346,11 +1336,11 @@ always be the same for the same content, but it is not guaranteed.</p></section>
 <dd>
 <div class="api_feature">
 <span>Feature: <a title="pyatv.const.FeatureName.PushUpdates" href="const#pyatv.const.FeatureName.PushUpdates">FeatureName.PushUpdates</a>,</span>
-<span>Supported by: <a title="pyatv.const.Protocol.DMAP" href="const#pyatv.const.Protocol.DMAP">Protocol.DMAP</a>, <a title="pyatv.const.Protocol.MRP" href="const#pyatv.const.Protocol.MRP">Protocol.MRP</a>, <a title="pyatv.const.Protocol.RAOP" href="const#pyatv.const.Protocol.RAOP">Protocol.RAOP</a></span>
+<span>Supported by: </span>
 </div>
 <section class="desc"><p>Begin to listen to updates.</p>
 <p>If an error occurs, start must be called again.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L746-L753" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L743-L750" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.PushUpdater.stop">
 <code class="name flex">
@@ -1359,7 +1349,7 @@ always be the same for the same content, but it is not guaranteed.</p></section>
 </dt>
 <dd>
 <section class="desc"><p>No longer forward updates to listener.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L755-L758" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L752-L755" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 </dd>
@@ -1704,7 +1694,7 @@ always be the same for the same content, but it is not guaranteed.</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#L768-L788" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L758-L778" class="git-link">Browse git</a></div>
 <h3>Subclasses</h3>
 <ul class="hlist">
 <li>pyatv.core.facade.FacadeStream</li>
@@ -1720,7 +1710,7 @@ always be the same for the same content, but it is not guaranteed.</p></section>
 </dt>
 <dd>
 <section class="desc"><p>Close connection and release allocated resources.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L771-L773" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L761-L763" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.Stream.play_url">
 <code class="name flex">
@@ -1733,7 +1723,7 @@ always be the same for the same content, but it is not guaranteed.</p></section>
 <span>Supported by: <a title="pyatv.const.Protocol.AirPlay" href="const#pyatv.const.Protocol.AirPlay">Protocol.AirPlay</a></span>
 </div>
 <section class="desc"><p>Play media from an URL on the device.</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L775-L778" 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-L768" class="git-link">Browse git</a></div>
 </dd>
 <dt id="pyatv.interface.Stream.stream_file">
 <code class="name flex">
@@ -1748,7 +1738,7 @@ always be the same for the same content, but it is not guaranteed.</p></section>
 <section class="desc"><p>Stream local or remote file to device.</p>
 <p>Supports either local file paths or a HTTP(s) address.</p>
 <p>INCUBATING METHOD - MIGHT CHANGE IN THE FUTURE!</p></section>
-<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L780-L788" class="git-link">Browse git</a></div>
+<div class="git-link-div"><a href="https://github.com/postlund/pyatv/blob/master/pyatv/interface.py#L770-L778" class="git-link">Browse git</a></div>
 </dd>
 </dl>
 </dd>
diff --git a/pyatv/__init__.py b/pyatv/__init__.py
index 2deec062..56c4c65a 100644
--- a/pyatv/__init__.py
+++ b/pyatv/__init__.py
@@ -12,6 +12,7 @@ import aiohttp
 
 from pyatv import exceptions, interface
 from pyatv.const import Protocol
+from pyatv.core import CoreStateDispatcher, ProtocolStateDispatcher
 from pyatv.core.facade import FacadeAppleTV
 from pyatv.core.scan import BaseScanner, MulticastMdnsScanner, UnicastMdnsScanner
 from pyatv.protocols import PROTOCOLS
@@ -82,7 +83,8 @@ async def connect(
 
     config_copy = deepcopy(config)
     session_manager = await http.create_session(session)
-    atv = FacadeAppleTV(config_copy, session_manager)
+    core_dispatcher = CoreStateDispatcher()
+    atv = FacadeAppleTV(config_copy, session_manager, core_dispatcher)
 
     try:
         for proto, proto_methods in PROTOCOLS.items():
@@ -95,7 +97,13 @@ async def connect(
             takeover_method = partial(atv.takeover, proto)
 
             for setup_data in proto_methods.setup(
-                loop, config_copy, service, atv, session_manager, takeover_method
+                loop,
+                config_copy,
+                service,
+                atv,
+                session_manager,
+                takeover_method,
+                ProtocolStateDispatcher(proto, core_dispatcher),
             ):
                 atv.add_protocol(setup_data)
 
diff --git a/pyatv/core/__init__.py b/pyatv/core/__init__.py
index 3a57d340..fd90f58b 100644
--- a/pyatv/core/__init__.py
+++ b/pyatv/core/__init__.py
@@ -1,9 +1,22 @@
 """Core module of pyatv."""
 import asyncio
-from typing import Any, Awaitable, Callable, Dict, Mapping, NamedTuple, Optional, Set
+from enum import Enum
+from typing import (
+    Any,
+    Awaitable,
+    Callable,
+    Dict,
+    List,
+    Mapping,
+    NamedTuple,
+    Optional,
+    Set,
+    Union,
+)
 
 from pyatv.const import FeatureName, PairingRequirement, Protocol
-from pyatv.interface import BaseService
+from pyatv.core.protocol import MessageDispatcher
+from pyatv.interface import BaseService, Playing, PushUpdater
 
 TakeoverMethod = Callable[
     ...,
@@ -11,6 +24,69 @@ TakeoverMethod = Callable[
 ]
 
 
+# pylint: disable=invalid-name
+
+
+class UpdatedState(Enum):
+    """Name of states that can be updated."""
+
+    Playing = 1
+    """Playing state in metadata was updated."""
+
+
+# pylint: enable=invalid-name
+
+
+class StateMessage(NamedTuple):
+    """Message sent when state of something changed."""
+
+    protocol: Protocol
+    state: UpdatedState
+
+    # Type depending on value of state:
+    # - UpdatedState.Playing -> interface.Playing
+    value: Any
+
+    def __str__(self):
+        """Return string representation of object."""
+        return f"[{self.protocol.name}.{self.state.name} -> {self.value}]"
+
+
+def _no_filter(message: StateMessage) -> bool:
+    return True
+
+
+CoreStateDispatcher = MessageDispatcher[UpdatedState, StateMessage]
+
+
+class ProtocolStateDispatcher:
+    """Dispatch internal protocol state updates to listeners."""
+
+    def __init__(
+        self,
+        protocol: Protocol,
+        core_dispatcher: CoreStateDispatcher,
+    ) -> None:
+        """Initialize a new ProtocolStateDispatcher instance."""
+        self._protocol = protocol
+        self._core_dispatcher = core_dispatcher
+
+    def listen_to(
+        self,
+        state: UpdatedState,
+        func: Callable[[StateMessage], Union[None, Awaitable[None]]],
+        message_filter: Callable[[StateMessage], bool] = _no_filter,
+    ) -> None:
+        """Listen to a specific type of message type."""
+        return self._core_dispatcher.listen_to(state, func, message_filter)
+
+    def dispatch(self, state: UpdatedState, value: Any) -> List[asyncio.Task]:
+        """Dispatch a message to listeners."""
+        return self._core_dispatcher.dispatch(
+            state, StateMessage(self._protocol, state, value)
+        )
+
+
 class MutableService(BaseService):
     """Mutable version of BaseService allowing some fields to be changed.
 
@@ -71,6 +147,33 @@ class MutableService(BaseService):
         return copy
 
 
+class AbstractPushUpdater(PushUpdater):
+    """Abstract push updater class.
+
+    This class adds a `post_update` method that will publishes a new state, but only
+    if it has been updated.
+    """
+
+    def __init__(self, state_dispatcher: ProtocolStateDispatcher):
+        """Initialize a new AbstractPushUpdater."""
+        super().__init__()
+        self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
+        self.state_dispatcher = state_dispatcher
+        self._previous_state: Optional[Playing] = None
+
+    def post_update(self, playing: Playing) -> None:
+        """Post an update to listener."""
+        if playing != self._previous_state:
+            # Dispatch message using message dispatcher
+            self.state_dispatcher.dispatch(UpdatedState.Playing, playing)
+
+            # Publish using regular (external) interface. This shall be removed at
+            # some point in time.
+            self.loop.call_soon(self.listener.playstatus_update, self, playing)
+
+        self._previous_state = playing
+
+
 class SetupData(NamedTuple):
     """Information for setting up a protocol."""
 
diff --git a/pyatv/core/facade.py b/pyatv/core/facade.py
index 37b4dee8..77d51b98 100644
--- a/pyatv/core/facade.py
+++ b/pyatv/core/facade.py
@@ -17,8 +17,15 @@ from queue import Queue
 from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union, cast
 
 from pyatv import const, exceptions, interface
-from pyatv.const import FeatureName, FeatureState, InputAction, Protocol
-from pyatv.core import SetupData
+from pyatv.const import (
+    DeviceState,
+    FeatureName,
+    FeatureState,
+    InputAction,
+    PowerState,
+    Protocol,
+)
+from pyatv.core import CoreStateDispatcher, SetupData, StateMessage, UpdatedState
 from pyatv.core.relayer import Relayer
 from pyatv.support import deprecated
 from pyatv.support.collections import dict_merge
@@ -251,11 +258,35 @@ class FacadePower(Relayer, interface.Power, interface.PowerListener):
         Protocol.RAOP,
     ]
 
-    def __init__(self):
+    def __init__(self, core_dispatcher: CoreStateDispatcher):
         """Initialize a new FacadePower instance."""
         # This is border line, maybe need another structure to support this
         Relayer.__init__(self, interface.Power, DEFAULT_PRIORITIES)
         interface.Power.__init__(self)
+        self._is_playing: Optional[bool] = None
+        core_dispatcher.listen_to(
+            UpdatedState.Playing,
+            self._playing_changed,
+            message_filter=lambda message: message.protocol == self.main_protocol,
+        )
+
+    def _playing_changed(self, message: StateMessage) -> None:
+        """State of something changed."""
+        playing = cast(interface.Playing, message.value)
+
+        # Initially we must ask the protocol about power state so we don't send
+        # duplicate updates
+        if self._is_playing is None:
+            self._is_playing = self.relay("power_state") == PowerState.On
+
+        # Computer new state so we can know if we should update or not
+        old_state = self.power_state
+        self._is_playing = playing.device_state != DeviceState.Idle
+        new_state = self.power_state
+
+        # Do not update state in case it didn't change
+        if new_state != old_state:
+            self.listener.powerstate_update(old_state, new_state)
 
     def powerstate_update(
         self, old_state: const.PowerState, new_state: const.PowerState
@@ -264,11 +295,15 @@ class FacadePower(Relayer, interface.Power, interface.PowerListener):
 
         Forward power state updates from protocol implementations to actual listener.
         """
-        self.listener.powerstate_update(old_state, new_state)
+        if not self._is_playing:
+            self.listener.powerstate_update(old_state, new_state)
 
     @property
     def power_state(self) -> const.PowerState:
         """Return device power state."""
+        # Override power state in case something is playing
+        if self._is_playing:
+            return const.PowerState.On
         return self.relay("power_state")
 
     async def turn_on(self, await_new_state: bool = False) -> None:
@@ -372,7 +407,7 @@ class FacadePushUpdater(
         Relayer.__init__(  # pylint: disable=non-parent-init-called
             self, interface.PushUpdater, DEFAULT_PRIORITIES
         )
-        interface.PushUpdater.__init__(self, asyncio.get_event_loop())
+        interface.PushUpdater.__init__(self)
 
     @property
     def active(self) -> bool:
@@ -409,7 +444,10 @@ class FacadeAppleTV(interface.AppleTV):
     """Facade implementation of the external interface."""
 
     def __init__(
-        self, config: interface.BaseConfig, session_manager: ClientSessionManager
+        self,
+        config: interface.BaseConfig,
+        session_manager: ClientSessionManager,
+        core_dispatcher: CoreStateDispatcher,
     ):
         """Initialize a new FacadeAppleTV instance."""
         super().__init__(max_calls=1)  # To StateProducer via interface.AppleTV
@@ -425,7 +463,7 @@ class FacadeAppleTV(interface.AppleTV):
             interface.Features: self._features,
             interface.RemoteControl: FacadeRemoteControl(),
             interface.Metadata: FacadeMetadata(),
-            interface.Power: FacadePower(),
+            interface.Power: FacadePower(core_dispatcher),
             interface.PushUpdater: self._push_updates,
             interface.Stream: FacadeStream(self._features),
             interface.Apps: FacadeApps(),
diff --git a/pyatv/core/protocol.py b/pyatv/core/protocol.py
index a78d31fc..ac10ac79 100644
--- a/pyatv/core/protocol.py
+++ b/pyatv/core/protocol.py
@@ -1,7 +1,18 @@
 """Generic functions for protocol logic."""
 import asyncio
+import inspect
 import logging
-from typing import Awaitable, Callable, Dict, Generic, List, Optional, TypeVar
+from typing import (
+    Awaitable,
+    Callable,
+    Dict,
+    Generic,
+    List,
+    Optional,
+    Tuple,
+    TypeVar,
+    Union,
+)
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -12,7 +23,12 @@ MessageType = TypeVar("MessageType")
 
 DispatchType = TypeVar("DispatchType")
 DispatchMessage = TypeVar("DispatchMessage")
-DispatchFunc = Callable[[DispatchMessage], Awaitable[None]]
+DispatchFunc = Callable[[DispatchMessage], Union[None, Awaitable[None]]]
+DispatchFilterFunc = Callable[[DispatchMessage], bool]
+
+
+def _no_filter(message: MessageType) -> bool:
+    return True
 
 
 async def heartbeater(
@@ -64,11 +80,18 @@ class MessageDispatcher(Generic[DispatchType, DispatchMessage]):
 
     def __init__(self):
         """Initialize a new MessageDispatcher instance."""
-        self.__listeners: Dict[DispatchType, List[DispatchFunc]] = {}
-
-    def listen_to(self, dispatch_type: DispatchType, func: DispatchFunc) -> None:
+        self.__listeners: Dict[
+            DispatchType, List[Tuple[DispatchFilterFunc, DispatchFunc]]
+        ] = {}
+
+    def listen_to(
+        self,
+        dispatch_type: DispatchType,
+        func: DispatchFunc,
+        message_filter: DispatchFilterFunc = _no_filter,
+    ) -> None:
         """Listen to a specific type of message type."""
-        self.__listeners.setdefault(dispatch_type, []).append(func)
+        self.__listeners.setdefault(dispatch_type, []).append((message_filter, func))
 
     def dispatch(
         self, dispatch_type: DispatchType, message: DispatchMessage
@@ -86,12 +109,16 @@ class MessageDispatcher(Generic[DispatchType, DispatchMessage]):
                 _LOGGER.exception("error during dispatch")
 
         tasks = []
-        for func in self.__listeners.get(dispatch_type, []):
+        loop = asyncio.get_event_loop()
+        listeners = self.__listeners.get(dispatch_type, [])
+        for func in [func for filter_func, func in listeners if filter_func(message)]:
             _LOGGER.debug(
                 "Dispatching message with type %s to %s",
                 dispatch_type,
                 func,
             )
-            tasks.append(asyncio.ensure_future(_call_listener(func(message))))
-
+            if inspect.iscoroutinefunction(func):
+                tasks.append(asyncio.ensure_future(_call_listener(func(message))))
+            else:
+                loop.call_soon(func, message)
         return tasks
diff --git a/pyatv/core/relayer.py b/pyatv/core/relayer.py
index beb8b758..84ca1ca8 100644
--- a/pyatv/core/relayer.py
+++ b/pyatv/core/relayer.py
@@ -61,6 +61,14 @@ class Relayer(Generic[T]):
                 return self._interfaces[priority]
         raise exceptions.NotSupportedError()
 
+    @property
+    def main_protocol(self) -> Optional[Protocol]:
+        """Return Protocol for main instance."""
+        for priority in chain(self._takeover_protocol, self._priorities):
+            if priority in self._interfaces:
+                return priority
+        return None
+
     @property
     def instances(self) -> Sequence[T]:
         """Return all instances added to this relayer."""
diff --git a/pyatv/interface.py b/pyatv/interface.py
index f3abb0a8..509e93bc 100644
--- a/pyatv/interface.py
+++ b/pyatv/interface.py
@@ -728,14 +728,11 @@ class PushListener(ABC):
 class PushUpdater(ABC, StateProducer):
     """Base class for push/async updates from an Apple TV.
 
-    Listener interface: `pyatv.interface.PushListener`
-    """
+    A `pyatv.interface.PushUpdater` shall only publish update in case the state
+    actually changes.
 
-    def __init__(self, loop: asyncio.AbstractEventLoop):
-        """Initialize a new PushUpdater."""
-        super().__init__()
-        self.loop = loop
-        self._previous_state: Optional[Playing] = None
+    Listener interface: `pyatv.interface.PushListener`.
+    """
 
     @property
     @abstractmethod
@@ -757,13 +754,6 @@ class PushUpdater(ABC, StateProducer):
         """No longer forward updates to listener."""
         raise NotImplementedError
 
-    def post_update(self, playing: Playing) -> None:
-        """Post an update to listener."""
-        if playing != self._previous_state:
-            self.loop.call_soon(self.listener.playstatus_update, self, playing)
-
-        self._previous_state = playing
-
 
 class Stream:  # pylint: disable=too-few-public-methods
     """Base class for stream functionality."""
diff --git a/pyatv/protocols/__init__.py b/pyatv/protocols/__init__.py
index 62cf0291..0d3ab497 100644
--- a/pyatv/protocols/__init__.py
+++ b/pyatv/protocols/__init__.py
@@ -4,7 +4,12 @@ from typing import Any, Awaitable, Callable, Dict, Generator, Mapping, NamedTupl
 
 from pyatv import interface
 from pyatv.const import Protocol
-from pyatv.core import MutableService, SetupData, TakeoverMethod
+from pyatv.core import (
+    MutableService,
+    ProtocolStateDispatcher,
+    SetupData,
+    TakeoverMethod,
+)
 from pyatv.core.scan import ScanMethod
 from pyatv.interface import BaseService, DeviceInfo
 from pyatv.protocols import airplay as airplay_proto
@@ -23,6 +28,7 @@ SetupMethod = Callable[
         StateProducer,
         ClientSessionManager,
         TakeoverMethod,
+        ProtocolStateDispatcher,
     ],
     Generator[SetupData, None, None],
 ]
diff --git a/pyatv/protocols/airplay/__init__.py b/pyatv/protocols/airplay/__init__.py
index d9db21b7..a2461d1b 100644
--- a/pyatv/protocols/airplay/__init__.py
+++ b/pyatv/protocols/airplay/__init__.py
@@ -8,7 +8,13 @@ from typing import Any, Dict, Generator, Mapping, Optional, Set
 from pyatv import exceptions
 from pyatv.auth.hap_pairing import AuthenticationType, HapCredentials, parse_credentials
 from pyatv.const import DeviceModel, FeatureName, PairingRequirement, Protocol
-from pyatv.core import MutableService, SetupData, TakeoverMethod, mdns
+from pyatv.core import (
+    MutableService,
+    ProtocolStateDispatcher,
+    SetupData,
+    TakeoverMethod,
+    mdns,
+)
 from pyatv.core.scan import ScanHandler, ScanHandlerReturn
 from pyatv.helpers import get_unique_id
 from pyatv.interface import (
@@ -186,6 +192,7 @@ def setup(  # pylint: disable=too-many-locals
     device_listener: StateProducer,
     session_manager: ClientSessionManager,
     takeover: TakeoverMethod,
+    state_dispatcher: ProtocolStateDispatcher,
 ) -> Generator[SetupData, None, None]:
     """Set up a new AirPlay service."""
     # TODO: Split up in connect/protocol and Stream implementation
@@ -250,6 +257,7 @@ def setup(  # pylint: disable=too-many-locals
             device_listener,
             session_manager,
             takeover,
+            state_dispatcher,
             AirPlayMrpConnection(control, device_listener),
             requires_heatbeat=False,  # Already have heartbeat on control channel
         )
diff --git a/pyatv/protocols/companion/__init__.py b/pyatv/protocols/companion/__init__.py
index c389a0b7..e11e6da6 100644
--- a/pyatv/protocols/companion/__init__.py
+++ b/pyatv/protocols/companion/__init__.py
@@ -14,7 +14,13 @@ from pyatv.const import (
     PairingRequirement,
     Protocol,
 )
-from pyatv.core import MutableService, SetupData, TakeoverMethod, mdns
+from pyatv.core import (
+    MutableService,
+    ProtocolStateDispatcher,
+    SetupData,
+    TakeoverMethod,
+    mdns,
+)
 from pyatv.core.scan import ScanHandler, ScanHandlerReturn
 from pyatv.interface import (
     App,
@@ -375,6 +381,7 @@ def setup(
     device_listener: StateProducer,
     session_manager: ClientSessionManager,
     takeover: TakeoverMethod,
+    state_dispatcher: ProtocolStateDispatcher,
 ) -> Generator[SetupData, None, None]:
     """Set up a new Companion service."""
     # Companion doesn't work without credentials, so don't setup if none exists
diff --git a/pyatv/protocols/dmap/__init__.py b/pyatv/protocols/dmap/__init__.py
index 21e1af6e..dbf4d0bf 100644
--- a/pyatv/protocols/dmap/__init__.py
+++ b/pyatv/protocols/dmap/__init__.py
@@ -21,7 +21,14 @@ from pyatv.const import (
     RepeatState,
     ShuffleState,
 )
-from pyatv.core import MutableService, SetupData, TakeoverMethod, mdns
+from pyatv.core import (
+    AbstractPushUpdater,
+    MutableService,
+    ProtocolStateDispatcher,
+    SetupData,
+    TakeoverMethod,
+    mdns,
+)
 from pyatv.core.scan import ScanHandler, ScanHandlerReturn
 from pyatv.helpers import get_unique_id
 from pyatv.interface import (
@@ -433,12 +440,14 @@ class DmapMetadata(Metadata):
         return await self.apple_tv.playstatus()
 
 
-class DmapPushUpdater(PushUpdater):
+class DmapPushUpdater(AbstractPushUpdater):
     """Implementation of API for handling push update from an Apple TV."""
 
-    def __init__(self, loop, apple_tv, listener):
+    def __init__(
+        self, apple_tv, state_dispatcher: ProtocolStateDispatcher, listener
+    ) -> None:
         """Initialize a new DmapPushUpdater instance."""
-        super().__init__(loop)
+        super().__init__(state_dispatcher)
         self._atv = apple_tv
         self._listener = weakref.ref(listener)
         self._future = None
@@ -650,6 +659,7 @@ def setup(  # pylint: disable=too-many-locals
     device_listener: StateProducer,
     session_manager: ClientSessionManager,
     takeover: TakeoverMethod,
+    state_dispatcher: ProtocolStateDispatcher,
 ) -> Generator[SetupData, None, None]:
     """Set up a new DMAP service."""
     daap_http = HttpSession(
@@ -658,7 +668,7 @@ def setup(  # pylint: disable=too-many-locals
     )
     requester = DaapRequester(daap_http, service.credentials)
     apple_tv = BaseDmapAppleTV(requester)
-    push_updater = DmapPushUpdater(loop, apple_tv, device_listener)
+    push_updater = DmapPushUpdater(apple_tv, state_dispatcher, device_listener)
     metadata = DmapMetadata(config.identifier, apple_tv)
     audio = DmapAudio(apple_tv)
 
diff --git a/pyatv/protocols/mrp/__init__.py b/pyatv/protocols/mrp/__init__.py
index 4c92a7a5..93b0405a 100644
--- a/pyatv/protocols/mrp/__init__.py
+++ b/pyatv/protocols/mrp/__init__.py
@@ -22,7 +22,14 @@ from pyatv.const import (
     RepeatState,
     ShuffleState,
 )
-from pyatv.core import MutableService, SetupData, TakeoverMethod, mdns
+from pyatv.core import (
+    AbstractPushUpdater,
+    MutableService,
+    ProtocolStateDispatcher,
+    SetupData,
+    TakeoverMethod,
+    mdns,
+)
 from pyatv.core.scan import ScanHandler, ScanHandlerReturn
 from pyatv.helpers import get_unique_id
 from pyatv.interface import (
@@ -595,12 +602,17 @@ class MrpPower(Power):
         return PowerState.Unknown
 
 
-class MrpPushUpdater(PushUpdater):
+class MrpPushUpdater(AbstractPushUpdater):
     """Implementation of API for handling push update from an Apple TV."""
 
-    def __init__(self, loop, metadata, psm):
+    def __init__(
+        self,
+        metadata: MrpMetadata,
+        psm: PlayerStateManager,
+        state_dispatcher: ProtocolStateDispatcher,
+    ) -> None:
         """Initialize a new MrpPushUpdater instance."""
-        super().__init__(loop)
+        super().__init__(state_dispatcher)
         self.metadata = metadata
         self.psm = psm
 
@@ -878,6 +890,7 @@ def create_with_connection(  # pylint: disable=too-many-locals
     device_listener: StateProducer,
     session_manager: ClientSessionManager,
     takeover: TakeoverMethod,
+    state_dispatcher: ProtocolStateDispatcher,
     connection: AbstractMrpConnection,
     requires_heatbeat: bool = True,
 ) -> SetupData:
@@ -888,7 +901,7 @@ def create_with_connection(  # pylint: disable=too-many-locals
     remote_control = MrpRemoteControl(loop, psm, protocol)
     metadata = MrpMetadata(protocol, psm, config.identifier)
     power = MrpPower(loop, protocol, remote_control)
-    push_updater = MrpPushUpdater(loop, metadata, psm)
+    push_updater = MrpPushUpdater(metadata, psm, state_dispatcher)
     audio = MrpAudio(protocol)
 
     interfaces = {
@@ -949,6 +962,7 @@ def setup(
     device_listener: StateProducer,
     session_manager: ClientSessionManager,
     takeover: TakeoverMethod,
+    state_dispatcher: ProtocolStateDispatcher,
 ) -> Generator[SetupData, None, None]:
     """Set up a new MRP service."""
     yield create_with_connection(
@@ -958,6 +972,7 @@ def setup(
         device_listener,
         session_manager,
         takeover,
+        state_dispatcher,
         MrpConnection(config.address, service.port, loop, atv=device_listener),
     )
 
diff --git a/pyatv/protocols/raop/__init__.py b/pyatv/protocols/raop/__init__.py
index 279b1458..034336af 100644
--- a/pyatv/protocols/raop/__init__.py
+++ b/pyatv/protocols/raop/__init__.py
@@ -16,7 +16,14 @@ from pyatv.const import (
     PairingRequirement,
     Protocol,
 )
-from pyatv.core import MutableService, SetupData, TakeoverMethod, mdns
+from pyatv.core import (
+    AbstractPushUpdater,
+    MutableService,
+    ProtocolStateDispatcher,
+    SetupData,
+    TakeoverMethod,
+    mdns,
+)
 from pyatv.core.scan import ScanHandler, ScanHandlerReturn
 from pyatv.helpers import get_unique_id
 from pyatv.interface import (
@@ -61,12 +68,12 @@ PERCENTAGE_MIN = 0.0
 PERCENTAGE_MAX = 100.0
 
 
-class RaopPushUpdater(PushUpdater):
+class RaopPushUpdater(AbstractPushUpdater):
     """Implementation of push update support for RAOP."""
 
-    def __init__(self, metadata: Metadata, loop: asyncio.AbstractEventLoop):
+    def __init__(self, metadata: Metadata, state_dispatcher: ProtocolStateDispatcher):
         """Initialize a new RaopPushUpdater instance."""
-        super().__init__(loop)
+        super().__init__(state_dispatcher)
         self._activated = False
         self.metadata = metadata
 
@@ -467,11 +474,12 @@ def setup(  # pylint: disable=too-many-locals
     device_listener: StateProducer,
     session_manager: ClientSessionManager,
     takeover: TakeoverMethod,
+    state_dispatcher: ProtocolStateDispatcher,
 ) -> Generator[SetupData, None, None]:
     """Set up a new RAOP service."""
     playback_manager = RaopPlaybackManager(str(config.address), service.port)
     metadata = RaopMetadata(playback_manager)
-    push_updater = RaopPushUpdater(metadata, loop)
+    push_updater = RaopPushUpdater(metadata, state_dispatcher)
 
     class RaopStateListener(RaopListener):
         """Listener for RAOP state changes."""


Test patch:
diff --git a/tests/core/test_core.py b/tests/core/test_core.py
new file mode 100644
index 00000000..fcfee25a
--- /dev/null
+++ b/tests/core/test_core.py
@@ -0,0 +1,61 @@
+"""Unit tests for pyatv.core."""
+from unittest.mock import ANY, MagicMock
+
+import pytest
+
+from pyatv.const import Protocol
+from pyatv.core import (
+    AbstractPushUpdater,
+    CoreStateDispatcher,
+    ProtocolStateDispatcher,
+    StateMessage,
+    UpdatedState,
+)
+from pyatv.interface import Playing
+
+
+@pytest.fixture(name="state_dispatcher")
+def state_dispatcher_fixture():
+    core_dispatcher = CoreStateDispatcher()
+    yield ProtocolStateDispatcher(Protocol.MRP, core_dispatcher)
+
+
+class PushUpdaterDummy(AbstractPushUpdater):
+    def active(self) -> bool:
+        """Return if push updater has been started."""
+        raise exceptions.NotSupportedError()
+
+    def start(self, initial_delay: int = 0) -> None:
+        """Begin to listen to updates.
+
+        If an error occurs, start must be called again.
+        """
+        raise exceptions.NotSupportedError()
+
+    def stop(self) -> None:
+        """No longer forward updates to listener."""
+        raise exceptions.NotSupportedError()
+
+
+@pytest.mark.parametrize("updates", [1, 2, 3])
+def test_post_ignore_duplicate_update(event_loop, state_dispatcher, updates):
+    listener = MagicMock()
+    playing = Playing()
+
+    def _state_changed(message):
+        assert message.protocol == Protocol.MRP
+        assert message.state == UpdatedState.Playing
+        listener.state_updated(message.value)
+
+    async def _post_updates(repeats: int):
+        updater = PushUpdaterDummy(state_dispatcher)
+        updater.listener = listener
+        state_dispatcher.listen_to(UpdatedState.Playing, _state_changed)
+        for _ in range(repeats):
+            updater.post_update(playing)
+
+    event_loop.run_until_complete(_post_updates(updates))
+
+    assert listener.playstatus_update.call_count == 1
+    listener.playstatus_update.assert_called_once_with(ANY, playing)
+    listener.state_updated.assert_called_once_with(playing)
diff --git a/tests/core/test_facade.py b/tests/core/test_facade.py
index 44a22f5d..6e4bfd6c 100644
--- a/tests/core/test_facade.py
+++ b/tests/core/test_facade.py
@@ -1,4 +1,5 @@
 """Unit tests for pyatv.core.facade."""
+from abc import ABC
 import asyncio
 import inspect
 from ipaddress import IPv4Address
@@ -10,8 +11,22 @@ import pytest
 
 from pyatv import exceptions
 from pyatv.conf import AppleTV
-from pyatv.const import DeviceState, FeatureName, MediaType, OperatingSystem, Protocol
-from pyatv.core import SetupData
+from pyatv.const import (
+    DeviceState,
+    FeatureName,
+    MediaType,
+    OperatingSystem,
+    PowerState,
+    Protocol,
+)
+from pyatv.core import (
+    AbstractPushUpdater,
+    CoreStateDispatcher,
+    ProtocolStateDispatcher,
+    SetupData,
+    StateMessage,
+    UpdatedState,
+)
 from pyatv.core.facade import FacadeAppleTV, SetupData
 from pyatv.interface import (
     Audio,
@@ -22,6 +37,7 @@ from pyatv.interface import (
     FeatureState,
     Playing,
     Power,
+    PowerListener,
     PushListener,
     PushUpdater,
     Stream,
@@ -34,10 +50,25 @@ TEST_URL = "http://test"
 pytestmark = pytest.mark.asyncio
 
 
+@pytest.fixture(name="core_dispatcher")
+def core_dispatcher():
+    yield CoreStateDispatcher()
+
+
+@pytest.fixture(name="mrp_state_dispatcher")
+def mrp_state_dispatcher_fixture(core_dispatcher):
+    yield ProtocolStateDispatcher(Protocol.MRP, core_dispatcher)
+
+
+@pytest.fixture(name="dmap_state_dispatcher")
+def dmap_state_dispatcher_fixture(core_dispatcher):
+    yield ProtocolStateDispatcher(Protocol.DMAP, core_dispatcher)
+
+
 @pytest.fixture(name="facade_dummy")
-def facade_dummy_fixture(session_manager):
+def facade_dummy_fixture(session_manager, core_dispatcher):
     conf = AppleTV(IPv4Address("127.0.0.1"), "Test")
-    facade = FacadeAppleTV(conf, session_manager)
+    facade = FacadeAppleTV(conf, session_manager, core_dispatcher)
     yield facade
 
 
@@ -86,26 +117,16 @@ class DummyFeatures(Features):
         )
 
 
-class DummyPushUpdater(PushUpdater):
-    def __init__(self, loop):
-        super().__init__(loop)
-
-    @property
-    def active(self) -> bool:
-        return False
-
-    def start(self, initial_delay: int = 0) -> None:
-        raise exceptions.NotSupportedError
-
-    def stop(self) -> None:
-        raise exceptions.NotSupportedError
-
-
 class DummyPower(Power):
     def __init__(self) -> None:
+        self.current_state = PowerState.Off
         self.turn_on_called = False
         self.turn_off_called = False
 
+    @property
+    def power_state(self):
+        return self.current_state
+
     async def turn_on(self, await_new_state: bool = False) -> None:
         self.turn_on_called = True
 
@@ -145,9 +166,9 @@ class DummyStream(Stream):
         self.url = url
 
 
-class DummyPushUpdater(PushUpdater):
-    def __init__(self, loop: asyncio.AbstractEventLoop):
-        super().__init__(loop)
+class DummyPushUpdater(AbstractPushUpdater):
+    def __init__(self, state_dispatcher):
+        super().__init__(state_dispatcher)
         self._active = False
 
     @property
@@ -174,10 +195,26 @@ class SavingPushListener(PushListener):
         pass
 
 
+class SavingPowerListener(PowerListener):
+    def __init__(self):
+        self.last_update = None
+        self.all_updates = []
+
+    def powerstate_update(self, old_state: PowerState, new_state: PowerState):
+        """Device power state was updated."""
+        self.last_update = new_state
+        self.all_updates.append(new_state)
+
+
 @pytest.fixture(name="register_interface")
 def register_interface_fixture(facade_dummy):
     def _register_func(feature: FeatureName, instance, protocol: Protocol):
-        interface = inspect.getmro(type(instance))[1]
+        # Find the first derived interface from pyatv.interface module
+        interface = next(
+            iface
+            for iface in inspect.getmro(type(instance))
+            if iface.__module__ == "pyatv.interface"
+        )
         sdg = SetupDataGenerator(protocol, feature)
         sdg.interfaces[interface] = instance
         facade_dummy.add_protocol(sdg.get_setup_data())
@@ -269,33 +306,23 @@ async def test_features_feature_overlap_uses_priority(facade_dummy, register_int
     )
 
 
-async def test_features_push_updates(facade_dummy, event_loop, register_interface):
+async def test_features_push_updates(
+    facade_dummy, register_interface, mrp_state_dispatcher
+):
     feat = facade_dummy.features
 
     assert feat.get_feature(FeatureName.PushUpdates).state == FeatureState.Unsupported
 
     register_interface(
-        FeatureName.PushUpdates, DummyPushUpdater(event_loop), Protocol.MRP
+        FeatureName.PushUpdates,
+        DummyPushUpdater(mrp_state_dispatcher),
+        Protocol.MRP,
     )
 
     await facade_dummy.connect()
     assert feat.get_feature(FeatureName.PushUpdates).state == FeatureState.Available
 
 
-@pytest.mark.parametrize(
-    "feature,func", [(FeatureName.TurnOn, "turn_on"), (FeatureName.TurnOff, "turn_off")]
-)
-async def test_power_prefer_companion(feature, func, facade_dummy, register_interface):
-    power_mrp, _ = register_interface(feature, DummyPower(), Protocol.MRP)
-    power_comp, _ = register_interface(feature, DummyPower(), Protocol.Companion)
-
-    await facade_dummy.connect()
-    await getattr(facade_dummy.power, func)()
-
-    assert not getattr(power_mrp, f"{func}_called")
-    assert getattr(power_comp, f"{func}_called")
-
-
 @pytest.mark.parametrize("volume", [-0.1, 100.1])
 async def test_audio_get_volume_out_of_range(facade_dummy, register_interface, volume):
     register_interface(FeatureName.Volume, DummyAudio(volume), Protocol.RAOP)
@@ -539,12 +566,17 @@ async def test_takeover_failure_restores(facade_dummy, register_interface):
     assert sdg_mrp.interfaces[Stream].url == "test"
 
 
-async def test_takeover_push_updates(facade_dummy, register_interface, event_loop):
+async def test_takeover_push_updates(
+    facade_dummy, register_interface, mrp_state_dispatcher, dmap_state_dispatcher
+):
     listener = SavingPushListener()
 
+    dmap_pusher = DummyPushUpdater(dmap_state_dispatcher)
+    mrp_pusher = DummyPushUpdater(mrp_state_dispatcher)
+
     async def _perform_update(expected_updates, expected_protocol):
-        raop_pusher.post_update(
-            Playing(MediaType.Music, DeviceState.Idle, title=f"raop_{expected_updates}")
+        dmap_pusher.post_update(
+            Playing(MediaType.Music, DeviceState.Idle, title=f"dmap_{expected_updates}")
         )
         mrp_pusher.post_update(
             Playing(MediaType.Music, DeviceState.Idle, title=f"mrp_{expected_updates}")
@@ -553,10 +585,7 @@ async def test_takeover_push_updates(facade_dummy, register_interface, event_loo
         await until(lambda: listener.no_of_updates == expected_updates)
         assert listener.last_update.title == f"{expected_protocol}_{expected_updates}"
 
-    raop_pusher = DummyPushUpdater(event_loop)
-    mrp_pusher = DummyPushUpdater(event_loop)
-
-    register_interface(FeatureName.PushUpdates, raop_pusher, Protocol.RAOP)
+    register_interface(FeatureName.PushUpdates, dmap_pusher, Protocol.DMAP)
     register_interface(FeatureName.PushUpdates, mrp_pusher, Protocol.MRP)
 
     await facade_dummy.connect()
@@ -568,10 +597,10 @@ async def test_takeover_push_updates(facade_dummy, register_interface, event_loo
     # priority is used, so update from MRP should be delivered.
     await _perform_update(1, "mrp")
 
-    takeover_release = facade_dummy.takeover(Protocol.RAOP, PushUpdater)
+    takeover_release = facade_dummy.takeover(Protocol.DMAP, PushUpdater)
 
     # After takeover, push update from RAOP should be delivered instead
-    await _perform_update(2, "raop")
+    await _perform_update(2, "dmap")
 
     takeover_release()
 
@@ -583,26 +612,186 @@ async def test_takeover_push_updates(facade_dummy, register_interface, event_loo
 # not be pushed when performing a takeover (as the protocol taken over was never
 # started)
 async def test_start_stop_all_push_updaters(
-    facade_dummy, register_interface, event_loop
+    facade_dummy, register_interface, mrp_state_dispatcher, dmap_state_dispatcher
 ):
-    raop_pusher = DummyPushUpdater(event_loop)
-    mrp_pusher = DummyPushUpdater(event_loop)
+    dmap_pusher = DummyPushUpdater(dmap_state_dispatcher)
+    mrp_pusher = DummyPushUpdater(mrp_state_dispatcher)
 
-    register_interface(FeatureName.PushUpdates, raop_pusher, Protocol.RAOP)
+    register_interface(FeatureName.PushUpdates, dmap_pusher, Protocol.DMAP)
     register_interface(FeatureName.PushUpdates, mrp_pusher, Protocol.MRP)
 
     await facade_dummy.connect()
     push_updater = facade_dummy.push_updater
 
-    assert not raop_pusher.active
+    assert not dmap_pusher.active
     assert not mrp_pusher.active
 
     push_updater.start()
 
-    assert raop_pusher.active
+    assert dmap_pusher.active
     assert mrp_pusher.active
 
     push_updater.stop()
 
-    assert not raop_pusher.active
+    assert not dmap_pusher.active
     assert not mrp_pusher.active
+
+
+# POWER RELATED TESTS
+
+
+@pytest.mark.parametrize(
+    "feature,func", [(FeatureName.TurnOn, "turn_on"), (FeatureName.TurnOff, "turn_off")]
+)
+async def test_power_prefer_companion(feature, func, facade_dummy, register_interface):
+    power_mrp, _ = register_interface(feature, DummyPower(), Protocol.MRP)
+    power_comp, _ = register_interface(feature, DummyPower(), Protocol.Companion)
+
+    await facade_dummy.connect()
+    await getattr(facade_dummy.power, func)()
+
+    assert not getattr(power_mrp, f"{func}_called")
+    assert getattr(power_comp, f"{func}_called")
+
+
+@pytest.fixture(name="power_instance")
+async def power_instance_fixture():
+    yield DummyPower()
+
+
+@pytest.fixture(name="power_setup")
+async def power_setup_fixture(facade_dummy, register_interface, power_instance):
+    listener = SavingPowerListener()
+
+    register_interface(FeatureName.PowerState, power_instance, Protocol.MRP)
+
+    await facade_dummy.connect()
+    facade_dummy.power.listener = listener
+
+    yield facade_dummy.power
+
+
+async def dispatch_device_state(mrp_state_dispatcher, state, protocol=Protocol.MRP):
+    event = asyncio.Event()
+
+    # Add a listener last in the last and make it set an asyncio.Event. That way we
+    # can synchronize and know that all other listeners have been called.
+    mrp_state_dispatcher.listen_to(UpdatedState.Playing, lambda message: event.set())
+    mrp_state_dispatcher.dispatch(
+        UpdatedState.Playing, Playing(MediaType.Unknown, state)
+    )
+
+    await event.wait()
+
+
+async def test_power_state_respect_playing_state(mrp_state_dispatcher, power_setup):
+    assert power_setup.power_state == PowerState.Off
+
+    # Trigger something to play (but keep power state off) changes it to On
+    await dispatch_device_state(mrp_state_dispatcher, DeviceState.Playing)
+    power_setup.listener.last_update == PowerState.On
+    power_setup.power_state == PowerState.On
+
+    # Trigger back to idle shall give power state Off again
+    await dispatch_device_state(mrp_state_dispatcher, DeviceState.Idle)
+    power_setup.listener.last_update == PowerState.Off
+    power_setup.power_state == PowerState.Off
+
+
+async def test_power_state_defaults_to_derived_power_state_from_off(
+    mrp_state_dispatcher, power_setup, power_instance
+):
+    assert power_setup.power_state == PowerState.Off
+
+    # Trigger something to play (but keep power state off) changes it to On
+    await dispatch_device_state(mrp_state_dispatcher, DeviceState.Playing)
+    power_setup.listener.last_update == PowerState.On
+
+    # Change derived power state to On
+    power_instance.current_state = PowerState.On
+
+    # Trigger back to idle shall give power state On as derived state is On
+    await dispatch_device_state(mrp_state_dispatcher, DeviceState.Idle)
+    power_setup.power_state == PowerState.On
+
+
+async def test_power_state_defaults_to_derived_power_state_from_on(
+    mrp_state_dispatcher, power_setup, power_instance
+):
+    power_instance.current_state = PowerState.On
+
+    # Trigger something to play (but keep power state off) changes it to On
+    await dispatch_device_state(mrp_state_dispatcher, DeviceState.Playing)
+    power_setup.listener.last_update == PowerState.On
+
+    # Trigger back to idle shall give power state On as derived state is On
+    await dispatch_device_state(mrp_state_dispatcher, DeviceState.Idle)
+    power_setup.power_state == PowerState.On
+
+
+async def test_power_play_state_only_from_main_protocol(
+    mrp_state_dispatcher, dmap_state_dispatcher, power_setup
+):
+    assert power_setup.power_state == PowerState.Off
+
+    # The initial Playing (DMAP) shall be ignored so we should only get On state
+    # since duplicates are not propagated
+    await dispatch_device_state(
+        dmap_state_dispatcher, DeviceState.Playing, protocol=Protocol.DMAP
+    )
+    assert power_setup.listener.last_update is None
+    await dispatch_device_state(
+        mrp_state_dispatcher, DeviceState.Idle, protocol=Protocol.MRP
+    )
+    assert power_setup.listener.last_update is None
+    await dispatch_device_state(
+        mrp_state_dispatcher, DeviceState.Playing, protocol=Protocol.MRP
+    )
+    assert power_setup.listener.last_update == PowerState.On
+
+
+async def test_power_play_state_no_dispatcher_duplicates(
+    mrp_state_dispatcher, power_setup
+):
+    assert power_setup.power_state == PowerState.Off
+
+    # Dispatch two "On" and one "Off". On shall only be registered once.
+    await dispatch_device_state(mrp_state_dispatcher, DeviceState.Playing)
+    await dispatch_device_state(mrp_state_dispatcher, DeviceState.Playing)
+    await dispatch_device_state(mrp_state_dispatcher, DeviceState.Idle)
+    assert power_setup.listener.last_update == PowerState.Off
+
+    assert len(power_setup.listener.all_updates) == 2
+
+
+async def test_power_play_state_no_listener_duplicates(
+    mrp_state_dispatcher, power_setup
+):
+    assert power_setup.power_state == PowerState.Off
+
+    # Dispatch "On" and then trigger a listener update with "On", which should not
+    # yield any additional update
+    await dispatch_device_state(mrp_state_dispatcher, DeviceState.Playing)
+    power_setup.powerstate_update(PowerState.Off, PowerState.On)
+
+    assert len(power_setup.listener.all_updates) == 1
+    assert power_setup.listener.last_update == PowerState.On
+
+
+async def test_power_no_updates_without_power_instance(
+    mrp_state_dispatcher, facade_dummy, register_interface
+):
+    listener = SavingPowerListener()
+
+    # Some kind of interface is needed to connect (arbitrary but not Power)
+    register_interface(FeatureName.Play, DummyFeatures(FeatureName.Play), Protocol.DMAP)
+
+    await facade_dummy.connect()
+    facade_dummy.power.listener = listener
+
+    # Defensive to make sure we have no instance
+    with pytest.raises(exceptions.NotSupportedError):
+        facade_dummy.power.power_state
+
+    await dispatch_device_state(mrp_state_dispatcher, DeviceState.Playing)
+    assert listener.last_update is None
diff --git a/tests/core/test_protocol.py b/tests/core/test_protocol.py
index 9b2d74ec..1686fafe 100644
--- a/tests/core/test_protocol.py
+++ b/tests/core/test_protocol.py
@@ -143,7 +143,7 @@ async def test_simple_dispatch():
         nonlocal dispatched_msg1
         dispatched_msg1 = message
 
-    async def dispatch_func2(message: bytes) -> None:
+    def dispatch_func2(message: bytes) -> None:
         nonlocal dispatched_msg2
         dispatched_msg2 = message
 
@@ -160,3 +160,22 @@ async def test_simple_dispatch():
     await asyncio.wait_for(asyncio.gather(*dispatcher.dispatch(2, b"456")), 5.0)
     assert dispatched_msg1 is None
     assert dispatched_msg2 == b"456"
+
+
+async def test_dispatch_with_filter():
+    dispatched_value = None
+
+    async def dispatch_func(value: int) -> None:
+        nonlocal dispatched_value
+        dispatched_value = value
+
+    dispatcher = MessageDispatcher[int, int]()
+    dispatcher.listen_to(
+        1, dispatch_func, message_filter=lambda message: (message % 2) == 0
+    )
+
+    await asyncio.wait_for(asyncio.gather(*dispatcher.dispatch(1, 1)), 5.0)
+    assert dispatched_value == None
+
+    await asyncio.wait_for(asyncio.gather(*dispatcher.dispatch(1, 2)), 5.0)
+    assert dispatched_value == 2
diff --git a/tests/core/test_relayer.py b/tests/core/test_relayer.py
index b4491ffc..c63f7e26 100644
--- a/tests/core/test_relayer.py
+++ b/tests/core/test_relayer.py
@@ -157,6 +157,34 @@ def test_main_instance_missing_instance_for_priority():
         relayer.main_instance
 
 
+def test_main_protocol():
+    relayer = Relayer(BaseClass, [Protocol.MRP, Protocol.DMAP, Protocol.AirPlay])
+
+    relayer.register(SubClass1(), Protocol.AirPlay)
+    assert relayer.main_protocol == Protocol.AirPlay
+
+    relayer.register(SubClass1(), Protocol.DMAP)
+    assert relayer.main_protocol == Protocol.DMAP
+
+    relayer.register(SubClass1(), Protocol.MRP)
+    assert relayer.main_protocol == Protocol.MRP
+
+
+def test_main_protocol_with_no_instance():
+    relayer = Relayer(BaseClass, [Protocol.MRP])
+    assert relayer.main_protocol is None
+
+
+def test_takeover_overrides_main_protocol():
+    relayer = Relayer(BaseClass, [Protocol.MRP, Protocol.DMAP])
+    relayer.register(SubClass4("mrp"), Protocol.MRP)
+    relayer.register(SubClass4("dmap"), Protocol.DMAP)
+
+    relayer.takeover(Protocol.DMAP)
+
+    assert relayer.main_protocol == Protocol.DMAP
+
+
 def test_get_instance_of_type():
     instance1 = SubClass1()
     instance2 = SubClass2()
diff --git a/tests/test_interface.py b/tests/test_interface.py
index eaffc34b..479128af 100644
--- a/tests/test_interface.py
+++ b/tests/test_interface.py
@@ -81,23 +81,6 @@ class FeaturesDummy(interface.Features):
         return FeatureInfo(state=state)
 
 
-class PushUpdaterDummy(interface.PushUpdater):
-    def active(self) -> bool:
-        """Return if push updater has been started."""
-        raise exceptions.NotSupportedError()
-
-    def start(self, initial_delay: int = 0) -> None:
-        """Begin to listen to updates.
-
-        If an error occurs, start must be called again.
-        """
-        raise exceptions.NotSupportedError()
-
-    def stop(self) -> None:
-        """No longer forward updates to listener."""
-        raise exceptions.NotSupportedError()
-
-
 @pytest.fixture
 def methods():
     yield interface.retrieve_commands(TestClass)
@@ -444,23 +427,3 @@ def test_app_equality():
     assert App(None, "test") != App(None, None)
     assert App(None, "test") == App(None, "test")
     assert App("test", "test2") == App("test", "test2")
-
-
-# PUSH UPDATER
-
-
-@pytest.mark.parametrize("updates", [1, 2, 3])
-def test_post_ignore_duplicate_update(event_loop, updates):
-    listener = MagicMock()
-    playing = Playing()
-
-    async def _post_updates(repeats: int):
-        updater = PushUpdaterDummy(event_loop)
-        updater.listener = listener
-        for _ in range(repeats):
-            updater.post_update(playing)
-
-    event_loop.run_until_complete(_post_updates(updates))
-
-    assert listener.playstatus_update.call_count == 1
-    listener.playstatus_update.assert_called_once_with(ANY, playing)