From 92c6cee2a1796d4180e795e9d6c848ba0770c39a Mon Sep 17 00:00:00 2001
From: Bjarni Ivarsson <bjarniivarsson@gmail.com>
Date: Mon, 28 Nov 2016 02:45:49 +0100
Subject: [PATCH] Support for media_position property on media_player (#4172)

* Added support for media_position property to media_player + implementation for sonos.

* Pla yback progress now updates without needed state transitions in HA.

* Linting fixes

* media_position_update_at property is now a datetime.

* Minor fix.

* Linting fixes.
---
 .../components/media_player/__init__.py       | 17 +++++
 .../components/media_player/sonos.py          | 70 ++++++++++++++++++-
 2 files changed, 86 insertions(+), 1 deletion(-)

diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py
index c9df431965b..d0cacd47b75 100644
--- a/homeassistant/components/media_player/__init__.py
+++ b/homeassistant/components/media_player/__init__.py
@@ -59,6 +59,8 @@ ATTR_MEDIA_SEEK_POSITION = 'seek_position'
 ATTR_MEDIA_CONTENT_ID = 'media_content_id'
 ATTR_MEDIA_CONTENT_TYPE = 'media_content_type'
 ATTR_MEDIA_DURATION = 'media_duration'
+ATTR_MEDIA_POSITION = 'media_position'
+ATTR_MEDIA_POSITION_UPDATED_AT = 'media_position_updated_at'
 ATTR_MEDIA_TITLE = 'media_title'
 ATTR_MEDIA_ARTIST = 'media_artist'
 ATTR_MEDIA_ALBUM_NAME = 'media_album_name'
@@ -120,6 +122,8 @@ ATTR_TO_PROPERTY = [
     ATTR_MEDIA_CONTENT_ID,
     ATTR_MEDIA_CONTENT_TYPE,
     ATTR_MEDIA_DURATION,
+    ATTR_MEDIA_POSITION,
+    ATTR_MEDIA_POSITION_UPDATED_AT,
     ATTR_MEDIA_TITLE,
     ATTR_MEDIA_ARTIST,
     ATTR_MEDIA_ALBUM_NAME,
@@ -447,6 +451,19 @@ class MediaPlayerDevice(Entity):
         """Duration of current playing media in seconds."""
         return None
 
+    @property
+    def media_position(self):
+        """Position of current playing media in seconds."""
+        return None
+
+    @property
+    def media_position_updated_at(self):
+        """When was the position of the current playing media valid.
+
+        Returns value from homeassistant.util.dt.utcnow().
+        """
+        return None
+
     @property
     def media_image_url(self):
         """Image url of current playing media."""
diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py
index 5b070e1f5bd..022e5742ee7 100644
--- a/homeassistant/components/media_player/sonos.py
+++ b/homeassistant/components/media_player/sonos.py
@@ -20,6 +20,7 @@ from homeassistant.const import (
     STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_OFF, ATTR_ENTITY_ID)
 from homeassistant.config import load_yaml_config_file
 import homeassistant.helpers.config_validation as cv
+from homeassistant.util.dt import utcnow
 
 REQUIREMENTS = ['SoCo==0.12']
 
@@ -264,6 +265,8 @@ class SonosDevice(MediaPlayerDevice):
         self._coordinator = None
         self._media_content_id = None
         self._media_duration = None
+        self._media_position = None
+        self._media_position_updated_at = None
         self._media_image_url = None
         self._media_artist = None
         self._media_album_name = None
@@ -404,6 +407,9 @@ class SonosDevice(MediaPlayerDevice):
                 media_album_name = track_info.get('album')
                 media_title = track_info.get('title')
 
+                media_position = None
+                media_position_updated_at = None
+
                 is_radio_stream = \
                     current_media_uri.startswith('x-sonosapi-stream:') or \
                     current_media_uri.startswith('x-rincon-mp3radio:')
@@ -425,7 +431,6 @@ class SonosDevice(MediaPlayerDevice):
                     media_image_url = None
 
                 elif is_radio_stream:
-                    is_radio_stream = True
                     media_image_url = self._format_media_image_url(
                         current_media_uri
                     )
@@ -489,6 +494,46 @@ class SonosDevice(MediaPlayerDevice):
                     support_next_track = True
                     support_pause = True
 
+                    position_info = self._player.avTransport.GetPositionInfo(
+                        [('InstanceID', 0),
+                         ('Channel', 'Master')]
+                    )
+                    rel_time = _parse_timespan(
+                        position_info.get("RelTime")
+                    )
+
+                    # player no longer reports position?
+                    update_media_position = rel_time is None and \
+                        self._media_position is not None
+
+                    # player started reporting position?
+                    update_media_position |= rel_time is not None and \
+                        self._media_position is None
+
+                    # position changed?
+                    if rel_time is not None and \
+                       self._media_position is not None:
+
+                        time_diff = utcnow() - self._media_position_updated_at
+                        time_diff = time_diff.total_seconds()
+
+                        calculated_position = \
+                            self._media_position + \
+                            time_diff
+
+                        update_media_position = \
+                            abs(calculated_position - rel_time) > 1.5
+
+                    if update_media_position:
+                        media_position = rel_time
+                        media_position_updated_at = utcnow()
+                    else:
+                        # don't update media_position (don't want unneeded
+                        # state transitions)
+                        media_position = self._media_position
+                        media_position_updated_at = \
+                            self._media_position_updated_at
+
                     playlist_position = track_info.get('playlist_position')
                     if playlist_position in ('', 'NOT_IMPLEMENTED', None):
                         playlist_position = None
@@ -514,6 +559,8 @@ class SonosDevice(MediaPlayerDevice):
                 self._media_duration = _parse_timespan(
                     track_info.get('duration')
                 )
+                self._media_position = media_position
+                self._media_position_updated_at = media_position_updated_at
                 self._media_image_url = media_image_url
                 self._media_artist = media_artist
                 self._media_album_name = media_album_name
@@ -541,6 +588,8 @@ class SonosDevice(MediaPlayerDevice):
             self._coordinator = None
             self._media_content_id = None
             self._media_duration = None
+            self._media_position = None
+            self._media_position_updated_at = None
             self._media_image_url = None
             self._media_artist = None
             self._media_album_name = None
@@ -642,6 +691,25 @@ class SonosDevice(MediaPlayerDevice):
         else:
             return self._media_duration
 
+    @property
+    def media_position(self):
+        """Position of current playing media in seconds."""
+        if self._coordinator:
+            return self._coordinator.media_position
+        else:
+            return self._media_position
+
+    @property
+    def media_position_updated_at(self):
+        """When was the position of the current playing media valid.
+
+        Returns value from homeassistant.util.dt.utcnow().
+        """
+        if self._coordinator:
+            return self._coordinator.media_position_updated_at
+        else:
+            return self._media_position_updated_at
+
     @property
     def media_image_url(self):
         """Image url of current playing media."""
-- 
GitLab