From 4928a99ac3f257c33ac2eec3b3e76390c72b1815 Mon Sep 17 00:00:00 2001 From: rleins Date: Fri, 2 Jan 2015 15:55:43 +0000 Subject: [PATCH] Sonos: Getter/Setter improvements for the use with FhemWeb git-svn-id: https://svn.fhem.de/fhem/trunk@7408 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/FHEM/00_SONOS.pm | 104 ++++++++++++++---------- fhem/FHEM/21_SONOSPLAYER.pm | 91 ++++++++++++++++----- fhem/FHEM/lib/UPnP/sonos_input_dock.jpg | Bin 0 -> 34332 bytes 3 files changed, 134 insertions(+), 61 deletions(-) create mode 100644 fhem/FHEM/lib/UPnP/sonos_input_dock.jpg diff --git a/fhem/FHEM/00_SONOS.pm b/fhem/FHEM/00_SONOS.pm index 099451e82..b62169ac1 100755 --- a/fhem/FHEM/00_SONOS.pm +++ b/fhem/FHEM/00_SONOS.pm @@ -30,6 +30,14 @@ # Changelog # # SVN-History: +# 02.01.2015 +# Anzeige bei der Wiedergabe eines Docks verbessert. Dort werden nun der Titel und Album/Artist-Informationen und ein Dock-Cover angezeigt. +# Getter/Setter bei Bedarf um ":noArg" erweitert. +# Getter/Setter sind nun nicht mehr CaseSensitive +# Setter für "Treble" und "Bass" haben nun auch einen Slider +# Setter "Icon" in "RoomIcon" umbenannt, damit die Auswahlliste den aktuellen vorauswählt +# Beim Erzeugen der Sonosplayer-Devices wird nun das Attribut "alias" auf den Sonos-Raumnamen gesetzt. +# Zusätzlich zu "StopAll" oder "PauseAll" gibt es am Sonos-Device nun auch "Stop" und "Pause" mit der gleichen Funktionalität # 01.01.2015 # Anzeige in der Player-ReadingsGroup für die Darstellung von disappeared angepasst, dabei auch gleich die Höhenverhältnisse etwas angepasst. # 31.12.2014 @@ -318,7 +326,9 @@ my %gets = ( my %sets = ( 'Groups' => 'groupdefinitions', 'StopAll' => '', - 'PauseAll' => '' + 'Stop' => '', + 'PauseAll' => '', + 'Pause' => '' ); my @SONOS_PossibleDefinitions = qw(NAME INTERVAL); @@ -784,41 +794,10 @@ sub SONOS_FhemWebCallback($) { return (undef, undef); } - # Wird im Prinzip einfach nicht mehr aufgerufen... - #if ($URL =~ m/^\/favorit\?res=(.*)/i) { - # my $resURL = uri_unescape($1); - # - # if ($resURL =~ m/^(x-rincon-cpcontainer|x-sonos-spotify).*?(spotify.*?)(\?|$)/i) { - # my $infos = get('https://embed.spotify.com/oembed/?url='.$2); - # - # if ($infos =~ m/"thumbnail_url":"(.*?)cover(.*?)"/i) { - # $resURL = $1.'original'.$2; - # $resURL =~ s/\\//g; - # } - # } elsif($URL =~ m/savedqueues.rsq/i) { - # $resURL = $attr{global}{modpath}.'/FHEM/lib/UPnP/sonos_playlist.jpg'; - # } else { - # my @player = SONOS_getAllSonosplayerDevices(); - # my $stream = 0; - # $stream = 1 if ($resURL =~ /x-sonosapi-stream/); - # $resURL = $1.'/getaa?'.($stream ? 's=1&' : '').'u='.uri_escape($resURL) if (ReadingsVal($player[0]->{NAME}, 'location', '') =~ m/^(http:\/\/.*?:.*?)\//i); - # } - # - # if ($resURL) { - # SONOS_Log undef, 5, 'Hole Cover: '.$resURL; - # - # if ($resURL =~ /^http/) { - # # Bild 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 $response = $ua->get($resURL); - # if ($response->is_success) { - # return ($response->header('Content-Type').'; charset=UTF8', $response->content); - # } - # } else { - # return ('charset=UTF8', SONOS_ReadFile($resURL)); - # } - # } - #} + if ($URL =~ m/^\/input_dock.jpg/i) { + FW_serveSpecial('sonos_input_dock', 'jpg', $attr{global}{modpath}.'/FHEM/lib/UPnP', 1); + return (undef, undef); + } } # Wenn wir hier ankommen, dann konnte nichts verarbeitet werden... @@ -1654,8 +1633,25 @@ sub SONOS_Get($@) { my $reading = $a[1]; my $name = $hash->{NAME}; + # for the ?-selector: which values are possible + if($a[1] eq '?') { + my @newGets = (); + for my $elem (sort keys %gets) { + push @newGets, $elem.(($gets{$elem} eq '') ? ':noArg' : ''); + } + return "Unknown argument, choose one of ".join(" ", @newGets); + } + # check argument - return "SONOS: Get with unknown argument $a[1], choose one of ".join(",", sort keys %gets) if(!defined($gets{$reading})); + my $found = 0; + for my $elem (keys %gets) { + if (lc($reading) eq lc($elem)) { + $reading = $elem; # Korrekte Schreibweise behalten + $found = 1; + last; + } + } + return "SONOS: Get with unknown argument $a[1], choose one of ".join(",", sort keys %gets) if(!$found); # some argument needs parameter(s), some not return "SONOS: $a[1] needs parameter(s): ".$gets{$a[1]} if (scalar(split(',', $gets{$a[1]})) > scalar(@a) - 2); @@ -1746,12 +1742,26 @@ sub SONOS_Set($@) { } else { %setcopy = %sets; } - + # for the ?-selector: which values are possible - return join(" ", sort keys %setcopy) if($a[1] eq '?'); - + if($a[1] eq '?') { + my @newSets = (); + for my $elem (sort keys %setcopy) { + push @newSets, $elem.(($setcopy{$elem} eq '') ? ':noArg' : ''); + } + return "Unknown argument, choose one of ".join(" ", @newSets); + } + # check argument - return "SONOS: Set with unknown argument $a[1], choose one of ".join(",", sort keys %sets) if(!defined($sets{$a[1]})); + my $found = 0; + for my $elem (keys %sets) { + if (lc($a[1]) eq lc($elem)) { + $a[1] = $elem; # Korrekte Schreibweise behalten + $found = 1; + last; + } + } + return "SONOS: Set with unknown argument $a[1], choose one of ".join(",", sort keys %sets) if(!$found); # some argument needs parameter(s), some not return "SONOS: $a[1] needs parameter(s): ".$sets{$a[1]} if (scalar(split(',', $sets{$a[1]})) > scalar(@a) - 2); @@ -1841,7 +1851,7 @@ sub SONOS_Set($@) { #} #SONOS_Log undef, 5, "Current after List: ".Dumper(\@current); - } elsif (lc($key) =~ m/(Stop|Pause)All/i) { + } elsif (lc($key) =~ m/(Stop|Pause)(All|)/i) { my $commandType = $1; # Aktuellen Zustand holen @@ -4053,6 +4063,7 @@ sub SONOS_Discover_Callback($$$) { # Define SonosPlayer-Device with attributes SONOS_Client_Notifier('CommandDefine:'.$name.' SONOSPLAYER '.$udn); SONOS_Client_Notifier('CommandAttr:'.$name.' room '.$SONOS_Client_Data{SonosDeviceName}); + SONOS_Client_Notifier('CommandAttr:'.$name.' alias '.$roomName); SONOS_Client_Notifier('CommandAttr:'.$name.' group '.$groupName); SONOS_Client_Notifier('CommandAttr:'.$name.' icon '.$iconPath); SONOS_Client_Notifier('CommandAttr:'.$name.' sortby 1'); @@ -4064,6 +4075,7 @@ sub SONOS_Discover_Callback($$$) { SONOS_Client_Notifier('CommandAttr:'.$name.' generateInfoSummarize2 '); SONOS_Client_Notifier('CommandAttr:'.$name.' generateInfoSummarize3 ~ Balance: '); SONOS_Client_Notifier('CommandAttr:'.$name.' stateVariable Presence'); + SONOS_Client_Notifier('CommandAttr:'.$name.' generateVolumeSlider 1'); SONOS_Client_Notifier('CommandAttr:'.$name.' getAlarms 1'); SONOS_Client_Data_Refresh('', $udn, 'getAlarms', 1); SONOS_Client_Notifier('CommandAttr:'.$name.' minVolume 0'); SONOS_Client_Data_Refresh('', $udn, 'minVolume', 0); @@ -4644,6 +4656,14 @@ sub SONOS_ServiceCallback($$) { SONOS_Client_Notifier('SetCurrent:Artist:'); SONOS_Client_Notifier('ProcessCover:'.$udn.':0:/fhem/sonos/cover/input_default.jpg:'); + } elsif ($currentTrackURI =~ m/x-sonos-dock:(RINCON_[\dA-Z]+)/) { + # Dock-Wiedergabe feststellen, und dann andere Informationen anzeigen + SONOS_Client_Notifier('SetCurrent:Album:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'currentAlbum', SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1))); + my $tmpTitle = SONOS_replaceSpecialStringCharacters(decode_entities($1)) if ($currentTrackMetaData =~ m/(.*?)<\/dc:title>/i); + SONOS_Client_Notifier('SetCurrent:Title:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'currentTitle', $tmpTitle)); + SONOS_Client_Notifier('SetCurrent:Artist:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'currentArtist', '')); + + SONOS_Client_Notifier('ProcessCover:'.$udn.':0:/fhem/sonos/cover/input_dock.jpg:'); } elsif ($currentTrackURI =~ m/x-sonos-htastream:(RINCON_[\dA-Z]+):spdif/) { # LineIn-Wiedergabe der Playbar feststellen, und dann andere Informationen anzeigen SONOS_Client_Notifier('SetCurrent:Album:'.SONOS_Client_Data_Retreive($1.'_MR', 'reading', 'roomName', $1)); diff --git a/fhem/FHEM/21_SONOSPLAYER.pm b/fhem/FHEM/21_SONOSPLAYER.pm index fc3925c5d..555b9ba36 100755 --- a/fhem/FHEM/21_SONOSPLAYER.pm +++ b/fhem/FHEM/21_SONOSPLAYER.pm @@ -158,7 +158,7 @@ my %sets = ( 'Reboot' => '', 'Wifi' => 'state', 'Name' => 'roomName', - 'Icon' => 'iconName' + 'RoomIcon' => 'iconName' ); my @possibleRoomIcons = qw(bathroom library office foyer dining tvroom hallway garage garden guestroom den bedroom kitchen portable media family pool masterbedroom playroom patio living); @@ -278,13 +278,35 @@ sub SONOSPLAYER_Get($@) { my $reading = $a[1]; my $name = $hash->{NAME}; - my $udn = $hash->{UDN}; + my $udn = $hash->{UDN}; + + # for the ?-selector: which values are possible + if($reading eq '?') { + my @newGets = (); + for my $elem (sort keys %gets) { + my $newElem = $elem.(($gets{$elem} eq '') ? ':noArg' : ''); + + $newElem = $elem.':0,1' if (lc($elem) eq 'ethernetportstatus'); + + push @newGets, $newElem; + } + return "Unknown argument, choose one of ".join(" ", @newGets); + } # check argument - return "SONOSPLAYER: Get with unknown argument $a[1], choose one of ".join(" ", sort keys %gets) if(!defined($gets{$reading})); + my $found = 0; + for my $elem (keys %gets) { + if (lc($reading) eq lc($elem)) { + $a[1] = $elem; # Korrekte Schreibweise behalten + $reading = $elem; # Korrekte Schreibweise behalten + $found = 1; + last; + } + } + return "SONOSPLAYER: Get with unknown argument $a[1], choose one of ".join(" ", sort keys %gets) if(!$found); # some argument needs parameter(s), some not - return "SONOSPLAYER: $a[1] needs parameter(s): ".$gets{$a[1]} if (scalar(split(',', $gets{$a[1]})) > scalar(@a) - 2); + return "SONOSPLAYER: $a[1] needs parameter(s): ".$gets{$reading} if (scalar(split(',', $gets{$reading})) > scalar(@a) - 2); # getter if (lc($reading) eq 'currenttrackposition') { @@ -345,16 +367,30 @@ sub SONOSPLAYER_Set($@) { if($a[1] eq '?') { # %setCopy enthält eine Kopie von %sets, da für eine ?-Anfrage u.U. ein Slider zurückgegeben werden muss... my %setcopy; - if (AttrVal($hash, 'generateVolumeSlider', 1) == 1) { - foreach my $key (keys %sets) { - my $oldkey = $key; + foreach my $key (keys %sets) { + my $oldkey = $key; + if (AttrVal($hash, 'generateVolumeSlider', 1) == 1) { $key = $key.':slider,0,1,100' if ($key eq 'Volume'); + $key = $key.':slider,0,1,100' if ($key eq 'GroupVolume'); + $key = $key.':slider,0,1,100' if ($key eq 'Treble'); + $key = $key.':slider,0,1,100' if ($key eq 'Bass'); $key = $key.':slider,-100,1,100' if ($key eq 'Balance'); - - $setcopy{$key} = $sets{$oldkey}; } - } else { - %setcopy = %sets; + + # On/Off einsetzen + $key = $key.':off,on' if ((lc($key) eq 'crossfademode') || (lc($key) eq 'groupmute') || (lc($key) eq 'ledstate') || (lc($key) eq 'loudness') || (lc($key) eq 'lute') || (lc($key) eq 'repeat') || (lc($key) eq 'shuffle')); + + # Iconauswahl einsetzen + if (lc($key) eq 'roomicon') { + my $icons = SONOSPLAYER_Get($hash, ($hash->{NAME}, 'PossibleRoomIcons')); + $icons =~ s/ //g; + $key = $key.':'.$icons; + } + + # Wifi-Auswahl setzen + $key = $key.':off,on,persist-off' if (lc($key) eq 'wifi'); + + $setcopy{$key} = $sets{$oldkey}; } my $sonosDev = SONOS_getDeviceDefHash(undef); @@ -362,12 +398,29 @@ sub SONOSPLAYER_Set($@) { $sets{Speak2} = 'volume language text' if (AttrVal($sonosDev->{NAME}, 'Speak2', '') ne ''); $sets{Speak3} = 'volume language text' if (AttrVal($sonosDev->{NAME}, 'Speak3', '') ne ''); $sets{Speak4} = 'volume language text' if (AttrVal($sonosDev->{NAME}, 'Speak4', '') ne ''); + + # for the ?-selector: which values are possible + if($a[1] eq '?') { + my @newSets = (); + for my $elem (sort keys %setcopy) { + push @newSets, $elem.(($setcopy{$elem} eq '') ? ':noArg' : ''); + } + return "Unknown argument, choose one of ".join(" ", @newSets); + } - return join(" ", sort keys %setcopy); + #return join(" ", sort keys %setcopy); } # check argument - return "SONOSPLAYER: Set with unknown argument $a[1], choose one of ".join(" ", sort keys %sets) if(!defined($sets{$a[1]})); + my $found = 0; + for my $elem (keys %sets) { + if (lc($a[1]) eq lc($elem)) { + $a[1] = $elem; # Korrekte Schreibweise behalten + $found = 1; + last; + } + } + return "SONOSPLAYER: Set with unknown argument $a[1], choose one of ".join(" ", sort keys %sets) if(!$found); # some argument needs parameter(s), some not return "SONOSPLAYER: $a[1] needs parameter(s): ".$sets{$a[1]} if (scalar(split(',', $sets{$a[1]})) > scalar(@a) - 2); @@ -702,7 +755,7 @@ sub SONOSPLAYER_Set($@) { } SONOS_DoWork($udn, 'setName', $text); - } elsif (lc($key) eq 'icon') { + } elsif (lc($key) eq 'roomicon') { $hash = SONOSPLAYER_GetRealTargetPlayerHash($hash); $udn = $hash->{UDN}; $value = lc($value); @@ -835,8 +888,8 @@ sub SONOSPLAYER_Log($$$) {
  • set <name> DailyIndexRefreshTime <time>
    Sets the current DailyIndexRefreshTime for the whole bunch of Zoneplayers.
  • -
  • -set <name> Icon <Iconname> +
  • +set <name> RoomIcon <Iconname>
    Sets the Icon for this Zone
  • set <name> Name <Zonename> @@ -996,7 +1049,7 @@ sub SONOSPLAYER_Log($$$) {
    Gets the Ethernet-Portstatus of the given Port. Can be 'Active' or 'Inactive'.
  • get <name> PossibleRoomIcons -
    Retreives a list of all possible Roomiconnames for the use with "set Icon".
  • +
    Retreives a list of all possible Roomiconnames for the use with "set RoomIcon".
  • Lists
    • @@ -1114,8 +1167,8 @@ Here an event is defined, where in time of 2 seconds the Mute-Button has to be p
    • set <name> DailyIndexRefreshTime <time>
      Setzt die aktuell gültige DailyIndexRefreshTime für alle Zoneplayer.
    • -
    • -set <name> Icon <Iconname> +
    • +set <name> RoomIcon <Iconname>
      Legt das Icon für die Zone fest
    • set <name> Name <Zonename> diff --git a/fhem/FHEM/lib/UPnP/sonos_input_dock.jpg b/fhem/FHEM/lib/UPnP/sonos_input_dock.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9e721a851a78617fc6a80cc67a8cd32a17d2471c GIT binary patch literal 34332 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 ZXV-ayEhaC