From 73bb9df85faed1d38f4dfa5a3e0004a46437a912 Mon Sep 17 00:00:00 2001
From: Reinerlein <>
Date: Sun, 14 May 2017 11:18:12 +0000
Subject: [PATCH] Sonos: New Features and bugfixes. See Changelog.
git-svn-id: https://svn.fhem.de/fhem/trunk@14279 2b470e98-0d58-463d-a4d8-8e2adae1ed80
---
fhem/FHEM/00_SONOS.pm | 969 ++++++++++++++----
fhem/FHEM/21_SONOSPLAYER.pm | 169 ++-
fhem/FHEM/lib/UPnP/ControlPoint.pm | 24 +-
.../lib/UPnP/sonos_bibliothek_quadratic.jpg | Bin 0 -> 2808 bytes
fhem/FHEM/lib/UPnP/sonos_bibliothek_round.png | Bin 0 -> 2817 bytes
fhem/FHEM/lib/UPnP/sonos_dock_quadratic.jpg | Bin 0 -> 5416 bytes
fhem/FHEM/lib/UPnP/sonos_dock_round.png | Bin 0 -> 5738 bytes
fhem/FHEM/lib/UPnP/sonos_leer.gif | Bin 0 -> 814 bytes
fhem/FHEM/lib/UPnP/sonos_linein_quadratic.jpg | Bin 0 -> 2948 bytes
fhem/FHEM/lib/UPnP/sonos_linein_round.png | Bin 0 -> 1988 bytes
.../FHEM/lib/UPnP/sonos_playbar_quadratic.jpg | Bin 0 -> 5306 bytes
fhem/FHEM/lib/UPnP/sonos_playbar_round.png | Bin 0 -> 9633 bytes
fhem/FHEM/lib/UPnP/sonos_tunein_quadratic.jpg | Bin 0 -> 2779 bytes
fhem/FHEM/lib/UPnP/sonos_tunein_round.png | Bin 0 -> 1979 bytes
14 files changed, 956 insertions(+), 206 deletions(-)
create mode 100644 fhem/FHEM/lib/UPnP/sonos_bibliothek_quadratic.jpg
create mode 100644 fhem/FHEM/lib/UPnP/sonos_bibliothek_round.png
create mode 100644 fhem/FHEM/lib/UPnP/sonos_dock_quadratic.jpg
create mode 100644 fhem/FHEM/lib/UPnP/sonos_dock_round.png
create mode 100644 fhem/FHEM/lib/UPnP/sonos_leer.gif
create mode 100644 fhem/FHEM/lib/UPnP/sonos_linein_quadratic.jpg
create mode 100644 fhem/FHEM/lib/UPnP/sonos_linein_round.png
create mode 100644 fhem/FHEM/lib/UPnP/sonos_playbar_quadratic.jpg
create mode 100644 fhem/FHEM/lib/UPnP/sonos_playbar_round.png
create mode 100644 fhem/FHEM/lib/UPnP/sonos_tunein_quadratic.jpg
create mode 100644 fhem/FHEM/lib/UPnP/sonos_tunein_round.png
diff --git a/fhem/FHEM/00_SONOS.pm b/fhem/FHEM/00_SONOS.pm
index fecec9bf5..bcfaaf2ca 100755
--- a/fhem/FHEM/00_SONOS.pm
+++ b/fhem/FHEM/00_SONOS.pm
@@ -1,6 +1,6 @@
########################################################################################
#
-# SONOS.pm (c) by Reiner Leins, April 2017
+# SONOS.pm (c) by Reiner Leins, Mai 2017
# rleins at lmsoft dot de
#
# $Id$
@@ -51,6 +51,29 @@
# Changelog (last 4 entries only, see Wiki for complete changelog)
#
# SVN-History:
+# 13.05.2017
+# FHEMWEB-Anzeige der Player auf die neu möglichen, nicht quadratischen, Radiocover angepasst.
+# In der Datei ControlPoint.pm wurde beim Öffnen des SSDP-Ports das Attribut ReusePort hinzugefügt (sofern das Betriebssystem das unterstützt).
+# Es gibt ein neues Reading "currentEnqueuedTransportHandle", welches zur Weitergabe der aktuellen Quelle des aktuellen Titels verwendet werden kann.
+# Es gibt zwei neue Readings "currentTrackHandle" und "nextTrackHandle", welche zur Weitergabe der aktuellen bzw. nächsten Wiedergabe geeignet sind.
+# Es gibt ein neues Reading "currentSource", welches (wenn beliefert) den Namen der Quelle des Titels angibt. Bei Spotify z.B. die gewählte Playliste oder das Album, oder die gewählte Sonos-Playliste.
+# Die Trackprovider werden jetzt über die verfügbaren, von Sonos bereitgestellten, MusicServices ermittelt.
+# (Fehlende) Radiocover werden nun über den offiziellen Webservice von Sonos ermittelt, und werden wieder geladen.
+# Etwaige, immer noch, fehlende Radiocover werden als Fallback wie bisher geladen. Das sollte aber nicht mehr vorkommen.
+# Spotify-Playlisten-Cover werden wieder geladen.
+# Teilweise wurden Bibliothekscover nicht geladen, das sollte wieder gehen.
+# Die Verbindungsprüfung zwischen Fhem-Modul und SubProzess erfolgt nur noch, wenn nicht sowieso schon Daten übertragen werden, und sollte dementsprechend nicht mehr dazwischenfunken.
+# Die Verbindungsprüfung und der grundsätzliche Verbindungsaufbau zum SubProzess wurden umgestellt.
+# Fehlermeldungen bei Fhem-Player-Such-Prozeduren verbessert.
+# Es gibt zwei (bzw. vier) neue Readings "TrackProviderIconRoundURL" und "TrackProviderIconQuadraticURL" (jeweils für "current" und für "next"). Mit diesen lassen sich die entsprechenden Provider-Icons anzeigen.
+# Die Titelanzeige in FhemWeb enthält nun auch die neuen Provider-Icons.
+# Detaildarstellung der SonosPlayer-Devices enthält jetzt auch die Coverdarstellung.
+# Bug in "SONOS_GetTimeFromString" behoben.
+# Es gibt zwei neue Attribute "simulateCurrentTrackPosition" und "simulateCurrentTrackPositionPercentFormat". Mit diesen kann man eine Simulation des Fortschritts von currentTrackPosition im gewählten Intervall aktivieren. Dabei werden die Readings "currentTrackPositionSimulated", "currentTrackPositionSimulatedSec" und "currentTrackPositionSimulatedPercent" (nur bei gelieferter "currentTrackDuration") aktualisiert.
+# Es gibt zwei neue Readings "currentTrackDurationSec" und "nextTrackDurationSec", welche die jeweilige Tracklänge in Sekunden angeben.
+# Durch ein mittlerweile verändertes Notify-Event-Handling in Fhem wurden die Bookmarks nicht immer zusammen mit dem globalen Save-Befehl gespeichert.
+# Die Cover-/Titelanzeige wird nun intern vom Modul durchgeführt. Deshalb ist keine zusätzliche ReadingsGroup für die Anzeige und Aktualisierung mehr notwendig. In der Raumansicht kann man das Verhalten für alle Sonosplayer einheitlich mit dem Attribut "deviceRoomView" beeinflussen. Es kann die Zustände "Both" und "DeviceLineOnly" annehmen.
+# Die Steuermöglichkeiten werden nun intern vom Modul dargestellt. Dazu muss die Cover-/Titelanzeige aktiviert sein. Will man die Steuerung ausblenden, kann man das Attribut "suppressControlButtons" für Sonosplayer einzeln setzen setzen.
# 09.04.2017
# Beim Maskieren der Anzeigelisten für Playlisten, Radios oder Favoriten werden Klammern und andere Sonderzeichen für reguläre Ausdrücke nun auch in Punkte umgewandelt, da diese sonst den regulären Such-Ausdruck stören.
# Beim Starten/Laden von Playlisten, Radios oder Favoriten werden, vor der eigentlichen Suche im Sonossystem, einfache Anführungszeichen (') in die HTML-Schreibweise (') übersetzt, da diese so auch von Sonos verwendet werden.
@@ -75,8 +98,6 @@
# Es gibt ein neues Attribut "getListsDirectlyToReadings", mit welchem die UserReadings bzgl. Favourites, Playlists und Radios (sowie currentTrackPosition) obsolet werden, da sie dann direkt in die passenden Readings geschrieben werden. Wenn man selber beeinflussen möchte, auf welche Weise und mit welchem Namen diese Readings gefüllt werden, dann darf dieses Attribut nicht gesetzt werden (es bleibt dann das bisherige Verhalten)
# Es gibt zwei neue Getter "Queue" und "QueueWithCovers", welche die aktuelle Abspielliste liefern. Diese können wieder mit UserReadings oder dem neuen Attribut "getListsDirectlyToReadings" in entsprechende Readings übertragen werden. Hierbei werden auch zwei neue Readings "QueueDuration" und "QueueDurationSec" gefüllt, die die gesamte Abspieldauer der Abspielliste enthalten.
# Die Prozedur "SONOS_getGroupsRG()" wurde umgebaut, da FW_makeImage von FhemWeb scheinbar die Variable "$_" verändert.
-# 13.03.2017
-# Saubere Fehlerbehandlung bei der Verarbeitung von currentFavouriteName, currentPlaylistName und currentRadioName.
#
########################################################################################
#
@@ -112,6 +133,7 @@ use Net::Ping;
use Socket;
use IO::Select;
use IO::Socket::INET;
+use HTTP::Request::Common;
use File::Path;
use File::stat;
use Time::HiRes qw(usleep gettimeofday);
@@ -122,6 +144,7 @@ use feature 'unicode_strings';
use Digest::MD5 qw(md5_hex);
use File::Temp;
use File::Copy;
+# use Encode::Guess;
use Data::Dumper;
$Data::Dumper::Terse = 1;
@@ -147,7 +170,7 @@ my %usedonlyIPs = ();
########################################################
# Standards aus FHEM einbinden
########################################################
-use vars qw{%attr %defs %intAt %data};
+use vars qw{%attr %modules %defs %intAt %data};
########################################################
@@ -218,23 +241,9 @@ my %sets = (
'EnableBookmark' => 'groupname'
);
-my %SONOS_ProviderList = ('^http:(\/\/.*)' => 'Radio',
- '^aac:(\/\/.*)' => 'Radio',
- '^\/\/' => 'Bibliothek',
- '^x-sonos-spotify:' => 'Spotify',
- '^x-sonos-http:amz' => 'Amazon Music',
- '^npsdy:' => 'Napster',
- '^x-sonos-http:track%3a' => 'SoundCloud',
- '^x-sonosapi-hls-static:' => 'Amazon Music');
-
my @SONOS_PossibleDefinitions = qw(NAME INTERVAL);
my @SONOS_PossibleAttributes = qw(targetSpeakFileHashCache targetSpeakFileTimestamp targetSpeakDir targetSpeakURL targetSpeakMP3FileDir targetSpeakMP3FileConverter SpeakGoogleURL Speak0 Speak1 Speak2 Speak3 Speak4 SpeakCover Speak1Cover Speak2Cover Speak3Cover Speak4Cover minVolume maxVolume minVolumeHeadphone maxVolumeHeadphone getAlarms disable generateVolumeEvent buttonEvents generateProxyAlbumArtURLs proxyCacheTime bookmarkSaveDir bookmarkTitleDefinition bookmarkPlaylistDefinition coverLoadTimeout getListsDirectlyToReadings getTitleInfoFromMaster stopSleeptimerInAction saveSleeptimerInAction);
-my @SONOS_PossibleReadings = qw(AlarmList AlarmListIDs UserID_Spotify UserID_Napster location SleepTimerVersion Mute OutputFixed HeadphoneConnected Balance Volume Loudness Bass Treble TruePlay SurroundEnable SurroundLevel SubEnable SubGain SubPolarity AudioDelay AudioDelayLeftRear AudioDelayRightRear NightMode DialogLevel AlarmListVersion ZonePlayerUUIDsInGroup ZoneGroupState ZoneGroupID fieldType IsBonded ZoneGroupName roomName roomNameAlias roomIcon currentTransportState transportState TransportState LineInConnected presence currentAlbum currentArtist currentTitle GroupVolume GroupMute FavouritesVersion RadiosVersion PlaylistsVersion QueueVersion QueueHash GroupMasterPlayer ShareIndexInProgress DirectControlClientID DirectControlIsSuspended DirectControlAccountID IsMaster MasterPlayer SlavePlayer ButtonState ButtonLockState AllPlayer LineInName LineInIcon);
-
-# Obsolete Einstellungen...
-my $SONOS_UseTelnetForQuestions = 1;
-my $SONOS_UseTelnetForQuestions_Host = 'localhost'; # Wird automatisch durch den anfragenden Host ersetzt
-my $SONOS_UseTelnetForQuestions_Port = 7072;
+my @SONOS_PossibleReadings = qw(AlarmList AlarmListIDs UserID_Spotify UserID_Napster location SleepTimerVersion Mute OutputFixed HeadphoneConnected Balance Volume Loudness Bass Treble TruePlay SurroundEnable SurroundLevel SubEnable SubGain SubPolarity AudioDelay AudioDelayLeftRear AudioDelayRightRear NightMode DialogLevel AlarmListVersion ZonePlayerUUIDsInGroup ZoneGroupState ZoneGroupID fieldType IsBonded ZoneGroupName roomName roomNameAlias roomIcon currentTransportState transportState TransportState LineInConnected presence currentAlbum currentArtist currentTitle currentStreamAudio GroupVolume GroupMute FavouritesVersion RadiosVersion PlaylistsVersion QueueVersion QueueHash GroupMasterPlayer ShareIndexInProgress DirectControlClientID DirectControlIsSuspended DirectControlAccountID IsMaster MasterPlayer SlavePlayer ButtonState ButtonLockState AllPlayer LineInName LineInIcon MusicServicesListVersion MusicServicesList);
# Communication between the two "levels" of threads
my $SONOS_ComObjectTransportQueue = Thread::Queue->new();
@@ -247,6 +256,7 @@ my $SONOS_Thread :shared = -1;
my $SONOS_Thread_IsAlive :shared = -1;
my $SONOS_Thread_PlayerRestore :shared = -1;
+my $SONOS_Thread_IsAlive_CheckerActive = 0;
my %SONOS_Thread_IsAlive_Counter;
my $SONOS_Thread_IsAlive_Counter_MaxMerci = 2;
@@ -254,6 +264,8 @@ my $SONOS_Thread_IsAlive_Counter_MaxMerci = 2;
my @SONOS_PINGTYPELIST = qw(none tcp udp icmp syn);
my $SONOS_DEFAULTPINGTYPE = 'syn';
my $SONOS_SUBSCRIPTIONSRENEWAL = 1800;
+my $SONOS_USERAGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, likeGecko) Chrome/23.0.1271.64 Safari/537.11';
+#my $SONOS_USERAGENT = 'Linux UPnP/1.0 Sonos/35.3-39010 (WDCR:Microsoft Windows NT 6.1.7601 Service Pack 1)';
my $SONOS_DIDLHeader = '';
my $SONOS_DIDLFooter = '';
my $SONOS_GOOGLETRANSLATOR_URL = 'http://translate.google.com/translate_tts?tl=%1$s&client=tw-ob&q=%2$s'; # 1->Sprache, 2->Text
@@ -290,6 +302,7 @@ my %SONOS_AlarmSubscriptions;
my %SONOS_ZoneGroupTopologySubscriptions;
my %SONOS_DevicePropertiesSubscriptions;
my %SONOS_AudioInSubscriptions;
+my %SONOS_MusicServicesSubscriptions;
# Bookmark-Daten
my %SONOS_BookmarkQueueHash;
@@ -351,6 +364,7 @@ sub SONOS_Initialize ($) {
$hash->{GetFn} = 'SONOS_Get';
$hash->{SetFn} = 'SONOS_Set';
$hash->{AttrFn} = 'SONOS_Attribute';
+ $hash->{NotifyFn} = 'SONOS_Notify';
# CGI
my $name = "sonos";
@@ -362,7 +376,7 @@ sub SONOS_Initialize ($) {
eval {
no strict;
no warnings;
- $hash->{AttrList}= 'disable:1,0 pingType:'.join(',', @SONOS_PINGTYPELIST).' usedonlyIPs ignoredIPs targetSpeakDir targetSpeakURL targetSpeakFileTimestamp:1,0 targetSpeakFileHashCache:1,0 targetSpeakMP3FileDir targetSpeakMP3FileConverter SpeakGoogleURL Speak1 Speak2 Speak3 Speak4 SpeakCover Speak1Cover Speak2Cover Speak3Cover Speak4Cover generateProxyAlbumArtURLs:1,0 proxyCacheTime proxyCacheDir bookmarkSaveDir bookmarkTitleDefinition bookmarkPlaylistDefinition coverLoadTimeout:1,2,3,4,5,6,7,8,9,10,15,20,25,30 getListsDirectlyToReadings:1,0 '.$readingFnAttributes;
+ $hash->{AttrList}= 'disable:1,0 pingType:'.join(',', @SONOS_PINGTYPELIST).' usedonlyIPs ignoredIPs targetSpeakDir targetSpeakURL targetSpeakFileTimestamp:1,0 targetSpeakFileHashCache:1,0 targetSpeakMP3FileDir targetSpeakMP3FileConverter SpeakGoogleURL Speak1 Speak2 Speak3 Speak4 SpeakCover Speak1Cover Speak2Cover Speak3Cover Speak4Cover generateProxyAlbumArtURLs:1,0 proxyCacheTime proxyCacheDir bookmarkSaveDir bookmarkTitleDefinition bookmarkPlaylistDefinition coverLoadTimeout:1,2,3,4,5,6,7,8,9,10,15,20,25,30 getListsDirectlyToReadings:1,0 deviceRoomView:Both,DeviceLineOnly '.$readingFnAttributes;
use strict;
use warnings;
};
@@ -473,7 +487,7 @@ sub SONOS_getCoverTitleRG($;$$) {
$transportState = FW_makeImage('audio_pause', 'Paused', 'SONOS_Transportstate') if ($transportState eq 'PAUSED_PLAYBACK');
$transportState = FW_makeImage('audio_stop', 'Stopped', 'SONOS_Transportstate') if ($transportState eq 'STOPPED');
- my $fullscreenDiv = '
'.ReadingsVal($device, 'roomName', $device).$transportState.'
'.((lc($presence) eq 'disappeared') ? 'Player disappeared' : ReadingsVal($device, 'infoSummarize1', '')).'
'.md5_hex(ReadingsVal($device, 'roomName', $device).ReadingsVal($device, 'infoSummarize2', '').ReadingsVal($device, 'currentTrackPosition', '').ReadingsVal($device, 'currentAlbumArtURL', '')).'
'.(($normalAudio) ? '
'.$currentRuntime.'
'.$currentStarttime.'
'.$playing.'
' : '').'
';
+ my $fullscreenDiv = ''.ReadingsVal($device, 'roomName', $device).$transportState.'
'.((lc($presence) eq 'disappeared') ? 'Player disappeared' : ReadingsVal($device, 'infoSummarize1', '')).'
'.md5_hex(ReadingsVal($device, 'roomName', $device).ReadingsVal($device, 'infoSummarize2', '').ReadingsVal($device, 'currentTrackPosition', '').ReadingsVal($device, 'currentAlbumArtURL', '')).'
'.(($normalAudio) ? '
'.$currentRuntime.'
'.$currentStarttime.'
'.$playing.'
' : '').'
';
my $javascriptTimer = 'function refreshTime'.$device.'() {
var playing = document.getElementById("prog_playing_'.$device.'");
@@ -531,7 +545,7 @@ sub SONOS_getCoverTitleRG($;$$) {
';
$javascriptText =~ s/\n/ /g;
- return $javascriptText.''.SONOS_getCoverRG($device).'
'.SONOS_URI_Escape($fullscreenDiv).'
'.SONOS_getTitleRG($device, $space).'
';
+ return $javascriptText.''.SONOS_getCoverRG($device).' '.SONOS_URI_Escape($fullscreenDiv).' | '.SONOS_getTitleRG($device, $space).' |
';
}
########################################################################################
@@ -546,7 +560,7 @@ sub SONOS_getCoverRG($;$) {
my $presence = ReadingsVal($device, 'presence', 'disappeared');
$presence = 'disappeared' if ($presence =~ m/~~NotLoadedMarker~~/i);
- return '
';
+ return '';
}
########################################################################################
@@ -556,14 +570,14 @@ sub SONOS_getCoverRG($;$) {
########################################################################################
sub SONOS_getTitleRG($;$) {
my ($device, $space) = @_;
- $space = '1em' if (!defined($space));
+ $space = '2.35em' if (!defined($space));
$space .= 'px' if (looks_like_number($space));
# Wenn der Player weg ist, nur eine Kurzinfo dazu anzeigen
my $presence = ReadingsVal($device, 'presence', 'disappeared');
$presence = 'disappeared' if ($presence =~ m/~~NotLoadedMarker~~/i);
if (lc($presence) eq 'disappeared') {
- return 'Player disappeared
';
+ return 'Player disappeared
';
}
my $infoString = '';
@@ -572,29 +586,29 @@ sub SONOS_getTitleRG($;$) {
$transportState = 'Spiele' if ($transportState eq 'PLAYING');
$transportState = 'Pausiere' if ($transportState eq 'PAUSED_PLAYBACK');
$transportState = 'Stop bei' if ($transportState eq 'STOPPED');
- # 55
-
+
+ my $source = ReadingsVal($device, 'currentSource', '');
+
# Läuft Radio oder ein "normaler" Titel
my $currentNormalAudio = ReadingsVal($device, 'currentNormalAudio', 1);
- $currentNormalAudio = 1 if (SONOS_Trim($currentNormalAudio) eq '');
+ $currentNormalAudio = 0 if (SONOS_Trim($currentNormalAudio) eq '');
if ($currentNormalAudio == 1) {
my $showNext = ReadingsVal($device, 'nextTitle', '') || ReadingsVal($device, 'nextArtist', '') || ReadingsVal($device, 'nextAlbum', '');
- $infoString = sprintf('%s Titel %s von %s (%s)
Titel:
%sInterpret:
%sAlbum:
%s'.($showNext ? '
Nächste Wiedergabe (%s):
Titel: %s
Interpret: %s
Album: %s
' : ''),
+ $infoString = sprintf('%s Titel %s von %s'.(($source) ? ' ~
'.$source.'' : '').'
Titel:
%sInterpret:
%sAlbum:
%s'.($showNext ? '
Nächste Wiedergabe:
| Titel: %s Interpret: %s Album: %s |
' : '').'
',
$transportState,
ReadingsVal($device, 'currentTrack', ''),
ReadingsVal($device, 'numberOfTracks', ''),
- ReadingsVal($device, 'currentTrackProvider', ''),
ReadingsVal($device, 'currentTitle', ''),
ReadingsVal($device, 'currentArtist', ''),
ReadingsVal($device, 'currentAlbum', ''),
$space,
- ReadingsVal($device, 'nextTrackProvider', ''),
ReadingsVal($device, 'nextAlbumArtURL', ''),
+ ReadingsVal($device, 'nextTrackProviderIconRoundURL', ''),
ReadingsVal($device, 'nextTitle', ''),
ReadingsVal($device, 'nextArtist', ''),
ReadingsVal($device, 'nextAlbum', ''));
} else {
- $infoString = sprintf('%s Radiostream
Sender: %s
Info: %s
Läuft: %s
',
+ $infoString = sprintf('%s Radiostream
Sender: %s
Info: %s
Läuft: %s
',
$transportState,
ReadingsVal($device, 'currentSender', ''),
ReadingsVal($device, 'currentSenderInfo', ''),
@@ -634,9 +648,9 @@ sub SONOS_getListRG($$;$) {
$command = "FW_cmd('/fhem?XHR=1&$command')";
if ($ul) {
- $resultString .= '
';
+ $resultString .= '';
} else {
- $resultString .= ' | '.(($reading eq 'Queue') ? $elems{$key}->{ShowTitle} : $elems{$key}->{Title})." |
\n";
+ $resultString .= ' | '.(($reading eq 'Queue') ? $elems{$key}->{ShowTitle} : $elems{$key}->{Title})." |
\n";
}
}
@@ -702,7 +716,13 @@ sub SONOS_FhemWebCallback($) {
last;
}
}
- return ("text/html; charset=UTF8", 'Call for Non-Sonos-Player: '.$URL) if (defined($ip) && $albumurl !~ /\.cloudfront.net\//i && $albumurl !~ /\.scdn.co\/image\//i && $albumurl !~ /cdn-radiotime-logos\.tunein\.com/i && $albumurl !~ /\/music\/image\?/i);
+ return ("text/html; charset=UTF8", 'Call for Non-Sonos-Player: '.$URL)
+ if (defined($ip)
+ && $albumurl !~ /\.cloudfront.net\//i
+ && $albumurl !~ /\.scdn.co\//i
+ && $albumurl !~ /sonos-logo\.ws\.sonos\.com\//i
+ && $albumurl !~ /\.tunein\.com/i
+ && $albumurl !~ /\/music\/image\?/i);
# Generierter Dateiname für die Cache-Funktionalitaet
my $albumHash;
@@ -738,7 +758,7 @@ sub SONOS_FhemWebCallback($) {
}
# Bild vom Player holen...
- my $ua = LWP::UserAgent->new(agent => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, likeGecko) Chrome/23.0.1271.64 Safari/537.11');
+ my $ua = LWP::UserAgent->new(agent => $SONOS_USERAGENT);
my $response = $ua->get($albumurl);
if ($response->is_success) {
SONOS_Log undef, 5, 'Cover wurde neu geladen: '.$albumurl;
@@ -763,7 +783,7 @@ sub SONOS_FhemWebCallback($) {
return (undef, undef);
} else {
- SONOS_Log undef, 1, 'Cover couldn\'t be loaded: '.$albumurl;
+ SONOS_Log undef, 1, 'Cover couldn\'t be loaded "'.$albumurl.'": '.$response->status_line,;
FW_serveSpecial('sonos_empty', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
return (undef, undef);
@@ -776,6 +796,57 @@ sub SONOS_FhemWebCallback($) {
SONOS_Log undef, 5, 'Cover: '.$URL;
+ if ($URL =~ m/^\/leer.gif/i) {
+ FW_serveSpecial('sonos_leer', 'gif', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
+ return (undef, undef);
+ }
+
+ if ($URL =~ m/^\/tunein_quadratic.jpg/i) {
+ FW_serveSpecial('sonos_tunein_quadratic', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
+ return (undef, undef);
+ }
+ if ($URL =~ m/^\/tunein_round.png/i) {
+ FW_serveSpecial('sonos_tunein_round', 'png', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
+ return (undef, undef);
+ }
+
+ if ($URL =~ m/^\/bibliothek_quadratic.jpg/i) {
+ FW_serveSpecial('sonos_bibliothek_quadratic', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
+ return (undef, undef);
+ }
+ if ($URL =~ m/^\/bibliothek_round.png/i) {
+ FW_serveSpecial('sonos_bibliothek_round', 'png', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
+ return (undef, undef);
+ }
+
+ if ($URL =~ m/^\/linein_quadratic.jpg/i) {
+ FW_serveSpecial('sonos_linein_quadratic', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
+ return (undef, undef);
+ }
+ if ($URL =~ m/^\/linein_round.png/i) {
+ FW_serveSpecial('sonos_linein_round', 'png', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
+ return (undef, undef);
+ }
+
+ if ($URL =~ m/^\/dock_quadratic.jpg/i) {
+ FW_serveSpecial('sonos_dock_quadratic', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
+ return (undef, undef);
+ }
+ if ($URL =~ m/^\/dock_round.png/i) {
+ FW_serveSpecial('sonos_dock_round', 'png', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
+ return (undef, undef);
+ }
+
+ if ($URL =~ m/^\/playbar_quadratic.jpg/i) {
+ FW_serveSpecial('sonos_playbar_quadratic', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
+ return (undef, undef);
+ }
+ if ($URL =~ m/^\/playbar_round.png/i) {
+ FW_serveSpecial('sonos_playbar_round', 'png', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
+ return (undef, undef);
+ }
+
+
if ($URL =~ m/^\/empty.jpg/i) {
FW_serveSpecial('sonos_empty', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1);
return (undef, undef);
@@ -802,21 +873,6 @@ sub SONOS_FhemWebCallback($) {
}
}
- ## FolderCover-Features...
- #if ($URL =~ m/^\/foldercover\//i) {
- # $URL =~ s/^\/foldercover//i;
- #
- # SONOS_Log undef, 0, 'FolderCover: '.$URL;
- #
- # # Da wir die Standard-Prozedur 'FW_serveSpecial' aus 'FHEMWEB' verwenden moechten, brauchen wir eine lokale Datei
- # my $tempFile = File::Temp->new(SUFFIX => '.image');
- # my $filename = $tempFile->filename;
- # $filename =~ s/\\/\//g;
- # SONOS_Log undef, 5, 'TempFilename: '.$filename;
- #
- #
- #}
-
# Wenn wir hier ankommen, dann konnte nichts verarbeitet werden...
return ("text/html; charset=UTF8", 'Call failure: '.$URL);
}
@@ -869,13 +925,12 @@ sub SONOS_Define($$) {
$delaytime = 0;
}
+ # Wir brauchen momentan nur die Notifies für global und jeden Sonosplayer...
+ $hash->{NOTIFYDEV} = 'global,TYPE=SONOSPLAYER';
+
$hash->{NAME} = $name;
$hash->{DeviceName} = $upnplistener;
- # Wir brauchen momentan nur die Notifies für global, da wir auf "save" reagieren wollen...
- $hash->{NOTIFYDEV} = 'global';
- $hash->{NotifyFn} = 'SONOS_Notify';
-
$hash->{INTERVAL} = $interval;
$hash->{WAITTIME} = $waittime;
$hash->{DELAYTIME} = $delaytime;
@@ -945,6 +1000,8 @@ sub SONOS_Attribute($$$@) {
SONOS_Log(undef, 5, 'Neu-Enabled');
$disableChange = 1;
}
+ } elsif ($attrName eq 'deviceRoomView') {
+ $modules{SONOSPLAYER}->{FW_addDetailToSummary} = ($attrValue =~ m/(Both)/i) if (defined($modules{SONOSPLAYER}));
}
} elsif ($mode eq 'del') {
if ($attrName eq 'disable') {
@@ -953,6 +1010,8 @@ sub SONOS_Attribute($$$@) {
$disableChange = 1;
$attrValue = 0;
}
+ } elsif ($attrName eq 'deviceRoomView') {
+ $modules{SONOSPLAYER}->{FW_addDetailToSummary} = 1 if (defined($modules{SONOSPLAYER}));
}
}
@@ -1015,11 +1074,47 @@ sub SONOS_StopSubProcess($) {
sub SONOS_Notify() {
my ($hash, $notifyhash) = @_;
- # Bei einem globalen save auch die bookmarks sichern...
- if (($notifyhash->{NAME} eq 'global') && ($notifyhash->{CHANGED}[0] eq 'SAVE')) {
- SONOS_DoWork('SONOS', 'SaveBookmarks', '');
+ return if (AttrVal($hash->{NAME}, 'disable', 0));
+
+ my $events = deviceEvents($notifyhash, 1);
+ return if(!$events);
+
+ my $triggerCoverTitle = 0;
+
+ foreach my $event (@{$events}) {
+ next if(!defined($event));
+
+ #SONOS_Log $hash->{UDN}, 0, 'Event: '.$notifyhash->{NAME}.'~'.$event;
+
+ # Wenn ein CoverTitle-Trigger gesendet werden muss...
+ if ($event =~ m/^(currentAlbumArtURL|currentTrackProviderIconRoundURL|currentTrackDuration|currentTrack|numberOfTracks|currentTitle|currentArtist|currentAlbum|nextAlbumArtURL|nextTrackProviderIconRoundURL|nextTitle|nextArtist|nextAlbum|currentSender|currentSenderInfo|currentSenderCurrent|transportState):/is) {
+ $triggerCoverTitle = 1;
+ }
+
+ # Wenn der Benutzer das Kommando 'Save' ausgeführt hat, dann auch die Bookmarks sichern...
+ if (($notifyhash->{NAME} eq 'global') && ($event eq 'SAVE')) {
+ SONOS_DoWork('SONOS', 'SaveBookmarks', '');
+ }
}
+ if ($triggerCoverTitle) {
+ InternalTimer(gettimeofday(), 'SONOS_TriggerCoverTitelLater', $notifyhash, 0);
+ }
+
+ return undef;
+}
+
+########################################################################################
+#
+# SONOS_TriggerCoverTitelLater - Refreshs the CoverTitle-Element later via DoTrigger
+#
+########################################################################################
+sub SONOS_TriggerCoverTitelLater($) {
+ my ($hash) = @_;
+
+ my $html = SONOSPLAYER_Detail('', $hash->{NAME}, '', 0);
+ DoTrigger($hash->{NAME}, 'display_covertitle: '.$html, 1);
+
return undef;
}
@@ -1046,6 +1141,8 @@ sub SONOS_Ready($) {
sub SONOS_Read($) {
my ($hash) = @_;
+ RemoveInternalTimer($hash, 'SONOS_IsSubprocessAliveChecker');
+
# Bis zum letzten (damit der Puffer leer ist) Zeilenumbruch einlesen, da SimpleRead immer nur kleine Päckchen einliest.
my $buf = DevIo_SimpleReadWithTimeout($hash, AttrVal($hash->{NAME}, 'coverLoadTimeout', $SONOS_DEFAULTCOVERLOADTIMEOUT));
@@ -1213,12 +1310,16 @@ sub SONOS_Read($) {
# Updating...
SONOS_readingsBulkUpdateIfChanged($hash, "currentTrack", '');
SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackURI", '');
+ SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackHandle", '');
SONOS_readingsBulkUpdateIfChanged($hash, "currentEnqueuedTransportURI", '');
+ SONOS_readingsBulkUpdateIfChanged($hash, "currentEnqueuedTransportHandle", '');
SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackDuration", '');
- SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackPosition", '');
- SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackPositionSec", 0);
+ SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackDurationSec", '');
+ readingsBulkUpdate($hash, "currentTrackPosition", '');
+ readingsBulkUpdate($hash, "currentTrackPositionSec", 0);
SONOS_readingsBulkUpdateIfChanged($hash, "currentTitle", 'Disappeared');
SONOS_readingsBulkUpdateIfChanged($hash, "currentArtist", '');
+ SONOS_readingsBulkUpdateIfChanged($hash, "currentSource", '');
SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbum", '');
SONOS_readingsBulkUpdateIfChanged($hash, "currentOriginalTrackNumber", '');
SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbumArtist", '');
@@ -1229,7 +1330,9 @@ sub SONOS_Read($) {
SONOS_readingsBulkUpdateIfChanged($hash, "currentStreamAudio", 0);
SONOS_readingsBulkUpdateIfChanged($hash, "currentNormalAudio", 1);
SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackDuration", '');
+ SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackDurationSec", '');
SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackURI", '');
+ SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackHandle", '');
SONOS_readingsBulkUpdateIfChanged($hash, "nextTitle", '');
SONOS_readingsBulkUpdateIfChanged($hash, "nextArtist", '');
SONOS_readingsBulkUpdateIfChanged($hash, "nextAlbum", '');
@@ -1254,6 +1357,10 @@ sub SONOS_Read($) {
if ($hash) {
readingsBeginUpdate($hash);
+ my $oldTransportState = ReadingsVal($hash->{NAME}, 'transportState', 0);
+ my $oldTrackHandle = ReadingsVal($hash->{NAME}, 'currentTrackHandle', '');
+ my $oldTrack = ReadingsVal($hash->{NAME}, 'currentTrack', '');
+ my $oldTrackPosition = ReadingsVal($hash->{NAME}, 'currentTrackPosition', '');
# Wurden für das Device bereits Favoriten geladen? Dann raussuchen, ob gerade ein solcher abgespielt wird...
$current{FavouriteName} = '';
@@ -1332,7 +1439,9 @@ sub SONOS_Read($) {
SONOS_readingsBulkUpdateIfChanged($hash, "numberOfTracks", $current{NumberOfTracks});
SONOS_readingsBulkUpdateIfChanged($hash, "currentTrack", $current{Track});
SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackURI", $current{TrackURI});
+ SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackHandle", $current{TrackHandle});
SONOS_readingsBulkUpdateIfChanged($hash, "currentEnqueuedTransportURI", $current{EnqueuedTransportURI});
+ SONOS_readingsBulkUpdateIfChanged($hash, "currentEnqueuedTransportHandle", $current{EnqueuedTransportHandle});
SONOS_readingsBulkUpdateIfChanged($hash, "currentFavouriteName", $current{FavouriteName});
SONOS_readingsBulkUpdateIfChanged($hash, "currentPlaylistName", $current{PlaylistName});
@@ -1353,11 +1462,22 @@ sub SONOS_Read($) {
}
SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackDuration", $current{TrackDuration});
- SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackPosition", $current{TrackPosition});
- SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackPositionSec", $current{TrackPositionSec});
+ SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackDurationSec", $current{TrackDurationSec});
+
+ if ($current{StreamAudio} && ($oldTransportState eq $current{TransportState})) {
+ SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackPosition", '0:00:00');
+ SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackPositionSec", '0:00:00');
+ } else {
+ readingsBulkUpdate($hash, "currentTrackPosition", $current{TrackPosition});
+ readingsBulkUpdate($hash, "currentTrackPositionSec", $current{TrackPositionSec});
+ }
+
SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackProvider", $current{TrackProvider});
+ SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackProviderIconQuadraticURL", $current{TrackProviderIconQuadraticURL});
+ SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackProviderIconRoundURL", $current{TrackProviderIconRoundURL});
SONOS_readingsBulkUpdateIfChanged($hash, "currentTitle", $current{Title});
SONOS_readingsBulkUpdateIfChanged($hash, "currentArtist", $current{Artist});
+ SONOS_readingsBulkUpdateIfChanged($hash, "currentSource", $current{Source});
SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbum", $current{Album});
SONOS_readingsBulkUpdateIfChanged($hash, "currentOriginalTrackNumber", $current{OriginalTrackNumber});
SONOS_readingsBulkUpdateIfChanged($hash, "currentAlbumArtist", $current{AlbumArtist});
@@ -1368,8 +1488,12 @@ sub SONOS_Read($) {
SONOS_readingsBulkUpdateIfChanged($hash, "currentStreamAudio", $current{StreamAudio});
SONOS_readingsBulkUpdateIfChanged($hash, "currentNormalAudio", $current{NormalAudio});
SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackDuration", $current{nextTrackDuration});
+ SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackDurationSec", $current{nextTrackDurationSec});
SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackURI", $current{nextTrackURI});
+ SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackHandle", $current{nextTrackHandle});
SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackProvider", $current{nextTrackProvider});
+ SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackProviderIconQuadraticURL", $current{nextTrackProviderIconQuadraticURL});
+ SONOS_readingsBulkUpdateIfChanged($hash, "nextTrackProviderIconRoundURL", $current{nextTrackProviderIconRoundURL});
SONOS_readingsBulkUpdateIfChanged($hash, "nextTitle", $current{nextTitle});
SONOS_readingsBulkUpdateIfChanged($hash, "nextArtist", $current{nextArtist});
SONOS_readingsBulkUpdateIfChanged($hash, "nextAlbum", $current{nextAlbum});
@@ -1415,8 +1539,11 @@ sub SONOS_Read($) {
$currentElem{Track} = $current{Track};
$currentElem{NumberOfTracks} = $current{NumberOfTracks};
$currentElem{TrackDuration} = $current{TrackDuration};
+ $currentElem{TrackDurationSec} = $current{TrackDurationSec};
$currentElem{TrackPosition} = $current{TrackPosition};
$currentElem{TrackProvider} = $current{TrackProvider};
+ $currentElem{TrackProviderIconQuadraticURL} = $current{TrackProviderIconQuadraticURL};
+ $currentElem{TrackProviderIconRoundURL} = $current{TrackProviderIconRoundURL};
# Loslegen
readingsBeginUpdate($elem);
@@ -1429,8 +1556,11 @@ sub SONOS_Read($) {
SONOS_readingsBulkUpdateIfChanged($elem, "currentTrack", $currentElem{Track});
SONOS_readingsBulkUpdateIfChanged($elem, "numberOfTracks", $currentElem{NumberOfTracks});
SONOS_readingsBulkUpdateIfChanged($elem, "currentTrackDuration", $currentElem{TrackDuration});
- SONOS_readingsBulkUpdateIfChanged($elem, "currentTrackPosition", $currentElem{TrackPosition});
- SONOS_readingsBulkUpdateIfChanged($hash, "currentTrackProvider", $currentElem{TrackProvider});
+ SONOS_readingsBulkUpdateIfChanged($elem, "currentTrackDurationSec", $currentElem{TrackDurationSec});
+ readingsBulkUpdate($elem, "currentTrackPosition", $currentElem{TrackPosition});
+ SONOS_readingsBulkUpdateIfChanged($elem, "currentTrackProvider", $currentElem{TrackProvider});
+ SONOS_readingsBulkUpdateIfChanged($elem, "currentTrackProviderIconQuadraticURL", $currentElem{TrackProviderIconQuadraticURL});
+ SONOS_readingsBulkUpdateIfChanged($elem, "currentTrackProviderIconRoundURL", $currentElem{TrackProviderIconRoundURL});
if (AttrVal($elem->{NAME}, 'generateSomethingChangedEvent', 0) == 1) {
readingsBulkUpdate($elem, "somethingChanged", 1);
@@ -1450,6 +1580,14 @@ sub SONOS_Read($) {
}
}
}
+
+ # SimulatedValues aktualisieren, wenn ein Wechsel des Titels stattgefunden hat, und gerade keine Wiedergabe erfolgt...
+ if (($current{TransportState} ne 'PLAYING')
+ && (($oldTrackHandle ne $current{TrackHandle})
+ || ($oldTrack != $current{Track})
+ || ($oldTrackPosition ne $current{TrackPosition}))) {
+ SONOSPLAYER_SimulateCurrentTrackPosition($hash);
+ }
} else {
SONOS_Log undef, 0, "Fehlerhafter Aufruf von CurrentBulkUpdate: $1";
}
@@ -1489,9 +1627,15 @@ sub SONOS_Read($) {
$srcURI = $groundURL.$tempURI;
$currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.'.SONOS_ImageDownloadTypeExtension($groundURL.$tempURI);
SONOS_Log undef, 4, "Transport-Event: Spotify-Bilder-Download failed. Use normal thumbnail: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');";
- }
+ }#http://192.168.0.47:1400/getaa?s=1&u=x-sonosapi-stream%3as84483%3fsid%3d254%26flags%3d32
} elsif ($tempURI =~ m/getaa.*?x-sonosapi-stream%3a(.+?)%3f/i) {
- $srcURI = 'http://cdn-radiotime-logos.tunein.com/'.$1.'g.png';;
+ $srcURI = SONOS_GetRadioMediaMetadata($hash->{UDN}, $1);
+ eval {
+ my $result = SONOS_ReadURL($srcURI);
+ if (!defined($result) || ($result =~ m/.*<\/Error>/i)) {
+ $srcURI = $groundURL.$tempURI;
+ }
+ };
$currentValue = $attr{global}{modpath}.'/www/images/default/SONOSPLAYER/'.$name.'_'.$nextName.'AlbumArt.png';
SONOS_Log undef, 4, "Transport-Event: Radiocover-Download: SONOS_DownloadReplaceIfChanged('$srcURI', '".$currentValue."');";
} elsif ($tempURI =~ m/^\/fhem\/sonos\/cover\/(.*)/i) {
@@ -1567,6 +1711,8 @@ sub SONOS_Read($) {
SONOS_DoTriggerInternal('Main', $line);
}
}
+
+ InternalTimer(gettimeofday() + $hash->{INTERVAL}, 'SONOS_IsSubprocessAliveChecker', $hash, 0);
}
########################################################################################
@@ -1657,11 +1803,11 @@ sub SONOS_StartClientProcessIfNeccessary($) {
# Antwort vom Client weglesen...
my $answer;
- $socket->recv($answer, 50);
+ $socket->recv($answer, 5000);
+ $socket->send("Test\r\n", 0);
# Hiermit wird eine etwaig bestehende Thread-Struktur beendet und diese Verbindung selbst geschlossen...
eval{
- $socket->send("disconnect\n", 0);
$socket->shutdown(2);
$socket->close();
};
@@ -1682,6 +1828,7 @@ sub SONOS_InitClientProcessLater($) {
# Begrüßung weglesen...
my $answer = DevIo_SimpleRead($hash);
+ DevIo_SimpleWrite($hash, "Establish connection\r\n", 0);
# Verbindung aufbauen...
InternalTimer(gettimeofday() + 1, 'SONOS_InitClientProcess', $hash, 0);
@@ -1782,27 +1929,26 @@ sub SONOS_IsSubprocessAliveChecker() {
return undef if (AttrVal($hash->{NAME}, 'disable', 0));
my $answer;
+ # Neue Verbindung parallel zur bestehenden Kommunikationsleitung.
+ # Nur zum Prüfen, ob der SubProzess noch lebt und antworten kann.
my $socket = new IO::Socket::INET(PeerAddr => $hash->{DeviceName}, Proto => 'tcp');
if ($socket) {
$socket->sockopt(SO_LINGER, pack("ii", 1, 0));
$socket->recv($answer, 500);
-
- $socket->send("hello\n", 0);
- $socket->recv($answer, 500);
-
- $socket->send("goaway\n", 0);
+ $socket->send("Test\r\n", 0);
$socket->shutdown(2);
$socket->close();
}
+ # Ab hier keine Parallelverbindung mehr offen...
if (defined($answer)) {
$answer =~ s/[\r\n]//g;
}
- if (!defined($answer) || ($answer ne 'OK')) {
- SONOS_Log undef, 0, 'No Answer from Subprocess. Restart Sonos-Subprocess...';
+ if (!defined($answer) || ($answer !~ m/^This is UPnP-Server listening for commands/)) {
+ SONOS_Log undef, 0, 'No (or incorrect) answer from Subprocess. Restart Sonos-Subprocess...';
# Verbindung beenden, damit der SubProzess die Chance hat neu initialisiert zu werden...
RemoveInternalTimer($hash);
@@ -1812,9 +1958,9 @@ sub SONOS_IsSubprocessAliveChecker() {
# Neu anstarten...
SONOS_StartClientProcessIfNeccessary($hash->{DeviceName}) if ($SONOS_StartedOwnUPnPServer);
InternalTimer(gettimeofday() + $hash->{WAITTIME}, 'SONOS_DelayOpenDev', $hash, 0);
- } elsif (defined($answer) && ($answer eq 'OK')) {
- SONOS_Log undef, 4, 'Got correct Answer from Subprocess...';
-
+ } elsif (defined($answer) && ($answer =~ m/^This is UPnP-Server listening for commands/)) {
+ SONOS_Log undef, 4, 'Got correct answer from Subprocess...';
+ RemoveInternalTimer($hash, 'SONOS_IsSubprocessAliveChecker');
InternalTimer(gettimeofday() + $hash->{INTERVAL}, 'SONOS_IsSubprocessAliveChecker', $hash, 0);
}
}
@@ -2205,6 +2351,9 @@ sub SONOS_DoWork($$;@) {
my $hash = SONOS_getSonosPlayerByName();
+ while ($SONOS_Thread_IsAlive_CheckerActive) {
+ select(undef, undef, undef, 0.1);
+ }
DevIo_SimpleWrite($hash, 'DoWork:'.$udn.':'.$method.':'.encode_utf8(join('--#--', @params))."\r\n", 0);
return undef;
@@ -2317,8 +2466,9 @@ sub SONOS_Discover() {
my $position = $SONOS_AVTransportControlProxy{$udn}->GetPositionInfo(0)->getValue('RelTime');
SONOS_Client_Notifier('ReadingsBeginUpdate:'.$udn);
- SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackPosition', $position);
- SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackPositionSec', SONOS_GetTimeSeconds($position));
+ my $modus = 'ReadingsBulkUpdate'.((SONOS_Client_Data_Retreive($udn, 'reading', 'currentStreamAudio', 0)) ? 'IfChanged' : '');
+ SONOS_Client_Data_Refresh($modus, $udn, 'currentTrackPosition', $position);
+ SONOS_Client_Data_Refresh($modus, $udn, 'currentTrackPositionSec', SONOS_GetTimeSeconds($position));
SONOS_Client_Notifier('ReadingsEndUpdate:'.$udn);
SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': DirectlySet');
@@ -3250,18 +3400,24 @@ sub SONOS_Discover() {
$resultHash{DurationSec} = 0;
$resultHash{Duration} = '0:00:00';
my $position = 0;
- while ($tmp =~ m/- .*?(.*?)<\/res>.*?(.*?)<\/dc:title>.*?(.*?)<\/dc:creator>.*?(.*?)<\/upnp:album>.*?<\/item>/ig) {
+ while ($tmp =~ m/
- .*?(.*?)<\/res>.*?(.*?)<\/dc:title>.*?(.*?)<\/dc:creator>.*?(.*?)<\/upnp:album>.*?<\/item>/ig) {
my $key = $1.sprintf("%04d", $2);
$resultHash{$key}->{Position} = ++$position;
- $resultHash{$key}->{ShowTitle} = $position.'. ('.$6.') '.$5.' ['.$3.']';
- $resultHash{$key}->{Title} = $5;
- $resultHash{$key}->{Artist} = $6;
- $resultHash{$key}->{Album} = $7;
- $resultHash{$key}->{Duration} = $3;
- $resultHash{$key}->{DurationSec} = SONOS_GetTimeSeconds($3);
- $resultHash{DurationSec} += SONOS_GetTimeSeconds($3);
- $resultHash{$key}->{Cover} = SONOS_MakeCoverURL($udn, $4);
- $resultHash{$key}->{Ressource} = decode_entities($4);
+ $resultHash{$key}->{Title} = $6;
+ $resultHash{$key}->{Artist} = $7;
+ $resultHash{$key}->{Album} = $8;
+ if (defined($4)) {
+ $resultHash{$key}->{ShowTitle} = $position.'. ('.$7.') '.$6.' ['.$4.']';
+ $resultHash{$key}->{Duration} = $4;
+ $resultHash{$key}->{DurationSec} = SONOS_GetTimeSeconds($4);
+ $resultHash{DurationSec} += SONOS_GetTimeSeconds($4);
+ } else {
+ $resultHash{$key}->{ShowTitle} = $position.'. ('.$7.') '.$6.' [k.A.]';
+ $resultHash{$key}->{Duration} = '0:00:00';
+ $resultHash{$key}->{DurationSec} = 0;
+ }
+ $resultHash{$key}->{Cover} = SONOS_MakeCoverURL($udn, $5);
+ $resultHash{$key}->{Ressource} = decode_entities($5);
}
$resultHash{Duration} = SONOS_ConvertSecondsToTime($resultHash{DurationSec});
@@ -3377,29 +3533,31 @@ sub SONOS_Discover() {
return;
}
- if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
- # Entscheiden, ob eine Abspielliste geladen und gestartet werden soll, oder etwas direkt abgespielt werden kann
- if ($resultHash{$favouriteName}{METADATA} =~ m/object\.container.*?<\/upnp:class>/i) {
- SONOS_Log $udn, 5, 'StartFavourite AddToQueue-Res: "'.$resultHash{$favouriteName}{RES}.'", -Meta: "'.$resultHash{$favouriteName}{METADATA}.'"';
-
- # Queue leeren
- $SONOS_AVTransportControlProxy{$udn}->RemoveAllTracksFromQueue(0);
-
- # Queue wieder füllen
- SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->AddURIToQueue(0, $resultHash{$favouriteName}{RES}, $resultHash{$favouriteName}{METADATA}, 0, 1)));
-
- # Queue aktivieren
- $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, '')->getValue('Result')), '');
- } else {
- SONOS_Log $udn, 5, 'StartFavourite SetAVTransport-Res: "'.$resultHash{$favouriteName}{RES}.'", -Meta: "'.$resultHash{$favouriteName}{METADATA}.'"';
-
- # Stück aktivieren
- SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, $resultHash{$favouriteName}{RES}, $resultHash{$favouriteName}{METADATA})));
- }
-
- # Abspielen starten, wenn nicht absichtlich verhindert
- $SONOS_AVTransportControlProxy{$udn}->Play(0, 1) if (!$nostart);
- }
+ SONOS_StartMetadata($workType, $udn, $resultHash{$favouriteName}{RES}, $resultHash{$favouriteName}{METADATA}, $nostart);
+
+# if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
+# # Entscheiden, ob eine Abspielliste geladen und gestartet werden soll, oder etwas direkt abgespielt werden kann
+# if ($resultHash{$favouriteName}{METADATA} =~ m/object\.container.*?<\/upnp:class>/i) {
+# SONOS_Log $udn, 5, 'StartFavourite AddToQueue-Res: "'.$resultHash{$favouriteName}{RES}.'", -Meta: "'.$resultHash{$favouriteName}{METADATA}.'"';
+#
+# # Queue leeren
+# $SONOS_AVTransportControlProxy{$udn}->RemoveAllTracksFromQueue(0);
+#
+# # Queue wieder füllen
+# SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->AddURIToQueue(0, $resultHash{$favouriteName}{RES}, $resultHash{$favouriteName}{METADATA}, 0, 1)));
+#
+# # Queue aktivieren
+# $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, '')->getValue('Result')), '');
+# } else {
+# SONOS_Log $udn, 5, 'StartFavourite SetAVTransport-Res: "'.$resultHash{$favouriteName}{RES}.'", -Meta: "'.$resultHash{$favouriteName}{METADATA}.'"';
+#
+# # Stück aktivieren
+# SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, $resultHash{$favouriteName}{RES}, $resultHash{$favouriteName}{METADATA})));
+# }
+#
+# # Abspielen starten, wenn nicht absichtlich verhindert
+# $SONOS_AVTransportControlProxy{$udn}->Play(0, 1) if (!$nostart);
+# }
}
} elsif ($workType eq 'loadPlaylist') {
my $answer = '';
@@ -3969,6 +4127,42 @@ sub SONOS_Discover() {
}
}
}
+
+ if (defined($SONOS_MusicServicesSubscriptions{$udn}) && (Time::HiRes::time() - $SONOS_MusicServicesSubscriptions{$udn}->{_startTime} > $SONOS_SUBSCRIPTIONSRENEWAL)) {
+ eval {
+ $SONOS_MusicServicesSubscriptions{$udn}->renew();
+ SONOS_Log $udn, 3, 'MusicServices-Subscription for ZonePlayer "'.$udn.'" has expired and is now renewed.';
+ };
+ if ($@) {
+ SONOS_Log $udn, 3, 'Error! MusicServices-Subscription for ZonePlayer "'.$udn.'" has expired and could not be renewed: '.$@;
+
+ # Wenn der Player nicht erreichbar war, dann entsprechend entfernen...
+ # Hier aber nur eine kleine Lösung, da es nur ein Notbehelf sein soll...
+ if ($@ =~ m/Can.t connect to/) {
+ SONOS_DeleteProxyObjects($udn);
+
+ # Player-Informationen aktualisieren...
+ SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'presence', 'disappeared');
+ SONOS_Client_Data_Refresh('ReadingsSingleUpdateIfChanged', $udn, 'state', 'disappeared');
+
+ # Discovery neu anstarten, falls der Player irgendwie doch noch erreichbar sein sollte...
+ $SONOS_Search = $SONOS_Controlpoint->searchByType('urn:schemas-upnp-org:device:ZonePlayer:1', \&SONOS_Discover_Callback);
+ }
+ }
+ }
+
+ } elsif ($workType eq 'startHandle') {
+ if ($params[0] =~ m/^(.+)\|(.+)$/) {
+ my $songURI = $1;
+ my $songMeta = $2;
+ SONOS_Log undef, 4, 'songURI: '.$songURI;
+ SONOS_Log undef, 4, 'songMeta: '.$songMeta;
+ SONOS_Log undef, 4, 'nostart: '.$params[1];
+
+ SONOS_StartMetadata($workType, $udn, $songURI, $songMeta, $params[1]);
+ } else {
+ SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Wrong Handle: '.$params[0]);
+ }
} elsif ($workType eq 'playURI') {
my $songURI = SONOS_ExpandURIForQueueing($params[0]);
SONOS_Log undef, 4, 'songURI: '.$songURI;
@@ -4138,6 +4332,39 @@ sub SONOS_Discover() {
return 1;
}
+########################################################################################
+#
+# SONOS_StartMetadata - Starts any kind of Metadata
+#
+########################################################################################
+sub SONOS_StartMetadata($$$$) {
+ my ($workType, $udn, $res, $meta, $nostart) = @_;
+
+ if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) {
+ # Entscheiden, ob eine Abspielliste geladen und gestartet werden soll, oder etwas direkt abgespielt werden kann
+ if ($meta =~ m/object\.container.*?<\/upnp:class>/i) {
+ SONOS_Log $udn, 5, 'StartFavourite AddToQueue-Res: "'.$res.'", -Meta: "'.$meta.'"';
+
+ # Queue leeren
+ $SONOS_AVTransportControlProxy{$udn}->RemoveAllTracksFromQueue(0);
+
+ # Queue wieder füllen
+ SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->AddURIToQueue(0, $res, $meta, 0, 1)));
+
+ # Queue aktivieren
+ $SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, SONOS_GetTagData('res', $SONOS_ContentDirectoryControlProxy{$udn}->Browse('Q:0', 'BrowseMetadata', '', 0, 0, '')->getValue('Result')), '');
+ } else {
+ SONOS_Log $udn, 5, 'StartFavourite SetAVTransport-Res: "'.$res.'", -Meta: "'.$meta.'"';
+
+ # Stück aktivieren
+ SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SetAVTransportURI(0, $res, $meta)));
+ }
+
+ # Abspielen starten, wenn nicht absichtlich verhindert
+ $SONOS_AVTransportControlProxy{$udn}->Play(0, 1) if (!$nostart);
+ }
+}
+
########################################################################################
#
# SONOS_RecursiveStructure - Retrieves the structure of the Sonos-Bibliothek
@@ -4365,20 +4592,27 @@ sub SONOS_CountInString($$) {
########################################################################################
#
-# SONOS_MakeCoverURL - Generates the approbriate cover-url incl. the use of an Fhem-Proxy
+# SONOS_MakeCoverURL - Generates the approbriate cover-url incl. the use of a Fhem-Proxy
#
########################################################################################
sub SONOS_MakeCoverURL($$) {
my ($udn, $resURL) = @_;
- SONOS_Log $udn, 5, 'MakeCoverURL-Before: '.$resURL;
+ SONOS_Log $udn, 0, 'MakeCoverURL-Before: '.$resURL;
if ($resURL =~ m/^x-rincon-cpcontainer.*?(spotify.*?)(\?|$)/i) {
$resURL = SONOS_getSpotifyCoverURL($1, 1);
} elsif ($resURL =~ m/^x-sonos-spotify:spotify%3atrack%3a(.*?)(\?|$)/i) {
$resURL = SONOS_getSpotifyCoverURL($1);
} elsif ($resURL =~ m/^x-sonosapi-stream:(.+?)\?/i) {
- $resURL = 'http://cdn-radiotime-logos.tunein.com/'.$1.'g.png';
+ my $resURLtemp = SONOS_GetRadioMediaMetadata($udn, $1);
+ eval {
+ my $result = SONOS_ReadURL($resURLtemp);
+ if (!defined($result) || ($result =~ m/.*<\/Error>/i)) {
+ $resURLtemp = $1.'/getaa?s=1&u='.SONOS_URI_Escape($resURL) if (SONOS_Client_Data_Retreive($udn, 'reading', 'location', '') =~ m/^(http:\/\/.*?:.*?)\//i);
+ }
+ };
+ $resURL = $resURLtemp;
} elsif (($resURL =~ m/x-rincon-playlist:.*?#(.*)/i) || ($resURL =~ m/savedqueues.rsq(#\d+)/i)) {
my $search = $1;
$search = 'SQ:'.$1 if ($search =~ m/#(\d+)/i);
@@ -4387,12 +4621,12 @@ sub SONOS_MakeCoverURL($$) {
$resURL = '/fhem/sonos/cover/playlist.jpg';
if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) {
- my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($search, 'BrowseDirectChildren', '', 0, 5, '');
+ my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($search, 'BrowseDirectChildren', '', 0, 15, '');
if ($result) {
my $tmp = $result->getValue('Result');
while (defined($tmp) && $tmp =~ m/.*?<\/container>/i) {
$search = $1;
- $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($search, 'BrowseDirectChildren', '', 0, 5, '');
+ $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($search, 'BrowseDirectChildren', '', 0, 15, '');
if ($result) {
$tmp = $result->getValue('Result');
@@ -4404,8 +4638,9 @@ sub SONOS_MakeCoverURL($$) {
if ($result) {
my $tmp = $result->getValue('Result');
+ my $coverOK = 0;
- if (defined($tmp) && $tmp =~ m/
- .*?(.*?)<\/upnp:albumArtURI>.*?<\/item>/i) {
+ while (!$coverOK && defined($tmp) && $tmp =~ m/
- .*?(.*?)<\/upnp:albumArtURI>.*?<\/item>/ig) {
$resURL = $1;
$resURL =~ s/%25/%/ig;
@@ -4415,19 +4650,30 @@ sub SONOS_MakeCoverURL($$) {
} else {
$resURL = $1.$resURL if (SONOS_Client_Data_Retreive($udn, 'reading', 'location', '') =~ m/^(http:\/\/.*?:.*?)\//i);
}
+
+ my $loadedCover = SONOS_ReadURL($resURL);
+ $coverOK = (defined($loadedCover) && ($loadedCover !~ m/.*<\/Error>/i))
}
}
}
} else {
my $stream = 0;
$stream = 1 if (($resURL =~ /x-sonosapi-stream/) && ($resURL !~ /x-sonos-http%3aamz/));
- $resURL = $1.'/getaa?'.($stream ? 's=1&' : '').'u='.SONOS_URI_Escape($resURL) if (SONOS_Client_Data_Retreive($udn, 'reading', 'location', '') =~ m/^(http:\/\/.*?:.*?)\//i);
+ $stream = 1 if (!$stream && (($resURL =~ /x-sonosapi-hls-static/) || ($resURL =~ /x-sonos-http:track%3a/)));
+
+ $resURL = SONOS_URI_Escape($resURL);
+ SONOS_Log undef, 0, 'resURL-1: '.$resURL;
+ $resURL =~ s/%26apos%3B/'/ig;
+ $resURL =~ s/%26amp%3B/%26/ig;
+ SONOS_Log undef, 0, 'resURL-2: '.$resURL;
+ $resURL = $1.'/getaa?'.($stream ? 's=1&' : '').'u='.$resURL if (SONOS_Client_Data_Retreive($udn, 'reading', 'location', '') =~ m/^(http:\/\/.*?:.*?)\//i);
+ SONOS_Log undef, 0, 'resURL-3: '.$resURL;
}
# Alles über Fhem als Proxy laufen lassen?
$resURL = '/fhem/sonos/proxy/aa?url='.SONOS_URI_Escape($resURL) if (($resURL !~ m/^\//) && SONOS_Client_Data_Retreive('undef', 'attr', 'generateProxyAlbumArtURLs', 0));
- SONOS_Log $udn, 5, 'MakeCoverURL-After: '.$resURL;
+ SONOS_Log $udn, 0, 'MakeCoverURL-After: '.$resURL;
return $resURL;
}
@@ -4449,7 +4695,7 @@ sub SONOS_getSpotifyCoverURL($;$) {
}
$infos =~ s/\\//g;
- $infos = $1.'original'.$3 if ($infos =~ m/(.*?\/)(cover|default)(\/.*)/i);
+ #$infos = $1.'original'.$3 if ($infos =~ m/(.*?\/)(cover|default)(\/.*)/i);
# Falls es ein Standardcover von Spotify geben soll, lieber das Thumbnail von Sonos verwenden...
return '' if ($infos =~ m/\/static\/img\/defaultCoverL.png/i);
@@ -4555,7 +4801,7 @@ sub SONOS_GetSpeakFile($$$$$) {
SONOS_Log $udn, 3, 'Load Google generated MP3 ('.$counter.'. Element) from "'.$url.'" to "'.$destFileName.$counter.'"';
- my $ua = LWP::UserAgent->new(agent => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_2) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11');
+ my $ua = LWP::UserAgent->new(agent => $SONOS_USERAGENT);
my $response = $ua->get($url, ':content_file' => $destFileName.$counter);
if (!$response->is_success) {
SONOS_Log $udn, 1, 'MP3 Download-Error: '.$response->status_line;
@@ -4983,31 +5229,49 @@ sub SONOS_PlayURITemp($$$) {
########################################################################################
sub SONOS_GetTrackProvider($;$) {
my ($songURI, $songTitle) = @_;
+ return ('', '', '') if (!defined($songURI) || ($songURI eq ''));
# Backslashe umwandeln
$songURI =~ s/\\/\//g;
# Gruppen- und LineIn-Wiedergaben bereits hier erkennen
if ($songURI =~ m/x-rincon:(RINCON_[\dA-Z]+)/) {
- return 'Gruppenwiedergabe: '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1);
+ return ('Gruppenwiedergabe: '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1), '', '');
} elsif ($songURI =~ m/x-rincon-stream:(RINCON_[\dA-Z]+)/) {
my $elem = 'LineIn';
$elem = $songTitle if (defined($songTitle) && $songTitle);
- return $elem.'-Wiedergabe: '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1);
+ return ($elem.'-Wiedergabe: '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1), '/fhem/sonos/cover/linein_round.png', '/fhem/sonos/cover/linein_quadratic.jpg');
} elsif ($songURI =~ m/x-sonos-dock:(RINCON_[\dA-Z]+)/) {
- return 'Dock-Wiedergabe: '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1);
+ return ('Dock-Wiedergabe: '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1), '/fhem/sonos/cover/dock_round.png', '/fhem/sonos/cover/dock_quadratic.jpg');
} elsif ($songURI =~ m/x-sonos-htastream:(RINCON_[\dA-Z]+):spdif/) {
- return 'SPDIF-Wiedergabe: '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1);
+ return ('SPDIF-Wiedergabe: '.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1), '/fhem/sonos/cover/playbar_round.png', '/fhem/sonos/cover/playbar_quadratic.jpg');
+ } elsif (($songURI =~ m/^http:(\/\/.*)/) || ($songURI =~ m/^aac:(\/\/.*)/) || ($songURI =~ m/x-rincon-mp3radio:\/\//)) {
+ return ('Radio', '/fhem/sonos/cover/tunein_round.png', '/fhem/sonos/cover/tunein_quadratic.jpg');
+ } elsif ($songURI =~ m/^\/\//) {
+ return ('Bibliothek', '/fhem/sonos/cover/bibliothek_round.png', '/fhem/sonos/cover/bibliothek_quadratic.jpg');
}
- # Hier die restlichen Erkennungen durchführen...
- for my $elem (keys %SONOS_ProviderList) {
- if ($songURI =~ /$elem/) {
- return $SONOS_ProviderList{$elem};
+ my $result = '';
+ my $roundIcon = '';
+ my $quadraticIcon = '';
+ eval {
+ my %musicServices = %{eval(SONOS_Client_Data_Retreive('undef', 'reading', 'MusicServicesList', '()'))};
+ if ($songURI =~ m/sid=(\d+)/i) {
+ my $sid = $1;
+ $result = $musicServices{$sid}{Name};
+ $result = '' if (!defined($result));
+
+ $roundIcon = $musicServices{$sid}{IconRoundURL};
+ $quadraticIcon = $musicServices{$sid}{IconQuadraticURL};
+
+ SONOS_Log undef, 4, 'TrackProvider for "'.$songURI.'" ~ SID='.$sid.' ~ Name: '.$result;
}
+ };
+ if ($@) {
+ SONOS_Log undef, 2, 'Unable to identify TrackProvider for "'.$songURI.'". Revert to empty default!';
}
- return '';
+ return ($result, $roundIcon, $quadraticIcon);
}
########################################################################################
@@ -5567,8 +5831,11 @@ sub SONOS_Discover_Callback($$$) {
my $devicePropertiesService = $device->getService('urn:schemas-upnp-org:service:DeviceProperties:1');
$SONOS_DevicePropertiesProxy{$udn} = $devicePropertiesService->controlProxy if ($devicePropertiesService);
+
#$SONOS_GroupManagementProxy{$udn} = $device->getService('urn:schemas-upnp-org:service:GroupManagement:1')->controlProxy if ($device->getService('urn:schemas-upnp-org:service:GroupManagement:1'));
- #$SONOS_MusicServicesProxy{$udn} = $device->getService('urn:schemas-upnp-org:service:MusicServices:1')->controlProxy if ($device->getService('urn:schemas-upnp-org:service:MusicServices:1'));
+
+ my $musicServicesService = $device->getService('urn:schemas-upnp-org:service:MusicServices:1');
+ $SONOS_MusicServicesProxy{$udn} = $musicServicesService->controlProxy if ($musicServicesService);
my $zoneGroupTopologyService = $device->getService('urn:schemas-upnp-org:service:ZoneGroupTopology:1');
$SONOS_ZoneGroupTopologyProxy{$udn} = $zoneGroupTopologyService->controlProxy if ($zoneGroupTopologyService);
@@ -5754,10 +6021,17 @@ sub SONOS_Discover_Callback($$$) {
my $tmp = $result->getValue('TrackURI');
$tmp =~ s/'/'/gi;
SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackURI', $tmp);
- SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackProvider', SONOS_GetTrackProvider($result->getValue('TrackURI')));
+ my ($trackProvider, $trackProviderRoundURL, $trackProviderQuadraticURL) = SONOS_GetTrackProvider($result->getValue('TrackURI'));
+ SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackProvider', $trackProvider);
+ SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackProviderIconRoundURL', $trackProviderRoundURL);
+ SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackProviderIconQuadraticURL', $trackProviderQuadraticURL);
SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackDuration', $result->getValue('TrackDuration'));
- SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackPosition', $result->getValue('RelTime'));
- SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackPositionSec', SONOS_GetTimeSeconds($result->getValue('RelTime')));
+ SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrackDurationSec', SONOS_GetTimeSeconds($result->getValue('TrackDuration')));
+
+ my $modus = 'ReadingsBulkUpdate'.((SONOS_Client_Data_Retreive($udn, 'reading', 'currentStreamAudio', 0)) ? 'IfChanged' : '');
+ SONOS_Client_Data_Refresh($modus, $udn, 'currentTrackPosition', $result->getValue('RelTime'));
+ SONOS_Client_Data_Refresh($modus, $udn, 'currentTrackPositionSec', SONOS_GetTimeSeconds($result->getValue('RelTime')));
+
SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', $udn, 'currentTrack', $result->getValue('Track'));
$result = $SONOS_AVTransportControlProxy{$udn}->GetMediaInfo(0);
@@ -5875,11 +6149,26 @@ sub SONOS_Discover_Callback($$$) {
SONOS_Log undef, 2, 'AudioIn-Service-subscribing successful with SID='.$SONOS_AudioInSubscriptions{$udn}->SID;
} else {
SONOS_Log undef, 1, 'AudioIn-Service-subscribing NOT successful';
+ delete($SONOS_AudioInSubscriptions{$udn});
}
} else {
undef($SONOS_AudioInSubscriptions{$udn});
}
+ # MusicServices-Subscription
+ if ($musicServicesService) {
+ $SONOS_MusicServicesSubscriptions{$udn} = $musicServicesService->subscribe(\&SONOS_MusicServicesCallback);
+ if (defined($SONOS_MusicServicesSubscriptions{$udn})) {
+ SONOS_Log undef, 2, 'MusicServices-Service-subscribing successful with SID='.$SONOS_MusicServicesSubscriptions{$udn}->SID;
+ } else {
+ SONOS_Log undef, 1, 'MusicServices-Service-subscribing NOT successful';
+ delete($SONOS_MusicServicesSubscriptions{$udn});
+ }
+ } else {
+ undef($SONOS_MusicServicesSubscriptions{$udn});
+ }
+
+
SONOS_Log undef, 3, 'Discover: End of discover-event for "'.$roomName.'".';
} elsif ($action eq 'deviceRemoved') {
my $udn = $device->UDN;
@@ -5923,9 +6212,12 @@ sub SONOS_GetDefineStringlist($$$$$$$$$$) {
push(@defs, 'CommandAttr:'.$name.' generateVolumeSlider 1');
push(@defs, 'CommandAttr:'.$name.' getAlarms 1');
push(@defs, 'CommandAttr:'.$name.' minVolume 0');
- push(@defs, 'CommandAttr:'.$name.' stateVariable Presence');
+ #push(@defs, 'CommandAttr:'.$name.' stateVariable Presence');
+ push(@defs, 'CommandAttr:'.$name.' stateFormat presence ~ currentTrackPositionSimulated / currentTrackDuration');
push(@defs, 'CommandAttr:'.$name.' getTitleInfoFromMaster 1');
+ push(@defs, 'CommandAttr:'.$name.' simulateCurrentTrackPosition 1');
+ push(@defs, 'CommandAttr:'.$name.' webCmd Volume');
#push(@defs, 'CommandAttr:'.$name.' webCmd Play:Pause:Previous:Next:VolumeD:VolumeU:MuteT');
} else {
push(@defs, 'CommandAttr:'.$name.' stateFormat presence');
@@ -5933,12 +6225,12 @@ sub SONOS_GetDefineStringlist($$$$$$$$$$) {
} elsif (lc($devicetype) eq 'sonosplayer_readingsgroup') {
if (!$isZoneBridge) {
if ($master) {
- push(@defs, 'CommandDefine:'.$name.'RG readingsGroup '.$name.':<{SONOS_getCoverTitleRG($DEVICE)}@infoSummarize2>');
- push(@defs, 'CommandAttr:'.$name.'RG room '.$sonosDeviceName);
- push(@defs, 'CommandAttr:'.$name.'RG group '.$groupName);
- push(@defs, 'CommandAttr:'.$name.'RG sortby 2');
- push(@defs, 'CommandAttr:'.$name.'RG noheading 1');
- push(@defs, 'CommandAttr:'.$name.'RG nonames 1');
+# push(@defs, 'CommandDefine:'.$name.'RG readingsGroup '.$name.':<{SONOS_getCoverTitleRG($DEVICE)}@infoSummarize2>');
+# push(@defs, 'CommandAttr:'.$name.'RG room '.$sonosDeviceName);
+# push(@defs, 'CommandAttr:'.$name.'RG group '.$groupName);
+# push(@defs, 'CommandAttr:'.$name.'RG sortby 2');
+# push(@defs, 'CommandAttr:'.$name.'RG noheading 1');
+# push(@defs, 'CommandAttr:'.$name.'RG nonames 1');
#push(@defs, 'CommandDefine:'.$name.'RG2 readingsGroup '.$name.':infoSummarize2@{SONOSPLAYER_GetMasterPlayerName($DEVICE)}');
#push(@defs, 'CommandAttr:'.$name.'RG2 valueFormat {" "}');
@@ -5963,19 +6255,19 @@ sub SONOS_GetDefineStringlist($$$$$$$$$$) {
} elsif (lc($devicetype) eq 'sonosplayer_remotecontrol') {
if (!$isZoneBridge) {
if ($master) {
- push(@defs, 'CommandDefine:'.$name.'RC remotecontrol');
- push(@defs, 'CommandAttr:'.$name.'RC room hidden');
- push(@defs, 'CommandAttr:'.$name.'RC group '.$sonosDeviceName);
- push(@defs, 'CommandAttr:'.$name.'RC rc_iconpath icons/remotecontrol');
- push(@defs, 'CommandAttr:'.$name.'RC rc_iconprefix black_btn_');
- push(@defs, 'CommandAttr:'.$name.'RC row00 Play:rc_PLAY.svg,Pause:rc_PAUSE.svg,Previous:rc_PREVIOUS.svg,Next:rc_NEXT.svg,:blank,VolumeD:rc_VOLDOWN.svg,VolumeU:rc_VOLUP.svg,:blank,MuteT:rc_MUTE.svg,ShuffleT:rc_SHUFFLE.svg,RepeatT:rc_REPEAT.svg');
+ #push(@defs, 'CommandDefine:'.$name.'RC remotecontrol');
+ #push(@defs, 'CommandAttr:'.$name.'RC room hidden');
+ #push(@defs, 'CommandAttr:'.$name.'RC group '.$sonosDeviceName);
+ #push(@defs, 'CommandAttr:'.$name.'RC rc_iconpath icons/remotecontrol');
+ #push(@defs, 'CommandAttr:'.$name.'RC rc_iconprefix black_btn_');
+ #push(@defs, 'CommandAttr:'.$name.'RC row00 Play:rc_PLAY.svg,Pause:rc_PAUSE.svg,Previous:rc_PREVIOUS.svg,Next:rc_NEXT.svg,:blank,VolumeD:rc_VOLDOWN.svg,VolumeU:rc_VOLUP.svg,:blank,MuteT:rc_MUTE.svg,ShuffleT:rc_SHUFFLE.svg,RepeatT:rc_REPEAT.svg');
- push(@defs, 'CommandDefine:'.$name.'RC_Notify notify '.$name.'RC set '.$name.' $EVENT');
+ #push(@defs, 'CommandDefine:'.$name.'RC_Notify notify '.$name.'RC set '.$name.' $EVENT');
- push(@defs, 'CommandDefine:'.$name.'RC_Weblink weblink htmlCode {fhem("get '.$name.'RC htmlcode", 1)}');
- push(@defs, 'CommandAttr:'.$name.'RC_Weblink room '.$sonosDeviceName);
- push(@defs, 'CommandAttr:'.$name.'RC_Weblink group '.$groupName);
- push(@defs, 'CommandAttr:'.$name.'RC_Weblink sortby 3');
+ #push(@defs, 'CommandDefine:'.$name.'RC_Weblink weblink htmlCode {fhem("get '.$name.'RC htmlcode", 1)}');
+ #push(@defs, 'CommandAttr:'.$name.'RC_Weblink room '.$sonosDeviceName);
+ #push(@defs, 'CommandAttr:'.$name.'RC_Weblink group '.$groupName);
+ #push(@defs, 'CommandAttr:'.$name.'RC_Weblink sortby 3');
}
}
}
@@ -6154,6 +6446,7 @@ sub SONOS_DeleteProxyObjects($) {
delete $SONOS_ZoneGroupTopologySubscriptions{$udn};
delete $SONOS_DevicePropertiesSubscriptions{$udn};
delete $SONOS_AudioInSubscriptions{$udn};
+ delete $SONOS_MusicServicesSubscriptions{$udn};
# Am Ende noch das Device entfernen...
delete $SONOS_UPnPDevice{$udn};
@@ -6183,22 +6476,29 @@ sub SONOS_GetReadingsToCurrentHash($$) {
$current{NumberOfTracks} = '';
$current{Track} = '';
$current{TrackURI} = '';
+ $current{TrackHandle} = '';
$current{TrackDuration} = '';
+ $current{TrackDurationSec} = '';
$current{TrackPosition} = '';
$current{TrackProvider} = '';
+ $current{TrackProviderIconQuadraticURL} = '';
+ $current{TrackProviderIconRoundURL} = '';
$current{TrackMetaData} = '';
$current{AlbumArtURI} = '';
$current{AlbumArtURL} = '';
$current{Title} = '';
$current{Artist} = '';
$current{Album} = '';
+ $current{Source} = '';
$current{OriginalTrackNumber} = '';
$current{AlbumArtist} = '';
$current{Sender} = '';
$current{SenderCurrent} = '';
$current{SenderInfo} = '';
$current{nextTrackDuration} = '';
+ $current{nextTrackDurationSec} = '';
$current{nextTrackURI} = '';
+ $current{nextTrackHandle} = '';
$current{nextAlbumArtURI} = '';
$current{nextAlbumArtURL} = '';
$current{nextTitle} = '';
@@ -6222,25 +6522,36 @@ sub SONOS_GetReadingsToCurrentHash($$) {
$current{NumberOfTracks} = ReadingsVal($name, 'numberOfTracks', '');
$current{Track} = ReadingsVal($name, 'currentTrack', '');
$current{TrackURI} = ReadingsVal($name, 'currentTrackURI', '');
- $current{TrackURI} = ReadingsVal($name, 'currentEnqueuedTransportURI', '');
+ $current{TrackHandle} = ReadingsVal($name, 'currentTrackHandle', '');
+ $current{EnqueuedTransportURI} = ReadingsVal($name, 'currentEnqueuedTransportURI', '');
+ $current{EnqueuedTransportHandle} = ReadingsVal($name, 'currentEnqueuedTransportHandle', '');
+
$current{TrackDuration} = ReadingsVal($name, 'currentTrackDuration', '');
+ $current{TrackDurationSec} = ReadingsVal($name, 'currentTrackDurationSec', '');
$current{TrackPosition} = ReadingsVal($name, 'currentTrackPosition', '');
$current{TrackPosition} = ReadingsVal($name, 'currentTrackPositionSec', '');
$current{TrackProvider} = ReadingsVal($name, 'currentTrackProvider', '');
+ $current{TrackProviderIconQuadraticURL} = ReadingsVal($name, 'currentTrackProviderIconQuadraticURL', '');
+ $current{TrackProviderIconRoundURL} = ReadingsVal($name, 'currentTrackProviderIconRoundURL', '');
#$current{TrackMetaData} = '';
$current{AlbumArtURI} = ReadingsVal($name, 'currentAlbumArtURI', '');
$current{AlbumArtURL} = ReadingsVal($name, 'currentAlbumArtURL', '');
$current{Title} = ReadingsVal($name, 'currentTitle', '');
$current{Artist} = ReadingsVal($name, 'currentArtist', '');
$current{Album} = ReadingsVal($name, 'currentAlbum', '');
+ $current{Source} = ReadingsVal($name, 'currentSource', '');
$current{OriginalTrackNumber} = ReadingsVal($name, 'currentOriginalTrackNumber', '');
$current{AlbumArtist} = ReadingsVal($name, 'currentAlbumArtist', '');
$current{Sender} = ReadingsVal($name, 'currentSender', '');
$current{SenderCurrent} = ReadingsVal($name, 'currentSenderCurrent', '');
$current{SenderInfo} = ReadingsVal($name, 'currentSenderInfo', '');
$current{nextTrackDuration} = ReadingsVal($name, 'nextTrackDuration', '');
+ $current{nextTrackDurationSec} = ReadingsVal($name, 'nextTrackDurationSec', '');
$current{nextTrackURI} = ReadingsVal($name, 'nextTrackURI', '');
+ $current{nextTrackHandle} = ReadingsVal($name, 'nextTrackHandle', '');
$current{nextTrackProvider} = ReadingsVal($name, 'nextTrackProvider', '');
+ $current{nextTrackProviderIconQuadraticURL} = ReadingsVal($name, 'nextTrackProviderIconQuadraticURL', '');
+ $current{nextTrackProviderIconRoundURL} = ReadingsVal($name, 'nextTrackProviderIconRoundURL', '');
$current{nextAlbumArtURI} = ReadingsVal($name, 'nextAlbumArtURI', '');
$current{nextAlbumArtURL} = ReadingsVal($name, 'nextAlbumArtURL', '');
$current{nextTitle} = ReadingsVal($name, 'nextTitle', '');
@@ -6470,15 +6781,15 @@ sub SONOS_TransportCallback($$) {
# Für die Bookmarkverwaltung ablegen
$SONOS_BookmarkSpeicher{OldTrackURIs}{$udn} = $currentTrackURI;
+ my $enqueuedTransportMetaData = decode_entities($1) if ($properties{LastChangeDecoded} =~ m//i);
+
# Wenn es ein Spotify-Track ist, dann den Benutzernamen sichern, damit man diesen beim nächsten Export zur Verfügung hat
if ($currentTrackURI =~ m/^x-sonos-spotify:/i) {
- my $enqueuedTransportMetaData = decode_entities($1) if ($properties{LastChangeDecoded} =~ m//i);
SONOS_Client_Notifier('ReadingsSingleUpdateIfChangedNoTrigger:undef:UserID_Spotify:'.SONOS_URI_Escape($1)) if ($enqueuedTransportMetaData =~ m/(SA_.*?)<\/desc>/i);
}
# Wenn es ein Napster/Rhapsody-Track ist, dann den Benutzernamen sichern, damit man diesen beim nächsten Export zur Verfügung hat
if ($currentTrackURI =~ m/^npsdy:/i) {
- my $enqueuedTransportMetaData = decode_entities($1) if ($properties{LastChangeDecoded} =~ m//i);
SONOS_Client_Notifier('ReadingsSingleUpdateIfChangedNoTrigger:undef:UserID_Napster:'.SONOS_URI_Escape($1)) if ($enqueuedTransportMetaData =~ m/(SA_.*?)<\/desc>/i);
}
@@ -6486,20 +6797,39 @@ sub SONOS_TransportCallback($$) {
my $enqueuedTransportURI = decode_entities($1) if ($properties{LastChangeDecoded} =~ m//i);
$enqueuedTransportURI = "" if (!defined($enqueuedTransportURI));
SONOS_Client_Notifier('SetCurrent:EnqueuedTransportURI:'.decode_entities($enqueuedTransportURI));
+ SONOS_Client_Notifier('SetCurrent:EnqueuedTransportHandle:'.decode_entities($enqueuedTransportURI).'|'.$enqueuedTransportMetaData);
+ if ($enqueuedTransportMetaData =~ m/(.*?)<\/dc:title>/) {
+ SONOS_Log $udn, 5, 'UTF8-Decode-Title1: '.$1;
+ my $text = $1;
+ eval {
+ $text = Encode::decode('UTF-8', $text, Encode::FB_CROAK);
+ };
+ eval {
+ SONOS_Log $udn, 5, 'UTF8-Decode-Title2: '.$text;
+ $text = Encode::decode('UTF-8', $text, Encode::FB_CROAK);
+ };
+ if ($@) {
+ SONOS_Log $udn, 5, 'UTF8-Decode: '.$@;
+ }
+ SONOS_Log $udn, 5, 'UTF8-Decode-Title3: '.$text;
+ SONOS_Client_Notifier('SetCurrent:Source:'.Encode::encode('UTF-8', $text));
+ }
# Current Trackdauer ermitteln
if ($properties{LastChangeDecoded} =~ m//i) {
SONOS_Client_Notifier('SetCurrent:TrackDuration:'.decode_entities($1));
+ SONOS_Client_Notifier('SetCurrent:TrackDurationSec:'.SONOS_GetTimeSeconds(decode_entities($1)));
$SONOS_BookmarkSpeicher{OldTrackDurations}{$udn} = SONOS_GetTimeSeconds(decode_entities($1));
}
# Current Track Metadaten ermitteln
my $currentTrackMetaData = decode_entities($1) if ($properties{LastChangeDecoded} =~ m//is);
SONOS_Log $udn, 4, 'Transport-Event: CurrentTrackMetaData: '.$currentTrackMetaData;
+ SONOS_Client_Notifier('SetCurrent:TrackHandle:'.$currentTrackURI.'|'.$currentTrackMetaData);
# Cover herunterladen (Infos dazu in den Track Metadaten)
my $tempURIground = decode_entities($currentTrackMetaData);
- $tempURIground =~ s/%25/%/ig;
+ #$tempURIground =~ s/%25/%/ig;
my $tempURI = '';
$tempURI = ($1) if ($tempURIground =~ m/(.*?)<\/upnp:albumArtURI>/i);
@@ -6507,9 +6837,6 @@ sub SONOS_TransportCallback($$) {
if ($tempURI =~ m/^(http:\/\/.*?\/)(.*)/) {
$groundURL = $1;
$tempURI = $2;
- } elsif ($tempURI =~ m/^x-sonosapi-stream:(.+?)\?/i) {
- $groundURL = 'http://cdn-radiotime-logos.tunein.com/';
- $tempURI = $1.'g.png';
}
SONOS_Client_Notifier('ProcessCover:'.$udn.':0:'.$tempURI.':'.$groundURL);
@@ -6523,7 +6850,11 @@ sub SONOS_TransportCallback($$) {
# Sender ermitteln (per SOAP-Request an den SonosPlayer)
if ($service->controlProxy()->GetMediaInfo(0)->getValue('CurrentURIMetaData') =~ m/(.*?)<\/dc:title>/i) {
SONOS_Client_Notifier('SetCurrent:Sender:'.$1);
- SONOS_Client_Notifier('SetCurrent:TrackProvider:'.SONOS_GetTrackProvider($currentTrackURI, $1));
+
+ my ($trackProvider, $trackProviderRoundURL, $trackProviderQuadraticURL) = SONOS_GetTrackProvider($currentTrackURI, $1);
+ SONOS_Client_Notifier('SetCurrent:TrackProvider:'.$trackProvider);
+ SONOS_Client_Notifier('SetCurrent:TrackProviderIconRoundURL:'.$trackProviderRoundURL);
+ SONOS_Client_Notifier('SetCurrent:TrackProviderIconQuadraticURL:'.$trackProviderQuadraticURL);
$SONOS_BookmarkSpeicher{OldTitles}{$udn} = $1;
}
@@ -6614,7 +6945,11 @@ sub SONOS_TransportCallback($$) {
$SONOS_BookmarkSpeicher{OldTitles}{$udn} = '('.$currentArtist.') '.$currentTitle;
}
- SONOS_Client_Notifier('SetCurrent:TrackProvider:'.SONOS_GetTrackProvider($currentTrackURI, $currentTitle));
+ my ($trackProvider, $trackProviderRoundURL, $trackProviderQuadraticURL) = SONOS_GetTrackProvider($currentTrackURI, $currentTitle);
+ SONOS_Client_Notifier('SetCurrent:TrackProvider:'.$trackProvider);
+ SONOS_Client_Notifier('SetCurrent:TrackProviderIconRoundURL:'.$trackProviderRoundURL);
+ SONOS_Client_Notifier('SetCurrent:TrackProviderIconQuadraticURL:'.$trackProviderQuadraticURL);
+ $SONOS_BookmarkSpeicher{OldTitles}{$udn} = $1;
# Original Tracknumber ermitteln
SONOS_Client_Notifier('SetCurrent:OriginalTrackNumber:'.decode_entities($1)) if ($currentTrackMetaData =~ m/(.*?)<\/upnp:originalTrackNumber>/i);
@@ -6630,16 +6965,23 @@ sub SONOS_TransportCallback($$) {
SONOS_Log $udn, 4, 'Transport-Event: NextTrackMetaData: '.$nextTrackMetaData;
SONOS_Client_Notifier('SetCurrent:nextTrackDuration:'.decode_entities($1)) if ($nextTrackMetaData =~ m//i);
+ SONOS_Client_Notifier('SetCurrent:nextTrackDurationSec:'.SONOS_GetTimeSeconds(decode_entities($1))) if ($nextTrackMetaData =~ m//i);
if ($properties{LastChangeDecoded} =~ m//i) {
my $tmp = SONOS_GetURIFromQueueValue($1);
$tmp =~ s/'/'/gi;
SONOS_Client_Notifier('SetCurrent:nextTrackURI:'.$tmp);
- SONOS_Client_Notifier('SetCurrent:nextTrackProvider:'.SONOS_GetTrackProvider($tmp));
+ SONOS_Client_Notifier('SetCurrent:nextTrackHandle:'.$tmp.'|'.$nextTrackMetaData);
+
+ my ($trackProvider, $trackProviderRoundURL, $trackProviderQuadraticURL) = SONOS_GetTrackProvider($tmp);
+ SONOS_Client_Notifier('SetCurrent:nextTrackProvider:'.$trackProvider);
+ SONOS_Client_Notifier('SetCurrent:nextTrackProviderIconRoundURL:'.$trackProviderRoundURL);
+ SONOS_Client_Notifier('SetCurrent:nextTrackProviderIconQuadraticURL:'.$trackProviderQuadraticURL);
+
}
$tempURIground = decode_entities($nextTrackMetaData);
- $tempURIground =~ s/%25/%/ig;
+ #$tempURIground =~ s/%25/%/ig;
$tempURI = '';
$tempURI = ($1) if ($tempURIground =~ m/(.*?)<\/upnp:albumArtURI>/i);
@@ -8307,6 +8649,157 @@ sub SONOS_AudioInCallback($$) {
return 0;
}
+########################################################################################
+#
+# SONOS_MusicServicesCallback - MusicServices-Callback,
+#
+# Parameter $service = Service-Representing Object
+# $properties = Properties, that have been changed in this event
+#
+########################################################################################
+sub SONOS_MusicServicesCallback($$) {
+ my ($service, %properties) = @_;
+
+ my $udn = $SONOS_Locations{$service->base};
+ my $udnShort = $1 if ($udn =~ m/(.*?)_MR/i);
+
+ if (!$udn) {
+ SONOS_Log undef, 1, 'MusicServices-Event receive error: SonosPlayer not found; Searching for \''.$service->base.'\'!';
+ return;
+ }
+
+ my $name = SONOS_Client_Data_Retreive($udn, 'def', 'NAME', $udn);
+
+ # If the Device is disabled, return here...
+ if (SONOS_Client_Data_Retreive($udn, 'attr', 'disable', 0) == 1) {
+ SONOS_Log $udn, 3, "MusicServices-Event: device '$name' disabled. No Events/Data will be processed!";
+ return;
+ }
+
+ SONOS_Log $udn, 3, 'Event: Received MusicServices-Event for Zone "'.$name.'".';
+ $SONOS_Client_SendQueue_Suspend = 1;
+
+ # Check if the correct ServiceType
+ if ($service->serviceType() ne 'urn:schemas-upnp-org:service:MusicServices:1') {
+ SONOS_Log $udn, 1, 'MusicServices-Event receive error: Wrong Servicetype, was \''.$service->serviceType().'\'!';
+ return;
+ }
+
+ SONOS_Log $udn, 4, "MusicServices-Event: All correct with this service-call till now. UDN='uuid:".$udn."'";
+
+ SONOS_Client_Notifier('ReadingsBeginUpdate:undef');
+
+ # ServiceListVersion wurde angepasst?
+ my $serviceListVersion = SONOS_Client_Data_Retreive('undef', 'reading', 'MusicServicesListVersion', '');
+ if (defined($properties{ServiceListVersion}) && $properties{ServiceListVersion} ne '') {
+ if ($serviceListVersion ne $properties{ServiceListVersion}) {
+ SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'MusicServicesListVersion', $properties{ServiceListVersion});
+
+ # Call MusicServiceProxy...
+ my $response = $SONOS_MusicServicesProxy{$udn}->ListAvailableServices();
+
+ # ServiceTypes
+ my @serviceTypes = split(',', $response->getValue('AvailableServiceTypeList'));
+ my $servicepos = 0;
+
+ my %musicServices = ();
+ my $result = $response->getValue('AvailableServiceDescriptorList');
+ SONOS_Log undef, 5, 'MusicService-Call: '.$result;
+ while ($result =~ m/.*?.*?.*?<\/Service>/sgi) {
+ my $id = $1;
+ my $name = $2;
+ my $smapi = $3;
+ my $capabilities = $4;
+ my $stringsURL = $5;
+
+ my $presentationMap = $6;
+
+ my $promoString = '';
+ if (defined($stringsURL) && ($stringsURL ne '')) {
+ my $strings = encode('UTF-8', get($stringsURL));
+ if (defined($strings) && ($strings ne '')) {
+ $promoString = $1 if ($strings =~ m/.*?(.*?)<\/string>.*?<\/stringtable>/si);
+ $promoString = $1 if (($promoString eq '') && ($strings =~ m/.*?(.*?)<\/string>.*?<\/stringtable>/si));
+ }
+ }
+
+ my $presentationMapData = encode('UTF-8', get($presentationMap));
+ if (defined($presentationMapData)) {
+ SONOS_Log undef, 5, 'PresentationMap('.$id.' ~ '.$name.'): '.$presentationMapData;
+ } else {
+ SONOS_Log undef, 5, 'PresentationMap('.$id.' ~ '.$name.'): undef';
+ }
+
+ my ($resolution, $resolutionSubst) = SONOS_ExtractMaxResolution($presentationMapData, 'ArtWorkSizeMap');
+ if (!defined($resolution)) {
+ ($resolution, $resolutionSubst) = SONOS_ExtractMaxResolution($presentationMapData, 'BrowseIconSizeMap');
+ }
+
+ #SONOS_GetMediaMetadata($udn, $id,
+
+ $musicServices{$id}{Name} = $name;
+ $musicServices{$id}{ServiceType} = $serviceTypes[$servicepos++];
+
+ $musicServices{$id}{IconQuadraticURL} = 'http://sonos-logo.ws.sonos.com/'.$musicServices{$id}{ServiceType}.'/'.$musicServices{$id}{ServiceType}.'-400x400.png';
+ $musicServices{$id}{IconQuadraticURL} = '/fhem/sonos/proxy/aa?url='.SONOS_URI_Escape($musicServices{$id}{IconQuadraticURL}) if (SONOS_Client_Data_Retreive('undef', 'attr', 'generateProxyAlbumArtURLs', 0));
+
+ $musicServices{$id}{IconRoundURL} = 'http://sonos-logo.ws.sonos.com/'.$musicServices{$id}{ServiceType}.'/'.$musicServices{$id}{ServiceType}.'-72x72.png';
+ $musicServices{$id}{IconRoundURL} = '/fhem/sonos/proxy/aa?url='.SONOS_URI_Escape($musicServices{$id}{IconRoundURL}) if (SONOS_Client_Data_Retreive('undef', 'attr', 'generateProxyAlbumArtURLs', 0));
+
+ $musicServices{$id}{SMAPI} = $smapi;
+ $musicServices{$id}{Resolution} = $resolution;
+ $musicServices{$id}{ResolutionSubstitution} = $resolutionSubst;
+ $musicServices{$id}{Capabilities} = $capabilities;
+
+ $promoString =~ s/[\r\n]/ /g;
+ $musicServices{$id}{PromoText} = $promoString;
+ }
+
+ SONOS_Log undef, 5, 'MusicService-List: '.SONOS_Dumper(\%musicServices);
+ SONOS_Client_Data_Refresh('ReadingsBulkUpdateIfChanged', 'undef', 'MusicServicesList', SONOS_Dumper(\%musicServices));
+ }
+ }
+
+ SONOS_Client_Notifier('ReadingsEndUpdate:undef');
+
+ $SONOS_Client_SendQueue_Suspend = 0;
+ SONOS_Log $udn, 3, 'Event: End of MusicServices-Event for Zone "'.$name.'".';
+
+ # Prüfen, ob der Player auf 'disappeared' steht, und in diesem Fall den DiscoverProcess neu anstarten...
+ if (SONOS_Client_Data_Retreive($udn, 'reading', 'presence', 'disappeared') eq 'disappeared') {
+ SONOS_Log $udn, 1, "MusicServices-Event: device '$name' is marked as disappeared. Restarting discovery-process!";
+
+ SONOS_RestartControlPoint();
+ }
+
+ return 0;
+}
+
+########################################################################################
+#
+# SONOS_ExtractMaxResolution - Extracts the available Coversizes
+#
+########################################################################################
+sub SONOS_ExtractMaxResolution($$) {
+ my ($map, $area) = @_;
+ return (undef, undef) if (!defined($map));
+
+
+ my $artworkSizeMap = $1 if ($map =~ m/.*?.*?(.*?)<\/imageSizeMap>.*?<\/Match>.*?<\/PresentationMap>/is);
+ return (undef, undef) if (!defined($artworkSizeMap));
+
+ my @resolutions = ();
+ while ($artworkSizeMap =~ m/.*?/gis) {
+ push(@resolutions, $1);
+ }
+ @resolutions = sort {$b <=> $a} @resolutions;
+ my $resolution = $resolutions[0];
+
+ my $resolutionSubst = $1 if ($artworkSizeMap =~ m/.*?/gis);
+
+ return ($resolution, $resolutionSubst);
+}
+
########################################################################################
#
# SONOS_replaceSpecialStringCharacters - Replaces invalid Characters in Strings (like ") for FHEM-internal
@@ -8621,6 +9114,121 @@ sub SONOS_DownloadReplaceIfChanged($$) {
}
}
+########################################################################################
+#
+# SONOS_GetRadioMediaMetadata - Read the Radio-Metadata from the Sonos-Webservice
+#
+########################################################################################
+sub SONOS_GetRadioMediaMetadata($$) {
+ my ($udn, $id) = @_;
+ my $udnKey = "$1-$2-$3-$4-$5-$6:D" if ($udn =~ m/RINCON_([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})01400(_MR|)/);
+
+ return '' if (!defined($udnKey));
+
+ my $ua = LWP::UserAgent->new(agent => $SONOS_USERAGENT);
+ my $response = $ua->request(POST 'http://legato.radiotime.com/Radio.asmx', 'content-type' => 'text/xml; charset="utf-8"',
+ Content => "
+
+
+
+ $udnKey
+ Sonos
+
+
+
+
+ $id
+
+
+ ");
+ SONOS_Log $udn, 5, 'Radioservice-Metadata: '.$response->content;
+
+ my $title = $1 if ($response->content =~ m/(.*?)<\/title>/i);
+ my $genreId = $1 if ($response->content =~ m/(.*?)<\/genreId>/i);
+ my $genre = $1 if ($response->content =~ m/(.*?)<\/genre>/i);
+ my $bitrate = $1 if ($response->content =~ m/(.*?)<\/bitrate>/i);
+ my $logo = $1 if ($response->content =~ m/(.*?)<\/logo>/i); $logo =~ s/(.*?)q(\..*)/$1g$2/;
+ my $subtitle = $1 if ($response->content =~ m/(.*?)<\/subtitle>/i);
+
+ return $logo;
+}
+
+########################################################################################
+#
+# SONOS_GetMediaMetadata - Read the Media-Metadata from the Sonos-Webservice
+#
+########################################################################################
+sub SONOS_GetMediaMetadata($$$) {
+ my ($udn, $sid, $id) = @_;
+ my $udnKey = "$1-$2-$3-$4-$5-$6:D" if ($udn =~ m/RINCON_([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})01400(_MR|)/);
+
+ # Shorthand to special handling for TuneIn-Artwork
+ return SONOS_GetRadioMediaMetadata($udn, $id) if ($sid == 254);
+
+ # Normal Artwork...
+ $udn =~ s/RINCON_(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)01400(_MR|)/$1-$2-$3-$4-$5-$6:D/;
+
+ my $musicServicesList = SONOS_Client_Data_Retreive($udn, 'reading', 'MusicServicesList', '()');
+ return '' if (!$musicServicesList);
+ my %musicService = %{eval($musicServicesList)->{$sid}};
+
+ my $url = $musicService{SMAPI};
+ if ($url) {
+ my $ua = LWP::UserAgent->new(agent => $SONOS_USERAGENT);
+ my $response = $ua->request(POST $url, 'content-type' => 'text/xml; charset="utf-8"',
+ Content => "
+
+
+
+ $udnKey
+ Sonos
+
+
+
+
+ $id
+
+
+ ");
+ SONOS_Log $udn, 0, 'MediaMetadata: '.$response->content;
+
+ my $title = $1 if ($response->content =~ m/(.*?)<\/title>/i);
+ my $genreId = $1 if ($response->content =~ m/(.*?)<\/genreId>/i);
+ my $genre = $1 if ($response->content =~ m/(.*?)<\/genre>/i);
+ my $bitrate = $1 if ($response->content =~ m/(.*?)<\/bitrate>/i);
+
+ my $logo = $1 if ($response->content =~ m/(.*?)<\/logo>/i);
+ if ($musicService{ResolutionSubstitution}) {
+ $logo =~ s//$musicService{ResolutionSubstitution}/;
+ }
+
+ my $subtitle = $1 if ($response->content =~ m/(.*?)<\/subtitle>/i);
+
+ return $logo;
+ } else {
+ return '';
+ }
+}
+
+########################################################################################
+#
+# SONOS_ReadURL - Read the content of the given URL
+#
+# Parameter $url = The url, that has to be read
+#
+########################################################################################
+sub SONOS_ReadURL($) {
+ my ($url) = @_;
+
+ my $ua = LWP::UserAgent->new(agent => $SONOS_USERAGENT);
+ my $response = $ua->get($url);
+ if ($response->is_success) {
+ return $response->content;
+ }
+
+ return undef;
+}
+
########################################################################################
#
# SONOS_ReadFile - Read the content of the given filename
@@ -8752,7 +9360,7 @@ sub SONOS_getSonosPlayerByName(;$) {
}
}
- SONOS_Log undef, 0, "The Method 'SONOS_getSonosPlayerByName' cannot find the FHEM-Device according to '$devicename'. This should not happen!";
+ SONOS_Log undef, 0, "The Method 'SONOS_getSonosPlayerByName' cannot find the FHEM-Device according to '".(defined($devicename) ? $devicename : 'undef')."'. This should not happen!";
return undef;
}
@@ -8773,7 +9381,7 @@ sub SONOS_getSonosPlayerByUDN(;$) {
return SONOS_getSonosPlayerByName();
}
- SONOS_Log undef, 0, "The Method 'SONOS_getSonosPlayerByUDN' cannot find the FHEM-Device according to '$udn'. This should not happen!";
+ SONOS_Log $udn, 0, "The Method 'SONOS_getSonosPlayerByUDN' cannot find the FHEM-Device according to '".(defined($udn) ? $udn : 'undef')."'. This should not happen!";
return undef;
}
@@ -8790,7 +9398,7 @@ sub SONOS_getSonosPlayerByRoomName($) {
return $main::defs{$fhem_dev} if($main::defs{$fhem_dev}{TYPE} eq 'SONOSPLAYER' && $main::defs{$fhem_dev}{READINGS}{roomName}{VAL} eq $roomName);
}
- SONOS_Log undef, 0, "The Method 'SONOS_getSonosPlayerByRoomName' cannot find the FHEM-Device according to '$roomName'. This should not happen!";
+ SONOS_Log undef, 0, "The Method 'SONOS_getSonosPlayerByRoomName' cannot find the FHEM-Device according to '".(defined($roomName) ? $roomName : 'undef')."'. This should not happen!";
return undef;
}
@@ -8975,7 +9583,7 @@ sub SONOS_GetTimeFromString($) {
eval {
use Time::Local;
if($timeStr =~ m/^(\d{4})-(\d{2})-(\d{2})( |_)([0-2]\d):([0-5]\d):([0-5]\d)$/) {
- return timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900);
+ return timelocal($7, $6, $5, $3, $2 - 1, $1 - 1900);
}
}
}
@@ -9100,9 +9708,6 @@ if (defined($SONOS_ListenPort)) {
while ($runEndlessLoop) {
# NormalQueueWorking wird für die Dauer einer Direkt-Wert-Anfrage deaktiviert, damit hier nicht blockiert und/oder zuviel weggelesen wird.
if ($SONOS_Client_NormalQueueWorking) {
- # Falls wir hier auf eine Antwort reagieren würden, die gar nicht hierfür bestimmt ist, dann übergehen...
- next if (!$SONOS_Client_NormalQueueWorking);
-
# Nachschauen, ob Subscriptions erneuert werden müssen
if (time() - $lastRenewSubscriptionCheckTime > 1800) {
$lastRenewSubscriptionCheckTime = time ();
@@ -9153,13 +9758,15 @@ if (defined($SONOS_ListenPort)) {
SONOS_Log undef, 3, "Connection accepted from $name:$port";
- # Von dort kommt die Anfrage, dort finde ich den Telnet-Port von Fhem :-)
- $SONOS_UseTelnetForQuestions_Host = $name;
-
# Send Welcome-Message
- send($client, "'This is UPnP-Server calling'\r\n", 0);
+ send($client, "This is UPnP-Server listening for commands\r\n", 0);
- $SONOS_Client_Selector->add($client);
+ # Antwort lesen, und nur wenn es eine dauerhaft gedachte Verbindung ist, dann auch merken...
+ my $answer = '';
+ recv($client, $answer, 500, 0);
+ if ($answer eq "Establish connection\r\n") {
+ $SONOS_Client_Selector->add($client);
+ }
} else { # Existing client calling
if (!$so->opened()) {
$SONOS_Client_Selector->remove($so);
@@ -9180,7 +9787,7 @@ if (defined($SONOS_ListenPort)) {
}
} else {
# Wenn die Verarbeitung gerade unterbrochen sein soll, dann hier etwas warten, um keine 100% CPU-Last zu erzeugen
- select(undef, undef, undef, 0.5);
+ select(undef, undef, undef, 0.2);
}
}
@@ -9734,6 +10341,8 @@ The order in the sublists are important, because the first entry defines the so-
Common
coverLoadTimeout <value>
One of (0..10,15,20,25,30). Defines the timeout for waiting of the Sonosplayer for Cover-Downloads. Defaults to 5.
+deviceRoomView <Both|DeviceLineOnly>
+
Defines the style of the Device in the room overview. Both
means "normal" Deviceline incl. Cover-/Titleview and maybe the control area, DeviceLineOnly
means only the "normal" Deviceline-view.
disable <value>
One of (0,1). With this value you can disable the whole module. Works immediatly. If set to 1 the subprocess will be terminated and no message will be transmitted. If set to 0 the subprocess is again started.
It is useful when you install new Sonos-Components and don't want any disgusting devices during the Sonos setup.
getListsDirectlyToReadings <value>
@@ -9914,6 +10523,8 @@ Dabei ist die Reihenfolge innerhalb der Unterlisten wichtig, da der erste Eintra
- Grundsätzliches
coverLoadTimeout <value>
Eines von (0..10,15,20,25,30). Definiert den Timeout der für die Abfrage des Covers beim Sonosplayer verwendet wird. Wenn nicht angegeben, dann wird 5 verwendet.
+deviceRoomView <Both|DeviceLineOnly>
+
Gibt an, was in der Raumansicht zum Sonosplayer-Device angezeigt werden soll. Both
bedeutet "normale" Devicezeile zzgl. Cover-/Titelanzeige und u.U. Steuerbereich, DeviceLineOnly
bedeutet nur die Anzeige der "normalen" Devicezeile.
disable <value>
Eines von (0,1). Hiermit kann das Modul abgeschaltet werden. Wirkt sofort. Bei 1 wird der SubProzess beendet, und somit keine weitere Verarbeitung durchgeführt. Bei 0 wird der Prozess wieder gestartet.
Damit kann das Modul temporär abgeschaltet werden, um bei der Neueinrichtung von Sonos-Komponenten keine halben Zustände mitzubekommen.
getListsDirectlyToReadings <value>
diff --git a/fhem/FHEM/21_SONOSPLAYER.pm b/fhem/FHEM/21_SONOSPLAYER.pm
index 7e128029a..2cfe3438d 100755
--- a/fhem/FHEM/21_SONOSPLAYER.pm
+++ b/fhem/FHEM/21_SONOSPLAYER.pm
@@ -1,6 +1,6 @@
########################################################################################
#
-# SONOSPLAYER.pm (c) by Reiner Leins, April 2017
+# SONOSPLAYER.pm (c) by Reiner Leins, Mai 2017
# rleins at lmsoft dot de
#
# $Id$
@@ -58,6 +58,13 @@ sub Log($$);
sub Log3($$$);
sub SONOSPLAYER_Log($$$);
+
+########################################################
+# Standards aus FHEM einbinden
+########################################################
+use vars qw{%modules %defs};
+
+
########################################################################################
# Variable Definitions
########################################################################################
@@ -99,6 +106,8 @@ my %sets = (
'StartRadio' => 'radioname',
'PlayURI' => 'songURI [Volume]',
'PlayURITemp' => 'songURI [Volume]',
+ 'LoadHandle' => 'Handle',
+ 'StartHandle' => 'Handle',
'AddURIToQueue' => 'songURI',
'Speak' => 'volume(0..100) language text',
'OutputFixed' => 'state',
@@ -178,8 +187,13 @@ sub SONOSPLAYER_Initialize ($) {
$hash->{SetFn} = "SONOSPLAYER_Set";
$hash->{StateFn} = "SONOSPLAYER_State";
$hash->{AttrFn} = 'SONOSPLAYER_Attribute';
+ $hash->{NotifyFn} = 'SONOSPLAYER_Notify';
- $hash->{AttrList} = "disable:1,0 generateVolumeSlider:1,0 generateVolumeEvent:1,0 generateSomethingChangedEvent:1,0 generateInfoSummarize1 generateInfoSummarize2 generateInfoSummarize3 generateInfoSummarize4 stateVariable:TransportState,NumberOfTracks,Track,TrackURI,TrackDuration,TrackProvider,Title,Artist,Album,OriginalTrackNumber,AlbumArtist,Sender,SenderCurrent,SenderInfo,StreamAudio,NormalAudio,AlbumArtURI,nextTrackDuration,nextTrackProvider,nextTrackURI,nextAlbumArtURI,nextTitle,nextArtist,nextAlbum,nextAlbumArtist,nextOriginalTrackNumber,Volume,Mute,OutputFixed,Shuffle,Repeat,CrossfadeMode,Balance,HeadphoneConnected,SleepTimer,Presence,RoomName,SaveRoomName,PlayerType,Location,SoftwareRevision,SerialNum,InfoSummarize1,InfoSummarize2,InfoSummarize3,InfoSummarize4 model minVolume maxVolume minVolumeHeadphone maxVolumeHeadphone VolumeStep getAlarms:1,0 buttonEvents getTitleInfoFromMaster:1,0 stopSleeptimerInAction:1,0 saveSleeptimerInAction:1,0 ".$readingFnAttributes;
+ $hash->{FW_detailFn} = 'SONOSPLAYER_Detail';
+ $hash->{FW_deviceOverview} = 1;
+ #$hash->{FW_addDetailToSummary} = 1;
+
+ $hash->{AttrList} = "disable:1,0 generateVolumeSlider:1,0 generateVolumeEvent:1,0 generateSomethingChangedEvent:1,0 generateInfoSummarize1 generateInfoSummarize2 generateInfoSummarize3 generateInfoSummarize4 stateVariable:TransportState,NumberOfTracks,Track,TrackURI,TrackDuration,TrackProvider,Title,Artist,Album,OriginalTrackNumber,AlbumArtist,Sender,SenderCurrent,SenderInfo,StreamAudio,NormalAudio,AlbumArtURI,nextTrackDuration,nextTrackProvider,nextTrackURI,nextAlbumArtURI,nextTitle,nextArtist,nextAlbum,nextAlbumArtist,nextOriginalTrackNumber,Volume,Mute,OutputFixed,Shuffle,Repeat,CrossfadeMode,Balance,HeadphoneConnected,SleepTimer,Presence,RoomName,SaveRoomName,PlayerType,Location,SoftwareRevision,SerialNum,InfoSummarize1,InfoSummarize2,InfoSummarize3,InfoSummarize4 model minVolume maxVolume minVolumeHeadphone maxVolumeHeadphone VolumeStep getAlarms:1,0 buttonEvents getTitleInfoFromMaster:1,0 stopSleeptimerInAction:1,0 saveSleeptimerInAction:1,0 simulateCurrentTrackPosition:0,1,2,3,4,5,6,7,8,9,10,15,20,25,30,45,60 simulateCurrentTrackPositionPercentFormat suppressControlButtons:1,0 ".$readingFnAttributes;
return undef;
}
@@ -193,8 +207,6 @@ sub SONOSPLAYER_Initialize ($) {
########################################################################################
sub SONOSPLAYER_Define ($$) {
my ($hash, $def) = @_;
-
- # $hash->{NotifyFn} = 'SONOSPLAYER_Notify';
# define SONOSPLAYER
# e.g.: define Sonos_Wohnzimmer SONOSPLAYER RINCON_000EFEFEFEF401400
@@ -209,15 +221,66 @@ sub SONOSPLAYER_Define ($$) {
# check syntax
return "SONOSPLAYER: Wrong syntax, must be define SONOSPLAYER " if(int(@a) < 3);
+ $hash->{NOTIFYDEV} = $name;
+ $hash->{helper}->{simulateCurrentTrackPosition} = 0;
+
readingsSingleUpdate($hash, "state", 'init', 1);
readingsSingleUpdate($hash, "presence", 'disappeared', 0); # Grund-Initialisierung, falls der Player sich nicht zurückmelden sollte...
+ # RoomDarstellung für alle Player festlegen
+ $modules{$hash->{TYPE}}->{FW_addDetailToSummary} = (AttrVal(SONOS_getSonosPlayerByName()->{NAME}, 'deviceRoomView', 'Both') =~ m/(Both)/i);
+
$hash->{UDN} = $udn;
readingsSingleUpdate($hash, "state", 'initialized', 1);
return undef;
}
+########################################################################################
+#
+# SONOSPLAYER_Detail - Returns the Detailview
+#
+########################################################################################
+sub SONOSPLAYER_Detail($$$;$) {
+ my ($FW_wname, $d, $room, $withRC) = @_;
+ $withRC = 1 if (!defined($withRC));
+
+ my $hash = $defs{$d};
+
+ return '' if (!ReadingsVal($d, 'IsMaster', 0) || (ReadingsVal($d, 'playerType', '') eq 'ZB100'));
+
+ # Open incl. Inform-Div
+ my $html .= '';
+
+ # Cover-/TitleView
+ $html .= '
';
+ $html .= SONOS_getCoverTitleRG($d);
+ $html .= '
';
+
+ # Close Inform-Div
+ $html .= '
';
+
+ # Control-Buttons
+ if (!AttrVal($d, 'suppressControlButtons', 0) && ($withRC)) {
+ $html.= '';
+ $html .= '
';
+ $html .= ''.FW_makeImage('rc_PREVIOUS.svg', 'Previous', 'rc-button').' |
+ '.FW_makeImage('rc_PLAY.svg', 'Play', 'rc-button').' |
+ '.FW_makeImage('rc_PAUSE.svg', 'Pause', 'rc-button').' |
+ '.FW_makeImage('rc_NEXT.svg', 'Next', 'rc-button').' |
+ '.FW_makeImage('rc_VOLDOWN.svg', 'VolDown', 'rc-button').' |
+ '.FW_makeImage('rc_MUTE.svg', 'Mute', 'rc-button').' |
+ '.FW_makeImage('rc_VOLUP.svg', 'VolUp', 'rc-button').' | ';
+ $html .= '
';
+ $html .= '
';
+ }
+
+ # Close
+ $html .= '';
+
+ return $html;
+}
+
########################################################################################
#
# SONOSPLAYER_Attribute - Implements AttrFn function
@@ -262,7 +325,6 @@ sub SONOSPLAYER_State($$$$) {
my ($hash, $time, $name, $value) = @_;
# Die folgenden Readings müssen immer neu initialisiert verwendet werden, und dürfen nicht aus dem Statefile verwendet werden
- #return 'Reading '.$hash->{NAME}."->$name must not be used out of statefile. This is not an error! This happens due to restrictions of Fhem." if ($name eq 'presence') || ($name eq 'LastActionResult') || ($name eq 'AlarmList') || ($name eq 'AlarmListIDs') || ($name eq 'AlarmListVersion');
if (($name eq 'presence') || ($name eq 'LastActionResult') || ($name eq 'AlarmList') || ($name eq 'AlarmListIDs') || ($name eq 'AlarmListVersion')) {
SONOSPLAYER_Log undef, 4, 'StateFn-Call. Ignore the following Reading: '.$hash->{NAME}.'->'.$name.'('.(defined($value) ? $value : '').')';
@@ -280,24 +342,62 @@ sub SONOSPLAYER_State($$$$) {
# SONOSPLAYER_Notify - Implements NotifyFn function
#
########################################################################################
-sub SONOSPLAYER_Notify() {
+sub SONOSPLAYER_Notify($$) {
my ($hash, $notifyhash) = @_;
- return undef;
+ my $events = deviceEvents($notifyhash, 1);
+ return if(!$events);
- # Das folgende habe ich erstmal wieder entfernt, da man ja öfter im laufenden Betrieb die Einstellungen speichert, und den Sonos-Komponenten dann immer wichtige Informationen für den Betrieb fehlen (nicht jedes Save wird vor dem Neustart von Fhem ausgeführt)
- #if (($notifyhash->{NAME} eq 'global') && (($notifyhash->{CHANGED}[0] eq 'SAVE') || ($notifyhash->{CHANGED}[0] eq 'SHUTDOWN'))) {
- # SONOSPLAYER_Log undef, 3, $hash->{NAME}.' has detected a global:'.$notifyhash->{CHANGED}[0].'-Event. Clear out some readings before...';
- #
- # # Einige Readings niemals speichern
- # delete($defs{$hash->{NAME}}{READINGS}{presence});
- # delete($defs{$hash->{NAME}}{READINGS}{LastActionResult});
- # delete($defs{$hash->{NAME}}{READINGS}{AlarmList});
- # delete($defs{$hash->{NAME}}{READINGS}{AlarmListIDs});
- # delete($defs{$hash->{NAME}}{READINGS}{AlarmListVersion});
- #}
- #
- #return undef;
+ foreach my $event (@{$events}) {
+ next if(!defined($event));
+
+ if ($event =~ m/transportState: (.+)/i) {
+ SONOSPLAYER_Log $hash->{NAME}, 5, 'Notify: '.$event;
+ if ($1 eq 'PLAYING') {
+ $hash->{helper}->{simulateCurrentTrackPosition} = AttrVal($hash->{NAME}, 'simulateCurrentTrackPosition', 0);
+
+ # Wiederholungskette für die Aktualisierung sofort anstarten...
+ InternalTimer(gettimeofday(), 'SONOSPLAYER_SimulateCurrentTrackPosition', $hash, 0);
+ } else {
+ $hash->{helper}->{simulateCurrentTrackPosition} = 0;
+
+ # Einmal noch etwas später aktualisieren...
+ InternalTimer(gettimeofday() + 0.1, 'SONOSPLAYER_SimulateCurrentTrackPosition', $hash, 0);
+ }
+ }
+ }
+
+ return undef;
+}
+
+########################################################################################
+#
+# SONOSPLAYER_SimulateCurrentTrackPosition - Implements the Simulation for the currentTrackPosition
+#
+########################################################################################
+sub SONOSPLAYER_SimulateCurrentTrackPosition() {
+ my ($hash) = @_;
+
+ return undef if (AttrVal($hash->{NAME}, 'disable', 0));
+
+ readingsBeginUpdate($hash);
+
+ my $trackDurationSec = SONOS_GetTimeSeconds(ReadingsVal($hash->{NAME}, 'currentTrackDuration', 0)) - 1;
+ my $trackPositionSec = time - SONOS_GetTimeFromString(ReadingsTimestamp($hash->{NAME}, 'currentTrackPositionSec', 0)) + ReadingsVal($hash->{NAME}, 'currentTrackPositionSec', 0);
+ readingsBulkUpdate($hash, 'currentTrackPositionSimulated', SONOS_ConvertSecondsToTime($trackPositionSec));
+ readingsBulkUpdate($hash, 'currentTrackPositionSimulatedSec', $trackPositionSec);
+
+ if ($trackDurationSec) {
+ readingsBulkUpdateIfChanged($hash, 'currentTrackPositionSimulatedPercent', sprintf(AttrVal($hash->{NAME}, 'simulateCurrentTrackPositionPercentFormat', '%.1f'), 100 * $trackPositionSec / $trackDurationSec));
+ } else {
+ readingsBulkUpdateIfChanged($hash, 'currentTrackPositionSimulatedPercent', sprintf(AttrVal($hash->{NAME}, 'simulateCurrentTrackPositionPercentFormat', '%.1f'), 0.0));
+ }
+
+ readingsEndUpdate($hash, 1);
+
+ if ($hash->{helper}->{simulateCurrentTrackPosition}) {
+ InternalTimer(gettimeofday() + $hash->{helper}->{simulateCurrentTrackPosition}, 'SONOSPLAYER_SimulateCurrentTrackPosition', $hash, 0);
+ }
}
########################################################################################
@@ -786,7 +886,19 @@ sub SONOSPLAYER_Set($@) {
$hash = SONOSPLAYER_GetRealTargetPlayerHash($hash);
$udn = $hash->{UDN};
- SONOS_DoWork($udn, 'playURITemp', $value, $value2);
+ SONOS_DoWork($udn, 'playURITemp', $value, $value2);
+ } elsif ($key =~ m/(start|load)handle/i) {
+ $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash);
+ $udn = $hash->{UDN};
+
+ # Hier die komplette restliche Zeile in den Text-Parameter packen, da damit auch Leerzeichen möglich sind
+ my $text = '';
+ for(my $i = 2; $i < @a; $i++) {
+ $text .= ' ' if ($i > 2);
+ $text .= $a[$i];
+ }
+
+ SONOS_DoWork($udn, 'startHandle', $text, (lc($key) eq 'loadhandle'));
} elsif (lc($key) eq 'adduritoqueue') {
$hash = SONOSPLAYER_GetRealTargetPlayerHash($hash);
$udn = $hash->{UDN};
@@ -1453,6 +1565,8 @@ sub SONOSPLAYER_Log($$$) {
One of (0,1). Enables a slider for volumecontrol in detail view.
getAlarms <int>
One of (0..1). Initializes a callback-method for Alarms. This included the information of the DailyIndexRefreshTime.
+suppressControlButtons <int>
+
One of (0,1). Enables the control-section shown under the Cover-/Titleview.
volumeStep <int>
One of (0..100). Defines the stepwidth for subsequent calls of VolumeU
and VolumeD
.
@@ -1467,6 +1581,10 @@ sub SONOSPLAYER_Log($$$) {
Generates the reading 'InfoSummarize4' with the given format. More Information on this in the examples-section.
getTitleInfoFromMaster <int>
One of (0, 1). Gets the current Playing-Informations from the Masterplayer (if one is present).
+simulateCurrentTrackPosition <int>
+
One of (0,1,2,3,4,5,6,7,8,9,10,15,20,25,30,45,60). Starts an internal Timer which refreshs the current trackposition into the Readings currentTrackPositionSimulated
and currentTrackPositionSimulatedSec
. At the same time the Reading currentTrackPositionSimulatedPercent
(between 0.0 and 100.0) will also be refreshed.
+simulateCurrentTrackPositionPercentFormat <Format>
+
Defines the format of the percentformat in the Reading currentTrackPositionSimulatedPercent
.
stateVariable <string>
One of (TransportState,NumberOfTracks,Track,TrackURI,TrackDuration,Title,Artist,Album,OriginalTrackNumber,AlbumArtist,
Sender,SenderCurrent,SenderInfo,StreamAudio,NormalAudio,AlbumArtURI,nextTrackDuration,nextTrackURI,nextAlbumArtURI,
nextTitle,nextArtist,nextAlbum,nextAlbumArtist,nextOriginalTrackNumber,Volume,Mute,Shuffle,Repeat,RepeatOne,CrossfadeMode,Balance,
HeadphoneConnected,SleepTimer,Presence,RoomName,SaveRoomName,PlayerType,Location,SoftwareRevision,SerialNum,InfoSummarize1,
InfoSummarize2,InfoSummarize3,InfoSummarize4). Defines, which variable has to be copied to the content of the state-variable.
@@ -1828,6 +1946,9 @@ Here an event is defined, where in time of 2 seconds the Mute-Button has to be p
One of (0,1). Aktiviert einen Slider für die Lautstärkekontrolle in der Detailansicht.
getAlarms <int>
One of (0..1). Richtet eine Callback-Methode für Alarme ein. Damit wird auch die DailyIndexRefreshTime automatisch aktualisiert.
+suppressControlButtons <int>
+
One of (0,1). Gibt an, ob die Steuerbuttons unter der Cover-/Titelanzeige angezeigt werden sollen (=1) oder nicht (=0).
+
volumeStep <int>
One of (0..100). Definiert die Schrittweite für die Aufrufe von VolumeU
und VolumeD
.
@@ -1841,7 +1962,11 @@ Here an event is defined, where in time of 2 seconds the Mute-Button has to be p
generateInfoSummarize4 <string>
Erzeugt das Reading 'InfoSummarize4' mit dem angegebenen Format. Mehr Informationen dazu im Bereich Beispiele.
getTitleInfoFromMaster <int>
-
Eins aus (0, 1). Bringt das Device dazu, seine aktuellen Abspielinformationen vom aktuellen Gruppenmaster zu holen, wenn es einen solchen gibt.
+
Eins aus (0,1,2,3,4,5,6,7,8,9,10,15,20,25,30,45,60). Bringt das Device dazu, seine aktuellen Abspielinformationen vom aktuellen Gruppenmaster zu holen, wenn es einen solchen gibt.
+simulateCurrentTrackPosition <int>
+
Eins aus (0, 1). Bringt das Device dazu, seine aktuelle Abspielposition simuliert weiterlaufen zu lassen. Dazu werden die Readings currentTrackPositionSimulated
und currentTrackPositionSimulatedSec
gesetzt. Gleichzeitig wird auch das Reading currentTrackPositionSimulatedPercent
(zwischen 0.0 und 100.0) gesetzt.
+simulateCurrentTrackPositionPercentFormat <Format>
+
Definiert das Format für die sprintf-Prozentausgabe im Reading currentTrackPositionSimulatedPercent
.
stateVariable <string>
One of (TransportState,NumberOfTracks,Track,TrackURI,TrackDuration,Title,Artist,Album,OriginalTrackNumber,AlbumArtist,
Sender,SenderCurrent,SenderInfo,StreamAudio,NormalAudio,AlbumArtURI,nextTrackDuration,nextTrackURI,nextAlbumArtURI,
nextTitle,nextArtist,nextAlbum,nextAlbumArtist,nextOriginalTrackNumber,Volume,Mute,Shuffle,Repeat,RepeatOne,CrossfadeMode,Balance,
HeadphoneConnected,SleepTimer,Presence,RoomName,SaveRoomName,PlayerType,Location,SoftwareRevision,SerialNum,InfoSummarize1,I
nfoSummarize2,InfoSummarize3,InfoSummarize4). Gibt an, welche Variable in das Reading state
kopiert werden soll.
diff --git a/fhem/FHEM/lib/UPnP/ControlPoint.pm b/fhem/FHEM/lib/UPnP/ControlPoint.pm
index f2e35b2c7..a43c768e7 100644
--- a/fhem/FHEM/lib/UPnP/ControlPoint.pm
+++ b/fhem/FHEM/lib/UPnP/ControlPoint.pm
@@ -78,11 +78,25 @@ sub new {
$self->{_subscriptionPort} = $self->{_subscriptionSocket}->sockport();;
# Create the socket on which we'll listen for SSDP Notifications.
- $self->{_ssdpMulticastSocket} = IO::Socket::INET->new(
- Proto => 'udp',
- Reuse => 1,
- LocalPort => SSDP_PORT) ||
- croak("Error creating SSDP multicast listen socket: $!\n");
+ # First try with ReusePort...
+ eval {
+ $self->{_ssdpMulticastSocket} = IO::Socket::INET->new(
+ Proto => 'udp',
+ Reuse => 1,
+ ReusePort => 1,
+ LocalPort => SSDP_PORT) ||
+ carp("Error creating SSDP multicast listen socket: $!\n");
+ };
+ if ($@ =~ /Your vendor has not defined Socket macro SO_REUSEPORT/i) {
+ $self->{_ssdpMulticastSocket} = IO::Socket::INET->new(
+ Proto => 'udp',
+ Reuse => 1,
+ LocalPort => SSDP_PORT) ||
+ carp("Error creating SSDP multicast listen socket: $!\n");
+ } else {
+ # Weiterwerfen...
+ carp($@);
+ }
my $ip_mreq = inet_aton(SSDP_IP) . INADDR_ANY;
setsockopt($self->{_ssdpMulticastSocket},
IP_LEVEL,
diff --git a/fhem/FHEM/lib/UPnP/sonos_bibliothek_quadratic.jpg b/fhem/FHEM/lib/UPnP/sonos_bibliothek_quadratic.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..becde3a65b6a012ed3004579784a7219e08ea917
GIT binary patch
literal 2808
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
T^~g~@00000NkvXXu0mjf)4NaS
literal 0
HcmV?d00001
diff --git a/fhem/FHEM/lib/UPnP/sonos_dock_quadratic.jpg b/fhem/FHEM/lib/UPnP/sonos_dock_quadratic.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..f0eff0d91ac275088bdbdc3b0a6d7fbfbc2c13f9
GIT binary patch
literal 5416
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
Vx6Jy{U3>Xt7E9EBo;Vr9{{{DY0KWhL
literal 0
HcmV?d00001
diff --git a/fhem/FHEM/lib/UPnP/sonos_dock_round.png b/fhem/FHEM/lib/UPnP/sonos_dock_round.png
new file mode 100644
index 0000000000000000000000000000000000000000..a5a2e40eab949ba3887a7da59269900eb538c6aa
GIT binary patch
literal 5738
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
QREYiF_joo@5e2{e2ZDHG2LJ#7
literal 0
HcmV?d00001
diff --git a/fhem/FHEM/lib/UPnP/sonos_linein_round.png b/fhem/FHEM/lib/UPnP/sonos_linein_round.png
new file mode 100644
index 0000000000000000000000000000000000000000..a723d48173a6f85f0014770b069821fe6b569c31
GIT binary patch
literal 1988
zcmeHH`&ZHj9R2#J2}aXmlpZP-n3iTwSGL@OK>1pdqAN`-m{{6sD!O&7bn*~Ud?bs~
zEIcSK(1{R7Gf#@4ORya^&9wPSMboh~#C)WV;os5jIrnqV=kwz|_j9v?NdYjZGZX*-
z7)ZpEElOXbosDG%9F4hP5vwF}01lA1Bd08j?L}-5766p__A9h?0AK?Ip9~{d?kOoL
z*REZ=e*OB58#hu@Q`6JanM@{bUGB91bTVBZJH3W@cvcc)YBvtnBP;KA)eHlarg9
zo0pfDpPw%f2!ujmVPRoWQBiSmu}CB;D=U*oBvPqVCX-cER8&@0R#jEW`z4YK20f
zR4P>}RZUGzU0q#$eSJeiLt|s3TCHwsYSL&lEiEmrt*vcsZSC#tU0q$>-Q7JsJ+EH9
z>h0}){rYu(fB(S1z~JEE(9qEE@bJjU$mr$B)a)%PT7@tE;PKv)SUn|M+Abu**Xou>1*-
zOeTd|!tf~r2wnILV+jHzff$hl02`avXw@1oU8@zRWI}jyX#9obt29Oo5KN2vG1;3$
z2y^-QfVYpg51763#?mNA5RVOuJwA9h?##=*aIO=@pAejIoStYy!NNl*4wQsw2j;)#
zlKo%lnmZ4I?pNZMpT6xL&<`#6CP9M^XBMO#jj~5X^KKn9p(DL4&9;)J}{CO##n2Vv9a0=`Y|>}jJ0+1iGcLu94fTLJ~+GY)@{sX
z2rj{2!}Y*F@}xy<)f6Z^^1%2I4cTULaUSh;P-Au%U*;h&@Z69m*c-ko<9h!_hdexu&NB4o%M44#2#luXQWHeNZ^I$Q@*0!Xh|GW
zH|LrP&YCJ|C4aSA`60emaj@qYF&!Ib{2nJU%iH_75hlkEsC##w_^1q9WH5U_)jwnc
zBl6Vtj?j&DIRoNsJzGR6Z)0?r>AQ%}@_OD(`v&rjLHF;LvGk4G?}^(EJksxPJ?C1h
z7c!6{@uue6PtFLgKz%Rkb%*gPoYD7IS1*|J&?_+cLiA&Q
z40rcqF(zKxqTQw*t{L|h!e!_m_NiwZz(A+jL)wre=AtS6tnJ&oN6O9@fDcm)UY;Kk
zXEe|K1?YhD=NP#Ujg6)DFE;hhT7$f&G9-UtPgOZ;bR|rv_v$XunaHP0XZ-}Ei)oqo
z&%YPUDWWL-eG|?xNus>)^_%hu&~3w|l6`zIKD6s*#{1163Q-z)`ykczu&3iAhvKeU
zd>>Bg=IL1DAXB1Q2y0j^{!biIkH#S^$vA=c!ow2K)(93U5mU3fh5nv+>8zb@^w*a!
z9H?V=!fN-5P%g6HXfItoWK>!w#%pIKO&d@k$>Cak`-cFIgNl8$!MX(4m34R`T<>ei
iFfog3X4@b#yJyTc5a0WAKMm{(UaMM=K*Gy$RMvl8?@Y!3
literal 0
HcmV?d00001
diff --git a/fhem/FHEM/lib/UPnP/sonos_playbar_quadratic.jpg b/fhem/FHEM/lib/UPnP/sonos_playbar_quadratic.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..306467118c3940310d327e8999a04cfac1b5be6d
GIT binary patch
literal 5306
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
literal 0
HcmV?d00001
diff --git a/fhem/FHEM/lib/UPnP/sonos_tunein_round.png b/fhem/FHEM/lib/UPnP/sonos_tunein_round.png
new file mode 100644
index 0000000000000000000000000000000000000000..ec4a7064b97b89822084b596e4e5f662ce2fe0da
GIT binary patch
literal 1979
zcmeHH{Xf$Q9RIF`&Qpjw^2)=~BXdo9y2Qk7bG5^$QSBJTr95pOuDTAZj^&(6S+CNZ
zE_t4XW)s^y&Wy<9;kArxqKM3Fwy~Rkqr1=R{r-GjpZAaN&qJ0!8ew8+V+a6%2@2^G
zsH^^K+yK^P=IeD-T>%jS5#B&mkL|RsSsw*I2?qfBUq+g6Jpcd${wIQbb^3wY=LhRb
z4%L+$d0BdN4Q0m|<(~EBUiIbh`ikSsN*`wB53DL=1I@3Y`eY+Lps6O9U3-dCcbfAO
z%Y7NbWrQ{}a4q$?7AC%p717oZ)!ulqqv;~A>2fDKu9K6<=O*%-le$`x1ud!Ft=C_7
zWc2b#eVv*8UF3cNSt!UB3jP#!-y7`B8|=*=>Z1(3DHsv{EqeR(-C&7$xKun+Jua$#
z|E}i!Xzj#k-GrDS5woP@4U^;S4-?!E67H0wc}m*;agz7(L)Xky_ssOb{OsWT=aDaS
zq6Ho1M`d5ebXZ)F$`_}WTFQ7y|DLfmr@7GCHmvQ_1FUSBrdaAhn)2=?T+Ss)P1a!+H-_^&vE!%EBvJ*vi679+y%u|k4)
z$Y^JcN`r%K#st~aXZj%WDL?M^OEw)XlAl
z(}gnQtQ_^jEF_>e#XY|s!=qC*KX2Dtg
z-iJf0<1xS6xK_dFd3r>C3YAvXhlI^yHKccpp
zKIk|!mN$9OQ@_P!M<}Yg^m?K9^NksdPUSL0L>RCSv@q7@w-W4@73GLmGsKDmj#7{Z
zO~%=FNHRi;il@y8o+Qo=x($^aq16Y;jaitU}+LldbrOogJgm>YUA9q`92iY8v<)OjSq`CsWgEcyvjpL(&-jHJ