diff --git a/fhem/FHEM/00_SONOS.pm b/fhem/FHEM/00_SONOS.pm index 718ae0e8b..2f10a4c97 100755 --- a/fhem/FHEM/00_SONOS.pm +++ b/fhem/FHEM/00_SONOS.pm @@ -48,6 +48,12 @@ # # SVN-History: # .04.2015 +# Neues Feature 'ExportSonosBibliothek': Hiermit kann eine Datei mit der textuellen Darstellung eines Struktur- und Titelhashs erzeugt werden, das die komplette Navigationsstruktur aus der Sonos-Bibliothek abbildet. Richtwerte bei ca. 20.000 Titeln auf einem Windows-Server mit Intel Core i5 mit 2.8GHz: Laufzeit: ca. 8Min, Arbeitsspeicher: ca. 1GB, Resultierende Datei: ca. 52MB +# Neues Feature 'DeletePlaylist': Hiermit kann eine Playlist gelöscht werden. Genauso wie bei LoadPlaylist kann man hier URL-Encoded arbeiten, oder einen regulären Ausdruck verwenden +# Neues Feature 'SnoozeAlarm': Hiermit kann ein gerade abspielender Alarm für die übergebene Zeit unterbrochen werden +# Neues Feature bei 'LoadPlaylist': Man kann nun einen Devicenamen angeben, dann wird dessen aktuelle Abspielliste kopiert +# Bei der Titelanzeige wird der Numerische Vergleichsfehler abgefangen, der auftritt, wenn der Player keinerlei aktuelle Abspielinformationen hat +# 03.04.2015 # IsAlive-Check Anpassungen: Bei einer Antwort wird nun nicht mehr geprüft, ob diese von derselben Netzwerkschnittstelle stammt, an die der Ping gesendet wurde. Damit werden Player besser erkannt, die sowohl am Funknetz als auch am LAN angeschlossen sind. # IsAlive-Check Anpassungen: Bei der Option 'tcp' wird nun versucht auf den Standard-Webport des Players zu verbinden (1400) # Callback-Aufrufmethoden: Wenn ein Player eine Nachricht an Fhem sendet und dieser Player in Fhem als 'disappeared' geführt wird, dann wird der Discovery-Process neu angestartet, um diesen Player wieder sauber zu erkennen @@ -391,6 +397,7 @@ sub SONOS_Log($$$); sub SONOS_StartClientProcessIfNeccessary($); sub SONOS_Client_Notifier($); sub SONOS_Client_ConsumeMessage($$); +sub SONOS_RecursiveStructure($$$$); sub SONOS_RCLayout(); @@ -608,12 +615,32 @@ sub SONOS_RCLayoutSVG2() { return @rows; } +######################################################################################## +# +# SONOS_LoadExportedSonosBibliothek - Sets the internal Value with the given Name in the given fhem-device with the loaded file given with filename +# +######################################################################################## +sub SONOS_LoadExportedSonosBibliothek($$$) { + my ($fileName, $deviceName, $internalName) = @_; + + my $fileInhalt = ''; + + open FILE, '<'.$fileName; + binmode(FILE, ':encoding(utf-8)'); + while () { + $fileInhalt .= $_; + } + close FILE; + + $defs{$deviceName}->{$internalName} = eval($fileInhalt); +} + ######################################################################################## # # SONOS_getCoverTitleRG - Returns the Cover- and Title-Readings for use in a ReadingsGroup # ######################################################################################## -sub SONOS_getCoverTitleRG($;$$) { +sub SONOS_getCoverTitleRG($;$$) { my ($device, $width, $space) = @_; $width = 500 if (!defined($width)); @@ -744,7 +771,9 @@ sub SONOS_getTitleRG($;$) { # 55 # Läuft Radio oder ein "normaler" Titel - if (ReadingsVal($device, 'currentNormalAudio', 1) == 1) { + my $currentNormalAudio = ReadingsVal($device, 'currentNormalAudio', 1); + $currentNormalAudio = 1 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: %s
Interpret: %s
Album: %s'.($showNext ? '
Nächste Wiedergabe (%s):
Titel: %s
Interpret: %s
Album: %s
' : ''), $transportState, @@ -2672,6 +2701,44 @@ sub SONOS_Discover() { SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': "'.join('","', sort values %resultHash).'"'); $Data::Dumper::Indent = 2; } + } elsif ($workType eq 'exportSonosBibliothek') { + my $filename = $params[0]; + + # Anfragen durchführen... + if (SONOS_CheckProxyObject($udn, $SONOS_ContentDirectoryControlProxy{$udn})) { + my $exports = {'Structure' => {}, 'Titles' => {}}; + + SONOS_Log undef, 3, 'ExportSonosBibliothek-Start'; + my $startTime = gettimeofday(); + SONOS_RecursiveStructure($udn, 'A:', $exports->{Structure}, $exports->{Titles}); + SONOS_Log undef, 3, 'ExportSonosBibliothek-End. Runtime (in seconds): '.int(gettimeofday() - $startTime); + + my $countTitles = scalar(keys %{$exports->{Titles}}); + + # In Datei wegschreiben + $Data::Dumper::Indent = 0; + eval { + #$Data::Dumper::Indent = 2; + #open FILE, '>M:/Hausautomation/fhem-dev/fhem/structure.txt'; + #print FILE Dumper($exports->{Structure}); + #close FILE; + #$Data::Dumper::Indent = 0; + + open FILE, '>'.$filename; + binmode(FILE, ':encoding(utf-8)'); + print FILE Dumper($exports); + close FILE; + }; + if ($@) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Error during filewriting: '.$@); + return; + } + $Data::Dumper::Indent = 2; + + $exports = undef; + + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Successfully written to file "'.$filename.'", Titles: '.$countTitles.', Duration: '.int(gettimeofday() - $startTime).'s'); + } } elsif ($workType eq 'loadSearchlist') { # Category holen my $regSearch = ($params[0] =~ m/^ *\/(.*)\/ *$/); @@ -3150,35 +3217,34 @@ sub SONOS_Discover() { } close FILE; - my $sliceSize = 16; - my $result; - my $count = 0; + # Elemente an die Queue anhängen + $answer .= SONOS_AddMultipleURIsToQueue($udn, \@URIs, \@Metas, $currentInsertPos); + } elsif ($playlistName =~ /^:device:(.*)/) { + my $sourceUDN = $1; + my @URIs = (); + my @Metas = (); - SONOS_Log $udn, 5, "Start-Adding: Count ".scalar(@URIs)." / $sliceSize"; + # Titel laden + my $playlistData; + my $startIndex = 0; - for my $i (0..int(scalar(@URIs) / $sliceSize)) { # Da hier Nullbasiert vorgegangen wird, brauchen wir die letzte Runde nicht noch hinzuaddieren - my $startIndex = $i * $sliceSize; - my $endIndex = $startIndex + $sliceSize - 1; - $endIndex = SONOS_Min(scalar(@URIs) - 1, $endIndex); + do { + $playlistData = $SONOS_ContentDirectoryControlProxy{$sourceUDN}->Browse('Q:0', 'BrowseDirectChildren', '', $startIndex, 0, ''); + my $tmp = decode('UTF-8', $playlistData->getValue('Result')); - SONOS_Log $udn, 5, "Add($i) von $startIndex bis $endIndex (".($endIndex - $startIndex + 1)." Elemente)"; - SONOS_Log $udn, 5, "Upload($currentInsertPos)-URI: ".join(' ', @URIs[$startIndex..$endIndex]); - SONOS_Log $udn, 5, "Upload($currentInsertPos)-Meta: ".join(' ', @Metas[$startIndex..$endIndex]); - - $result = $SONOS_AVTransportControlProxy{$udn}->AddMultipleURIsToQueue(0, 0, $endIndex - $startIndex + 1, join(' ', @URIs[$startIndex..$endIndex]), join(' ', @Metas[$startIndex..$endIndex]), '', '', $currentInsertPos, 0); - if (!$result->isSuccessful()) { - $answer .= 'Adding-Error: '.SONOS_UPnPAnswerMessage($result).' '; + while ($tmp =~ m/.*?(.*?)<\/res>.*?<\/item>/ig) { + my ($res, $meta) = SONOS_CreateURIMeta(decode_entities($1)); + next if (!defined($res)); + + push(@URIs, $res); + push(@Metas, $meta); } - $currentInsertPos += $endIndex - $startIndex + 1; - $count = $endIndex + 1; - } + $startIndex += $playlistData->getValue('NumberReturned'); + } while ($startIndex < $playlistData->getValue('TotalMatches')); - if ($result->isSuccessful()) { - $answer .= 'Added '.$count.' entries from file "'.$1.'". There are now '.$result->getValue('NewQueueLength').' entries in Queue. '; - } else { - $answer .= 'Adding: '.SONOS_UPnPAnswerMessage($result).' '; - } + # Elemente an die Queue anhängen + $answer .= SONOS_AddMultipleURIsToQueue($udn, \@URIs, \@Metas, $currentInsertPos); } else { my $browseResult = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('SQ:', 'BrowseDirectChildren', '', 0, 0, ''); my $tmp = $browseResult->getValue('Result'); @@ -3276,6 +3342,12 @@ sub SONOS_Discover() { SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_AnswerMessage(0)); } } + } elsif ($workType eq 'setSnoozeAlarm') { + my $time = $params[0]; + + if (SONOS_CheckProxyObject($udn, $SONOS_AVTransportControlProxy{$udn})) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': '.SONOS_UPnPAnswerMessage($SONOS_AVTransportControlProxy{$udn}->SnoozeAlarm(0, $time))); + } } elsif ($workType eq 'setDailyIndexRefreshTime') { my $time = $params[0]; @@ -3390,6 +3462,44 @@ sub SONOS_Discover() { } } } + } elsif ($workType eq 'deletePlaylist') { + my $regSearch = ($params[0] =~ m/^ *\/(.*)\/ *$/); + my $playlistName = $1 if ($regSearch); + $playlistName = uri_unescape($params[0]) if (!$regSearch); + + # RegEx prüfen... + if ($regSearch) { + eval { "" =~ m/$playlistName/ }; + if($@) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Bad RegExp "'.$playlistName.'": '.$@); + return; + } + } + + my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse('SQ:', 'BrowseDirectChildren', '', 0, 0, ''); + my $tmp = $result->getValue('Result'); + + my %resultHash; + while ($tmp =~ m/(.*?)<\/dc:title>.*?<\/container>/ig) { + my $name = $2; + $resultHash{$name} = $1; + + # Den ersten Match ermitteln, und sich den echten Namen für die Zukunft merken... + if ($regSearch) { + if ($name =~ m/$playlistName/) { + $playlistName = $name; + $regSearch = 0; + } + } + } + + # Wenn RegSearch gesetzt war, und nichts gefunden wurde... + if (!$resultHash{$playlistName} || $regSearch) { + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Playlist "'.$playlistName.'" not found. Choose one of: "'.join('","', sort keys %resultHash).'"'); + return; + } + + SONOS_MakeSigHandlerReturnValue($udn, 'LastActionResult', ucfirst($workType).': Playlist "'.$playlistName.'" deleted: '.SONOS_UPnPAnswerMessage($SONOS_ContentDirectoryControlProxy{$udn}->DestroyObject($resultHash{$playlistName}))); } elsif ($workType eq 'deleteProxyObjects') { # Wird vom Sonos-Device selber in IsAlive benötigt SONOS_DeleteProxyObjects($udn); @@ -3673,6 +3783,115 @@ sub SONOS_Discover() { return 1; } +######################################################################################## +# +# SONOS_RecursiveStructure - Retrieves the structure of the Sonos-Bibliothek +# +######################################################################################## +sub SONOS_RecursiveStructure($$$$) { + my ($udn, $search, $exportsStruct, $exportsTitles) = @_; + + my $startIndex = 0; + my $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($search, 'BrowseDirectChildren', '', $startIndex, 0, ''); + return if (!defined($result->getValue('NumberReturned'))); + $startIndex += $result->getValue('NumberReturned'); + my $tmp = decode('UTF-8', $result->getValue('Result')); + + # Alle Suchergebnisse vom Player abfragen... + while ($startIndex < $result->getValue('TotalMatches')) { + $result = $SONOS_ContentDirectoryControlProxy{$udn}->Browse($search, 'BrowseDirectChildren', '', $startIndex, 0, ''); + $tmp .= decode('UTF-8', $result->getValue('Result')); + + $startIndex += $result->getValue('NumberReturned'); + } + + # Struktur verarbeiten... + while ($tmp =~ m/(.*?)<\/container>/ig) { + my $id = $1; + my $item = $2; + + next if (SONOS_Trim($id) eq ''); # Wenn keine ID angegeben ist, dann überspringen + + $exportsStruct->{$id}->{ID} = $id; + $exportsStruct->{$id}->{Title} = $1 if ($item =~ m/(.*?)<\/dc:title>/i); + $exportsStruct->{$id}->{Artist} = $1 if ($item =~ m/(.*?)<\/dc:creator>/i); + $exportsStruct->{$id}->{Cover} = SONOS_MakeCoverURL($udn, $1) if ($item =~ m/(.*?)<\/upnp:albumArtURI>/i); + $exportsStruct->{$id}->{Type} = 'Container'; + $exportsStruct->{$id}->{Children} = {}; + + # Wenn hier eine Titel-ID gesucht werden soll, die es bereits lokal gibt, dann nicht mehr anfragen... + if (!$exportsTitles->{$id}) { + SONOS_RecursiveStructure($udn, $id, $exportsStruct->{$id}->{Children}, $exportsTitles); + } + } + + # Titel verarbeiten... + while ($tmp =~ m/(.*?)<\/item>/ig) { + my $id = $1; + my $item = $2; + + next if (SONOS_Trim($id) eq ''); # Wenn keine ID angegeben ist, dann überspringen + + # Titel merken... + $exportsTitles->{$id}->{ID} = $id; + $exportsTitles->{$id}->{TrackURI} = SONOS_GetURIFromQueueValue($1) if ($item =~ m/(.*?)<\/res>/i); + $exportsTitles->{$id}->{Title} = $1 if ($item =~ m/(.*?)<\/dc:title>/i); + $exportsTitles->{$id}->{Artist} = $1 if ($item =~ m/(.*?)<\/dc:creator>/i); + $exportsTitles->{$id}->{AlbumArtist} = $1 if ($item =~ m/(.*?)<\/r:albumArtist>/i); + $exportsTitles->{$id}->{Album} = $1 if ($item =~ m/(.*?)<\/upnp:album>/i); + $exportsTitles->{$id}->{Cover} = SONOS_MakeCoverURL($udn, $1) if ($item =~ m/(.*?)<\/upnp:albumArtURI>/i); + $exportsTitles->{$id}->{OriginalTrackNumber} = $1 if ($item =~ m/(.*?)<\/upnp:originalTrackNumber>/i); + + # Verweis in der Struktur merken... + $exportsStruct->{$id}->{ID} = $id; + $exportsStruct->{$id}->{Type} = 'Track'; + } +} + +######################################################################################## +# +# SONOS_AddMultipleURIsToQueue - Adds the given URIs to the current queue of the player +# +######################################################################################## +sub SONOS_AddMultipleURIsToQueue($$$$) { + my ($udn, $URIs, $Metas, $currentInsertPos) = @_; + my @URIs = @{$URIs}; + my @Metas = @{$Metas}; + + my $sliceSize = 16; + my $result; + my $count = 0; + my $answer = ''; + + SONOS_Log $udn, 5, "Start-Adding: Count ".scalar(@URIs)." / $sliceSize"; + + for my $i (0..int(scalar(@URIs) / $sliceSize)) { # Da hier Nullbasiert vorgegangen wird, brauchen wir die letzte Runde nicht noch hinzuaddieren + my $startIndex = $i * $sliceSize; + my $endIndex = $startIndex + $sliceSize - 1; + $endIndex = SONOS_Min(scalar(@URIs) - 1, $endIndex); + + SONOS_Log $udn, 5, "Add($i) von $startIndex bis $endIndex (".($endIndex - $startIndex + 1)." Elemente)"; + SONOS_Log $udn, 5, "Upload($currentInsertPos)-URI: ".join(' ', @URIs[$startIndex..$endIndex]); + SONOS_Log $udn, 5, "Upload($currentInsertPos)-Meta: ".join(' ', @Metas[$startIndex..$endIndex]); + + $result = $SONOS_AVTransportControlProxy{$udn}->AddMultipleURIsToQueue(0, 0, $endIndex - $startIndex + 1, join(' ', @URIs[$startIndex..$endIndex]), join(' ', @Metas[$startIndex..$endIndex]), '', '', $currentInsertPos, 0); + if (!$result->isSuccessful()) { + $answer .= 'Adding-Error: '.SONOS_UPnPAnswerMessage($result).' '; + } + + $currentInsertPos += $endIndex - $startIndex + 1; + $count = $endIndex + 1; + } + + if ($result->isSuccessful()) { + $answer .= 'Added '.$count.' entries from file "'.$1.'". There are now '.$result->getValue('NewQueueLength').' entries in Queue. '; + } else { + $answer .= 'Adding: '.SONOS_UPnPAnswerMessage($result).' '; + } + + return $answer; +} + ######################################################################################## # # SONOS_Fisher_Yates_Shuffle - Shuffles the given array @@ -5036,7 +5255,7 @@ sub SONOS_Discover_Callback($$$) { if ($transportService) { $SONOS_TransportSubscriptions{$udn} = $transportService->subscribe(\&SONOS_ServiceCallback); if (defined($SONOS_TransportSubscriptions{$udn})) { - SONOS_Log undef, 2, 'Service-subscribing successful with SID="'.$SONOS_TransportSubscriptions{$udn}->SID; + SONOS_Log undef, 2, 'Service-subscribing successful with SID='.$SONOS_TransportSubscriptions{$udn}->SID; } else { SONOS_Log undef, 1, 'Service-subscribing NOT successful'; } @@ -5051,7 +5270,7 @@ sub SONOS_Discover_Callback($$$) { $SONOS_RenderingSubscriptions{$udn} = $renderingService->subscribe(\&SONOS_RenderingCallback); $SONOS_ButtonPressQueue{$udn} = Thread::Queue->new(); if (defined($SONOS_RenderingSubscriptions{$udn})) { - SONOS_Log undef, 2, 'Rendering-Service-subscribing successful with SID="'.$SONOS_RenderingSubscriptions{$udn}->SID; + SONOS_Log undef, 2, 'Rendering-Service-subscribing successful with SID='.$SONOS_RenderingSubscriptions{$udn}->SID; } else { SONOS_Log undef, 1, 'Rendering-Service-subscribing NOT successful'; } @@ -5063,7 +5282,7 @@ sub SONOS_Discover_Callback($$$) { if ($groupRenderingService && (SONOS_Client_Data_Retreive($udn, 'attr', 'minVolume', -1) != -1 || SONOS_Client_Data_Retreive($udn, 'attr', 'maxVolume', -1) != -1 || SONOS_Client_Data_Retreive($udn, 'attr', 'minVolumeHeadphone', -1) != -1 || SONOS_Client_Data_Retreive($udn, 'attr', 'maxVolumeHeadphone', -1) != -1 )) { $SONOS_GroupRenderingSubscriptions{$udn} = $groupRenderingService->subscribe(\&SONOS_GroupRenderingCallback); if (defined($SONOS_GroupRenderingSubscriptions{$udn})) { - SONOS_Log undef, 2, 'GroupRendering-Service-subscribing successful with SID="'.$SONOS_GroupRenderingSubscriptions{$udn}->SID; + SONOS_Log undef, 2, 'GroupRendering-Service-subscribing successful with SID='.$SONOS_GroupRenderingSubscriptions{$udn}->SID; } else { SONOS_Log undef, 1, 'GroupRendering-Service-subscribing NOT successful'; } @@ -5075,7 +5294,7 @@ sub SONOS_Discover_Callback($$$) { if ($alarmService && (SONOS_Client_Data_Retreive($udn, 'attr', 'getAlarms', 0) != 0)) { $SONOS_AlarmSubscriptions{$udn} = $alarmService->subscribe(\&SONOS_AlarmCallback); if (defined($SONOS_AlarmSubscriptions{$udn})) { - SONOS_Log undef, 2, 'Alarm-Service-subscribing successful with SID="'.$SONOS_AlarmSubscriptions{$udn}->SID; + SONOS_Log undef, 2, 'Alarm-Service-subscribing successful with SID='.$SONOS_AlarmSubscriptions{$udn}->SID; } else { SONOS_Log undef, 1, 'Alarm-Service-subscribing NOT successful'; } @@ -5087,7 +5306,7 @@ sub SONOS_Discover_Callback($$$) { if ($zoneGroupTopologyService) { $SONOS_ZoneGroupTopologySubscriptions{$udn} = $zoneGroupTopologyService->subscribe(\&SONOS_ZoneGroupTopologyCallback); if (defined($SONOS_ZoneGroupTopologySubscriptions{$udn})) { - SONOS_Log undef, 2, 'ZoneGroupTopology-Service-subscribing successful with SID="'.$SONOS_ZoneGroupTopologySubscriptions{$udn}->SID; + SONOS_Log undef, 2, 'ZoneGroupTopology-Service-subscribing successful with SID='.$SONOS_ZoneGroupTopologySubscriptions{$udn}->SID; } else { SONOS_Log undef, 1, 'ZoneGroupTopology-Service-subscribing NOT successful'; } @@ -5099,7 +5318,7 @@ sub SONOS_Discover_Callback($$$) { if ($devicePropertiesService) { $SONOS_DevicePropertiesSubscriptions{$udn} = $devicePropertiesService->subscribe(\&SONOS_DevicePropertiesCallback); if (defined($SONOS_DevicePropertiesSubscriptions{$udn})) { - SONOS_Log undef, 2, 'DeviceProperties-Service-subscribing successful with SID="'.$SONOS_DevicePropertiesSubscriptions{$udn}->SID; + SONOS_Log undef, 2, 'DeviceProperties-Service-subscribing successful with SID='.$SONOS_DevicePropertiesSubscriptions{$udn}->SID; } else { SONOS_Log undef, 1, 'DeviceProperties-Service-subscribing NOT successful'; } @@ -5111,7 +5330,7 @@ sub SONOS_Discover_Callback($$$) { if ($audioInService) { $SONOS_AudioInSubscriptions{$udn} = $audioInService->subscribe(\&SONOS_AudioInCallback); if (defined($SONOS_AudioInSubscriptions{$udn})) { - SONOS_Log undef, 2, 'AudioIn-Service-subscribing successful with SID="'.$SONOS_AudioInSubscriptions{$udn}->SID; + SONOS_Log undef, 2, 'AudioIn-Service-subscribing successful with SID='.$SONOS_AudioInSubscriptions{$udn}->SID; } else { SONOS_Log undef, 1, 'AudioIn-Service-subscribing NOT successful'; } diff --git a/fhem/FHEM/21_SONOSPLAYER.pm b/fhem/FHEM/21_SONOSPLAYER.pm index fc68a8b6a..141547e23 100755 --- a/fhem/FHEM/21_SONOSPLAYER.pm +++ b/fhem/FHEM/21_SONOSPLAYER.pm @@ -120,6 +120,7 @@ my %sets = ( 'LoadPlaylist' => 'playlistname', 'StartPlaylist' => 'playlistname', 'SavePlaylist' => 'playlistname', + 'DeletePlaylist' => 'playlistname', 'CurrentPlaylist' => '', 'EmptyPlaylist' => '', 'StartFavourite' => 'favouritename', @@ -151,6 +152,7 @@ my %sets = ( 'Track' => 'tracknumber|Random', 'currentTrack' => 'tracknumber', 'Alarm' => 'create|update|delete ID valueHash', + 'SnoozeAlarm' => 'time', 'DailyIndexRefreshTime' => 'timestamp', 'SleepTimer' => 'time', 'AddMember' => 'member_devicename', @@ -166,7 +168,8 @@ my %sets = ( 'RoomIcon' => 'iconName', 'LoadSearchlist' => 'category categoryElem titleFilter/albumFilter/artistFilter maxElems', 'StartSearchlist' => 'category categoryElem titleFilter/albumFilter/artistFilter maxElems', - 'ResetAttributesToDefault' => 'deleteOtherAttributes' + 'ResetAttributesToDefault' => 'deleteOtherAttributes', + 'ExportSonosBibliothek' => 'filename' ); my @possibleRoomIcons = qw(bathroom library office foyer dining tvroom hallway garage garden guestroom den bedroom kitchen portable media family pool masterbedroom playroom patio living); @@ -584,11 +587,17 @@ sub SONOSPLAYER_Set($@) { } elsif (lc($key) eq 'loadplaylist') { $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); $udn = $hash->{UDN}; - + $value2 = 1 if (!defined($value2)); if ($value =~ m/^file:(.*)/) { SONOS_DoWork($udn, 'loadPlaylist', ':m3ufile:'.$1, $value2); + } elsif (defined($defs{$value})) { + my $dHash = SONOS_getDeviceDefHash($value); + SONOSPLAYER_Log undef, 3, 'Device: '.$dHash->{NAME}.' ~ '.$dHash->{UDN}; + if (defined($dHash)) { + SONOS_DoWork($udn, 'loadPlaylist', ':device:'.$dHash->{UDN}, $value2); + } } else { SONOS_DoWork($udn, 'loadPlaylist', $value, $value2); } @@ -618,6 +627,8 @@ sub SONOSPLAYER_Set($@) { } else { SONOS_DoWork($udn, 'savePlaylist', $value, ''); } + } elsif (lc($key) eq 'deleteplaylist') { + SONOS_DoWork($udn, 'deletePlaylist', $value); } elsif (lc($key) eq 'currentplaylist') { $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); $udn = $hash->{UDN}; @@ -634,6 +645,8 @@ sub SONOSPLAYER_Set($@) { SONOS_DoWork($udn, 'loadSearchlist', $value, $value2, $a[4], $a[5]); SONOS_DoWork($udn, 'play'); + } elsif (lc($key) eq 'exportsonosbibliothek') { + SONOS_DoWork($udn, 'exportSonosBibliothek', $value); } elsif (lc($key) eq 'playuri') { $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); $udn = $hash->{UDN}; @@ -702,6 +715,11 @@ sub SONOSPLAYER_Set($@) { } SONOS_DoWork($udn, 'setAlarm', $value, $value2, $text); + } elsif (lc($key) eq 'snoozealarm') { + $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); + $udn = $hash->{UDN}; + + SONOS_DoWork($udn, 'setSnoozeAlarm', $value); } elsif (lc($key) eq 'dailyindexrefreshtime') { SONOS_DoWork($udn, 'setDailyIndexRefreshTime', $value); } elsif (lc($key) eq 'sleeptimer') { @@ -985,6 +1003,9 @@ sub SONOSPLAYER_Log($$$) {
  • DailyIndexRefreshTime <time>
    Sets the current DailyIndexRefreshTime for the whole bunch of Zoneplayers.
  • +
  • +ExportSonosBibliothek <filename> +
    Exports a file with a textual representation of a structure- and titlehash of the complete Sonos-Bibliothek. Warning: Will use a large amount of CPU-Time and RAM!
  • Name <Zonename>
    Sets the Name for this Zone
  • @@ -1000,6 +1021,9 @@ sub SONOSPLAYER_Log($$$) {
  • RoomIcon <Iconname>
    Sets the Icon for this Zone
  • +
  • +SnoozeAlarm <Time> +
    Snoozes a currently playing alarm for the given time
  • Wifi <State>
    Sets the WiFi-State of the given Player. Can be 'off', 'persist-off' or 'on'.
  • @@ -1111,12 +1135,15 @@ sub SONOSPLAYER_Log($$$) {
  • CurrentPlaylist
    Sets the current playing to the current queue, but doesn't start playing (e.g. after hearing of a radiostream, where the current playlist still exists but is currently "not in use")
  • +
  • +DeletePlaylist +
    Deletes the Sonos-Playlist with the given name. According to the possibilities of the playlistname have a close look at LoadPlaylist.
  • EmptyPlaylist
    Clears the current queue
  • -LoadPlaylist <Playlistname> [EmptyQueueBeforeImport] -
    Loads the named playlist to the current playing queue. The parameter should be URL-encoded for proper naming of lists with special characters. The Playlistname can be a filename and then must be startet with 'file:' (e.g. 'file:c:/Test.m3u')
    If EmptyQueueBeforeImport is given and set to 1, the queue will be emptied before the import process. If not given, the parameter will be interpreted as 1.
    Additionally it's possible to use a regular expression as the name. The first hit will be used. The format is e.g. /hits.2014/.
  • +LoadPlaylist <Playlistname|Fhem-Devicename> [EmptyQueueBeforeImport] +
    Loads the named playlist to the current playing queue. The parameter should be URL-encoded for proper naming of lists with special characters. The Playlistnamen can be an Fhem-Devicename, then the current playlist of this referenced player will be copied. The Playlistname can also be a filename and then must be startet with 'file:' (e.g. 'file:c:/Test.m3u')
    If EmptyQueueBeforeImport is given and set to 1, the queue will be emptied before the import process. If not given, the parameter will be interpreted as 1.
    Additionally it's possible to use a regular expression as the name. The first hit will be used. The format is e.g. /hits.2014/.
  • LoadRadio <Radiostationname>
    Loads the named radiostation (favorite). The current queue will not be touched but deactivated. The parameter should be URL-encoded for proper naming of lists with special characters.
    Additionally it's possible to use a regular expression as the name. The first hit will be used. The format is e.g. /radio/.
  • @@ -1281,6 +1308,9 @@ Here an event is defined, where in time of 2 seconds the Mute-Button has to be p
  • DailyIndexRefreshTime <time>
    Setzt die aktuell gültige DailyIndexRefreshTime für alle Zoneplayer.
  • +
  • +ExportSonosBibliothek <filename> +
    Exportiert eine Datei mit der textuellen Darstellung eines Struktur- und Titelhashs, das die komplette Navigationsstruktur aus der Sonos-Bibliothek abbildet. Achtung: Benötigt eine große Menge CPU-Zeit und Arbeitsspeicher für die Ausführung!
  • Name <Zonename>
    Legt den Namen der Zone fest.
  • @@ -1296,6 +1326,9 @@ Here an event is defined, where in time of 2 seconds the Mute-Button has to be p
  • RoomIcon <Iconname>
    Legt das Icon für die Zone fest
  • +
  • +SnoozeAlarm <Time> +
    Unterbricht eine laufende Alarmwiedergabe für den übergebenen Zeitraum.
  • Wifi <State>
    Setzt den WiFi-Zustand des Players. Kann 'off', 'persist-off' oder 'on' sein.
  • @@ -1407,12 +1440,15 @@ Here an event is defined, where in time of 2 seconds the Mute-Button has to be p
  • CurrentPlaylist
    Setzt den Abspielmodus auf die aktuelle Abspielliste, startet aber keine Wiedergabe (z.B. nach dem Hören eines Radiostreams, wo die aktuelle Abspielliste noch existiert, aber gerade "nicht verwendet" wird)
  • +
  • +DeletePlaylist +
    Löscht die bezeichnete Playliste. Zum möglichen Format des Playlistenamen unter LoadPlaylist nachsehen.
  • EmptyPlaylist
    Leert die aktuelle Abspielliste
  • -LoadPlaylist <Playlistname> [EmptyQueueBeforeImport] -
    Lädt die angegebene Playlist in die aktuelle Abspielliste. Der Parameter sollte/kann URL-Encoded werden um auch Spezialzeichen zu ermöglichen. Der Playlistname kann auch ein Dateiname sein. Dann muss dieser mit 'file:' beginnen (z.B. 'file:c:/Test.m3u).
    Wenn der Parameter EmptyQueueBeforeImport mit ''1'' angegeben wirde, wird die aktuelle Abspielliste vor dem Import geleert. Standardmäßig wird hier ''1'' angenommen.
    Zusätzlich kann ein regulärer Ausdruck für den Namen verwendet werden. Der erste Treffer wird verwendet. Das Format ist z.B. /hits.2014/.
  • +LoadPlaylist <Playlistname|Fhem-Devicename> [EmptyQueueBeforeImport] +
    Lädt die angegebene Playlist in die aktuelle Abspielliste. Der Parameter sollte/kann URL-Encoded werden um auch Spezialzeichen zu ermöglichen. Der Playlistname kann ein Fhem-Sonosplayer-Devicename sein, dann wird dessen aktuelle Abpielliste kopiert. Der Playlistname kann aber auch ein Dateiname sein. Dann muss dieser mit 'file:' beginnen (z.B. 'file:c:/Test.m3u).
    Wenn der Parameter EmptyQueueBeforeImport mit ''1'' angegeben wirde, wird die aktuelle Abspielliste vor dem Import geleert. Standardmäßig wird hier ''1'' angenommen.
    Zusätzlich kann ein regulärer Ausdruck für den Namen verwendet werden. Der erste Treffer wird verwendet. Das Format ist z.B. /hits.2014/.
  • LoadRadio <Radiostationname>
    Startet den angegebenen Radiostream. Der Name bezeichnet einen Sender in der Radiofavoritenliste. Die aktuelle Abspielliste wird nicht verändert. Der Parameter sollte/kann URL-Encoded werden um auch Spezialzeichen zu ermöglichen.
    Zusätzlich kann ein regulärer Ausdruck für den Namen verwendet werden. Der erste Treffer wird verwendet. Das Format ist z.B. /radio/.