################################################################ # # $Id$ # # (c) 2014 Copyright: Wzut # All rights reserved # # FHEM Forum : http://forum.fhem.de/index.php/topic,18517.msg400328.html#msg400328 # # This code is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # The GNU General Public License can be found at # http://www.gnu.org/copyleft/gpl.html. # A copy is found in the textfile GPL.txt and important notices to the license # from the author is found in LICENSE.txt distributed with these scripts. # This script is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. ################################################################ # Version 1.32 - 03.01.17 # Version 1.31 - 30.12.16 # Version 1.3 - 14.12.16 # Version 1.2 - 10.04.16 # Version 1.1 - 03.02.16 # Version 1.01 - 18.08.14 # add set toggle command # Version 1.0 - 21.02.14 # add german doc , readings & state times only on change, devStateIcon # Version 0.95 - 17.02.14 # add command set IdleNow # Version 0.9 - 15.02.14 # Version 0.8 - 01.02.14 , first version package main; use strict; use warnings; use Time::HiRes qw(gettimeofday); use URI::Escape; use POSIX; use Blocking; # http://www.fhemwiki.de/wiki/Blocking_Call use IO::Socket; use Getopt::Std; use HttpUtils; use XML::Simple qw(:strict); use HTML::Entities; sub MPD_html($); my %gets = ( "music:noArg" => "", "playlists:noArg" => "", "playlistinfo:noArg" => "", "statusRequest:noArg" => "", "currentsong:noArg" => "", "outputs:noArg" => "", ); my %sets = ( "play" => "", "clear:noArg" => "", "stop:noArg" => "", "pause:noArg" => "", "previous:noArg" => "", "next:noArg" => "", "random:noArg" => "", "repeat:noArg" => "", "volume:slider,0,1,100" => "", "volumeUp:noArg" => "", "volumeDown:noArg" => "", "playlist" => "", "playfile" => "", "updateDb:noArg" => "", "mpdCMD" => "", "reset:noArg" => "", "single:noArg" => "", "IdleNow:noArg" => "", "toggle:noArg" => "", "clear_readings:noArg" => "", ); use constant clb => "command_list_begin\n"; use constant cle => "status\nstats\ncurrentsong\ncommand_list_end"; use constant lfm => "http://ws.audioscrobbler.com/2.0/?method=artist.getinfo&api_key="; ################################### sub MPD_Initialize($) { my ($hash) = @_; my $name = $hash->{NAME}; $hash->{GetFn} = "MPD_Get"; $hash->{SetFn} = "MPD_Set"; $hash->{DefFn} = "MPD_Define"; $hash->{UndefFn} = "MPD_Undef"; $hash->{ShutdownFn} = "MPD_Undef"; $hash->{AttrFn} = "MPD_Attr"; $hash->{AttrList} = "disable:0,1 password loadMusic:0,1 loadPlaylists:0,1 volumeStep:1,2,5,10 titleSplit:1,0 timeout waits stateMusic:0,1 statePlaylists:0,1 lastfm_api_key image_size:-1,0,1,2,3 cache artist_summary:0,1 artist_content:0,1 player:mpd,mopidy,forked-daapd ".$readingFnAttributes; $hash->{FW_summaryFn} = "MPD_summaryFn"; } sub MPD_updateConfig($) { # this routine is called 5 sec after the last define of a restart # this gives FHEM sufficient time to fill in attributes my ($hash) = @_; my $name = $hash->{NAME}; if (!$init_done) { RemoveInternalTimer($hash); InternalTimer(gettimeofday()+5,"MPD_updateConfig", $hash, 0); return; } my $error; $hash->{".playlist"} = ""; $hash->{".playlists"} = ""; $hash->{".musiclist"} = ""; $hash->{".music"} = ""; $hash->{".outputs"} = ""; $hash->{".lasterror"} = ""; $hash->{PRESENCE} = "absent"; $hash->{".volume"} = -1; $hash->{".artist"} = ""; $hash->{".password"} = AttrVal($name, "password", ""); $hash->{TIMEOUT} = AttrVal($name, "timeout", 2); $hash->{".sMusicL"} = AttrVal($name, "stateMusic", 1); $hash->{".sPlayL"} = AttrVal($name, "statePlaylists", 1); $hash->{".apikey"} = AttrVal($name, "lastfm_api_key", "f3a26c7c8b4c4306bc382557d5c04ad5"); $hash->{".player"} = AttrVal($name, "player", "mpd"); delete($gets{"music:noArg"}) if ($hash->{".player"} eq "mopidy"); ## kommen wir via reset Kommando ? if ($hash->{".reset"}) { $hash->{".reset"} = 0; RemoveInternalTimer($hash); if(defined($hash->{IPID})) { BlockingKill($hash->{helper}{RUNNING_PID}); Log3 $name,4, "$name, Idle Kill PID : ".$hash->{IPID}; delete $hash->{helper}{RUNNING_PID}; delete $hash->{IPID}; Log3 $name,4,"$name, Reset done"; } } if (IsDisabled($name)) { readingsSingleUpdate($hash,"state","disabled",1); return undef; } MPD_ClearReadings($hash); # beim Starten etwas aufräumen MPD_Outputs_Status($hash); if ((AttrVal($name, "icon_size", -1) > -1) && (AttrVal($name, "cache", "") ne "")) { my $cache = AttrVal($name, "cache", ""); unless(-e ("./www/".$cache) or mkdir ("./www/".$cache)) { #Verzeichnis anlegen gescheitert Log3 $name,3,"$name, Could not create directory: www/$cache"; } #else {Log3 $name,4,"$name, lastfm cache = www/$cache";} } if (MPD_try_idle($hash)) { # Playlisten und Musik Dir laden ? # nicht bei Player mopidy, listall wird von ihm nicht unterstützt ! if ((AttrVal($name, "loadMusic", "1") eq "1") && !$error && ($hash->{".player"} ne "mopidy")) { $error = mpd_cmd($hash, "i|listall|music"); Log3 $name,3,"$name, error loading music -> $error" if ($error); readingsSingleUpdate($hash,"error",$error,1) if ($error); } if ((AttrVal($name, "loadPlaylists", "1") eq "1") && !$error) { $error = mpd_cmd($hash, "i|lsinfo|playlists"); Log3 $name,3,"$name, error loading playlists -> $error" if ($error); readingsSingleUpdate($hash,"error",$error,1) if ($error); } } else { readingsSingleUpdate($hash,"state","error",1);} return undef; } sub MPD_Define($$) { my ($hash, $def) = @_; my $name = $hash->{NAME}; my @a = split("[ \t][ \t]*", $def); return "Usage: define $name [] []" if(int(@a) > 4); $hash->{HOST} = (defined($a[2])) ? $a[2] : "127.0.0.1"; $hash->{PORT} = (defined($a[3])) ? $a[3] : "6600" ; $hash->{".reset"} = 0; Log3 $name,3,"$name, Device defined."; readingsSingleUpdate($hash,"state","defined",1); $attr{$name}{devStateIcon} = 'play:rc_PLAY:stop stop:rc_STOP:play pause:rc_PAUSE:pause error:icoBlitz' unless (exists($attr{$name}{devStateIcon})); $attr{$name}{icon} = 'it_radio' unless (exists($attr{$name}{icon})); $attr{$name}{titleSplit} = '1' unless (exists($attr{$name}{titleSplit})); $attr{$name}{player} = 'mpd' unless (exists($attr{$name}{player})); $attr{$name}{loadPlaylists} = '1' unless (exists($attr{$name}{loadPlaylists})); #$attr{$name}{cache} = 'lfm' unless (exists($attr{$name}{cache})); #$attr{$name}{loadMusic} = '1' unless (exists($attr{$name}{loadMusic})) && ($attr{$name}{player} ne 'mopidy'); RemoveInternalTimer($hash); InternalTimer(gettimeofday()+5, "MPD_updateConfig", $hash, 0); return undef; } sub MPD_Undef ($$) { my ($hash, $arg) = @_; RemoveInternalTimer($hash); if(defined($hash->{helper}{RUNNING_PID})) { BlockingKill($hash->{helper}{RUNNING_PID}); } return undef; } sub MPD_Attr (@) { my ($cmd, $name, $attrName, $attrVal) = @_; my $hash = $defs{$name}; if ($cmd eq "set") { if ($attrName eq "timeout") { if (int($attrVal) < 1) {$attrVal = 1;} $hash->{TIMEOUT} = $attrVal; $attr{$name}{timeout} = $attrVal; } elsif ($attrName eq "password") { $hash->{".password"} = $attrVal; $attr{$name}{password} = $attrVal; } elsif (($attrName eq "disable") && ($attrVal == 1)) { readingsSingleUpdate($hash,"state","disabled",1); $attr{$name}{disable} = $attrVal; } elsif (($attrName eq "disable") && ($attrVal == 0)) { $attr{$name}{disable} = $attrVal; readingsSingleUpdate($hash,"state","reset",1); $hash->{".reset"} = 1; MPD_updateConfig($hash); } elsif ($attrName eq "statePlaylists") { $attr{$name}{statePlaylists} = $attrVal; $hash->{".sPlayL"}=$attrVal; } elsif ($attrName eq "stateMusic") { $attr{$name}{stateMusic} = $attrVal; $hash->{".sMusicL"}=$attrVal; } elsif ($attrName eq "player") { $attr{$name}{player} = $attrVal; $hash->{".player"}=$attrVal; } elsif ($attrName eq "cache") { unless(-e ("./www/".$attrVal) or mkdir ("./www/".$attrVal)) { #Verzeichnis anlegen gescheitert return "Could not create directory: www/$attrVal"; } $attr{$name}{cache} = $attrVal; } } elsif ($cmd eq "del") { if ($attrName eq "disable") { $attr{$name}{disable} = 0; readingsSingleUpdate($hash,"state","reset",1); $hash->{".reset"}=1; MPD_updateConfig($hash); } elsif ($attrName eq "statePlaylists") { $hash->{".sPlayL"} = 1; } elsif ($attrName eq "stateMusic") { $hash->{".sMusicL"} = 1; } elsif ($attrName eq "player") { $hash->{".player"} = "mpd"; } } return undef; } sub MPD_ClearReadings($) { my ($hash)= @_; readingsBeginUpdate($hash); if ($hash->{".player"} eq "forked-daapd") { readingsBulkUpdate($hash,"albumartistsort",""); readingsBulkUpdate($hash,"artistsort",""); } #readingsBulkUpdate($hash,"albumartist",""); readingsBulkUpdate($hash,"Album",""); readingsBulkUpdate($hash,"Artist",""); readingsBulkUpdate($hash,"file",""); readingsBulkUpdate($hash,"Genre",""); readingsBulkUpdate($hash,"Last-Modified",""); readingsBulkUpdate($hash,"Title",""); readingsBulkUpdate($hash,"Name",""); readingsBulkUpdate($hash,"Date",""); readingsBulkUpdate($hash,"Track",""); readingsBulkUpdate($hash,"playlistname",""); #readingsBulkUpdate($hash,"artist_image","/fhem/icons/1px-spacer", ""); #readingsBulkUpdate($hash,"artist_image_html",""); readingsBulkUpdate($hash,"artist_summary","") if (AttrVal($hash->{NAME}, "artist_summary","")); readingsBulkUpdate($hash,"artist_content","") if (AttrVal($hash->{NAME}, "artist_content","")); readingsEndUpdate($hash, 0); return; } sub MPD_Set($@) { my ($hash, @a)= @_; my $name= $hash->{NAME}; my $ret ; return join(" ", sort keys %sets) if(@a < 2); return undef if(IsDisabled($name)); my $cmd = $a[1]; return join(" ", sort keys %sets) if ($cmd eq "?"); if ($cmd eq "mpdCMD") { my $sub; shift @a; shift @a; $sub = join (" ", @a); return $name." ".$sub.":\n".mpd_cmd($hash, "i|$sub|x"); } my $subcmd = (defined($a[2])) ? $a[2] : ""; return undef if ($subcmd eq '---'); # erster Eintrag im select Feld ignorieren my $step = int(AttrVal($name, "volumeStep", 5)); # vllt runtersetzen auf default = 2 ? my $vol_now = int($hash->{".volume"}); my $vol_new; if ($cmd eq "reset") { $hash->{".reset"} = 1; MPD_updateConfig($hash); return undef;} if ($cmd eq "pause") { $ret = mpd_cmd($hash, clb."pause\n".cle); return $ret; } if ($cmd eq "update") { $ret = mpd_cmd($hash, clb."update\n".cle); return $ret; } if ($cmd eq "stop") { readingsSingleUpdate($hash,"artist_image","/fhem/icons/1px-spacer",1); readingsSingleUpdate($hash,"artist_image_html","",1); $ret = mpd_cmd($hash, clb."stop\n".cle); return $ret; } if ($cmd eq "toggle") { $ret = mpd_cmd($hash, clb."play\n".cle) if (($hash->{STATE} eq "stop") || ($hash->{STATE} eq "pause")); if ($hash->{STATE} eq "play") { readingsSingleUpdate($hash,"artist_image","/fhem/icons/1px-spacer",1); readingsSingleUpdate($hash,"artist_image_html","",1); $ret = mpd_cmd($hash, clb."stop\n".cle); } } if ($cmd eq "previous") { if (defined($hash->{READINGS}{"song"}{VAL}) > 0) { MPD_ClearReadings($hash); $ret = mpd_cmd($hash, clb."previous\n".cle); } else { return undef; } } if ($cmd eq "next") { if ($hash->{READINGS}{"nextsong"}{VAL} != $hash->{READINGS}{"song"}{VAL}) { MPD_ClearReadings($hash); $ret = mpd_cmd($hash, clb."next\n".cle); } else { return undef; } } if ($cmd eq "random") { my $rand = ($hash->{READINGS}{random}{VAL}) ? "0" : "1"; $ret = mpd_cmd($hash, clb."random $rand\n".cle); } if ($cmd eq "repeat") { my $rep = ($hash->{READINGS}{repeat}{VAL}) ? "0" : "1"; $ret = mpd_cmd($hash, clb."repeat $rep\n".cle); } if ($cmd eq "single") { my $single = ($hash->{READINGS}{single}{VAL}) ? "0" : "1"; $ret = mpd_cmd($hash, clb."single $single\n".cle); } if ($cmd eq "clear") { MPD_ClearReadings($hash); $ret = mpd_cmd($hash, clb."clear\n".cle); $hash->{".music"} = ""; $hash->{".playlist"} = ""; } if ($cmd eq "volume") { if (int($subcmd) > 100) { $vol_new = "100"; } elsif (int($subcmd) < 0) { $vol_new = "0"; } else { $vol_new = $subcmd; } # sollte nun zwischen 0 und 100 sein } if ($cmd eq "volumeUp") { $vol_new = (($vol_now + $step) <= 100) ? $vol_now+$step : "100"; } if ($cmd eq "volumeDown") { $vol_new = (($vol_now - $step) >= 0) ? $vol_now-$step : " 0"; } # muessen wir die Laustärke verändern ? if (defined($vol_new)) { $ret = mpd_cmd($hash, clb."setvol $vol_new\n".cle); } # einfaches Play bzw Play Listenplatz Nr. ? if ($cmd eq "play") { MPD_ClearReadings($hash); $ret = mpd_cmd($hash,clb."play $subcmd\n".cle); } if ($cmd eq "IdleNow") { return "$name: sorry, a Idle process is always running with pid ".$hash->{IPID} if(defined($hash->{IPID})); MPD_try_idle($hash); return undef; } if ($cmd eq "clear_readings") { MPD_ClearReadings($hash); return undef; } # die ersten beiden brauchen wir nicht mehr shift @a; shift @a; # den Rest als ein String $subcmd = join(" ",@a); if ($cmd eq "playlist") { return "$name : no name !" if (!$subcmd); MPD_ClearReadings($hash); $hash->{".music"} = ""; my $old_list = $hash->{".playlist"}; $hash->{".playlist"} = $subcmd; # interne Playlisten Verwaltung readingsSingleUpdate($hash,"playlistname",$subcmd,1); $ret = mpd_cmd($hash, clb."stop\nclear\nload \"$subcmd\"\nplay\n".cle); if ($old_list ne $hash->{".playlist"}) { MPD_NewPlaylist($hash,mpd_cmd($hash, "i|playlistinfo|x|")); } } if ($cmd eq "playfile") { return "$name, no File !" if (!$subcmd); MPD_ClearReadings($hash); $hash->{".playlist"} = ""; readingsSingleUpdate($hash,"playlistname","",1); $hash->{".music"} = $subcmd; # interne Song Verwaltung $ret = mpd_cmd($hash, clb."stop\nclear\nadd \"$subcmd\"\nplay\n".cle); } if ($cmd eq "updateDb") { $ret = mpd_cmd($hash, clb."rescan\n".cle); } if ($cmd eq "mpd_event") { if ($subcmd) { #MPD_ClearReadings($hash) if (index($subcmd,"playlist") != -1); readingsSingleUpdate($hash,"mpd_event",$subcmd,1); } mpd_cmd($hash, clb.cle); return undef; } if (substr($cmd,0,13) eq "outputenabled") { my $oid = substr($cmd,13,1); if ($subcmd eq "1") { $ret = mpd_cmd($hash, "i|enableoutput $oid|x"); Log3 $name , 5, "enableoutput $oid | $subcmd"; } else { $ret = mpd_cmd($hash, "i|disableoutput $oid|x"); Log3 $name , 5 ,"disableoutput $oid | $subcmd"; } MPD_Outputs_Status($hash); } return $ret; } sub MPD_Get($@) { my ($hash, @a)= @_; my $name= $hash->{NAME}; my $ret; my $cmd; return "get $name needs at least one argument" if(int(@a) < 2); $cmd = $a[1]; return(MPD_html($hash)) if ($cmd eq "webrc"); return "no get cmd on a disabled device !" if(IsDisabled($name)); if ($cmd eq "playlists") { $hash->{".playlists"} = ""; mpd_cmd($hash, "i|lsinfo|playlists"); return format_get_output("Playlists",$hash->{".playlists"}); } if ($cmd eq "music") { return "Command not supported by player mopidy !" if ($hash->{".player"} eq "mopidy"); $hash->{".musiclist"} = ""; mpd_cmd($hash, "i|listall|music"); return format_get_output("Music",$hash->{".musiclist"}); } if ($cmd eq "statusRequest") { mpd_cmd($hash, clb.cle); $ret = mpd_cmd($hash, "i|".clb.cle."|x|s"); return format_get_output("Status Request", $ret) if($ret); return undef; } if ($cmd eq "outputs") { MPD_Outputs_Status($hash); return format_get_output("Outputs", $hash->{".outputs"}); } return format_get_output("Current Song", mpd_cmd($hash, "i|currentsong|x")) if ($cmd eq "currentsong"); return format_get_output("Playlist Info",mpd_cmd($hash, "i|playlistinfo|x")) if ($cmd eq "playlistinfo"); return "$name get with unknown argument $cmd, choose one of " . join(" ", sort keys %gets); } sub format_get_output($$) { my ($head,$ret)= @_; my $width = 10; my @arr = split("\n",$ret); #my @sort = sort(@arr); foreach(@arr) { $width = length($_) if(length($_) > $width); } return $head."\n".("-" x $width)."\n".$ret; } sub MPD_Outputs_Status($) { my ($hash)= @_; my $name = $hash->{NAME}; $hash->{".outputs"} = mpd_cmd($hash, "i|outputs|x"); my @outp = split("\n" , $hash->{".outputs"}); readingsBeginUpdate($hash); my $outpid = "0"; foreach (@outp) { my @val = split(": " , $_); Log3 $name, 4 ,"$name: MPD_Outputs_Status -> $val[0] = $val[1]"; $outpid = ($val[0] eq "outputid") ? $val[1] : $outpid; readingsBulkUpdate($hash,$val[0].$outpid,$val[1]) if ($val[0] ne "outputid"); $sets{$val[0].$outpid.":0,1"} = "" if ($val[0] eq "outputenabled"); } readingsEndUpdate($hash, 1); } sub mpd_cmd($$) { my ($hash,$a)= @_; my $output = ""; my $sp; my $artist; my $name = $hash->{NAME}; my $playlists = $hash->{".playlists"}; $hash->{VERSION} = undef; $hash->{PRESENCE} = "absent"; my $sock = IO::Socket::INET->new( PeerHost => $hash->{HOST}, PeerPort => $hash->{PORT}, Proto => 'tcp', Timeout => $hash->{TIMEOUT}); if (!$sock) { readingsBeginUpdate($hash); readingsBulkUpdate($hash,"state","error"); readingsBulkUpdate($hash,"error",$!); readingsBulkUpdate($hash,"presence","absent"); # MPD ist wohl tot :( readingsEndUpdate($hash, 1 ); Log3 $name, 2 , "$name, cmd error : ".$!; return $!; } while (<$sock>) # MPD rede mit mir , egal was ;) { last if $_ ; } # end of output. chomp $_; return "not a valid mpd server, welcome string was: ".$_ if $_ !~ /^OK MPD (.+)$/; $hash->{PRESENCE} = "present"; my ($b , $c) = split("OK MPD " , $_); $hash->{VERSION} = $c; # ok, now we're connected - let's issue the commands. if ($hash->{".password"} ne "") { # lets try to authenticate with a password print $sock "password ".$hash->{".password"}."\r\n"; while (<$sock>) { last if $_ ; } # end of output. chomp; if ($_ !~ /^OK$/) { print $sock "close\n"; close($sock); readingsSingleUpdate($hash,"error",$_,1); return "password auth failed : ".$_ ; } } my @commands = split("\\|" , $a); if ($commands[0] ne "i") { # start Ausgabe nach Readings oder Internals readingsBeginUpdate($hash); readingsBulkUpdate($hash,"presence","present"); # MPD lebt foreach (@commands) { my $cmd = $_; print $sock "$cmd\r\n"; Log3 $name, 5 , "$name, mpd_cmd[1] -> $cmd"; while (<$sock>) { chomp $_; return "MPD_Msg ACK ERROR ".$_ if $_ =~ s/^ACK //; # oops - error. last if $_ =~ /^OK/; # end of output. Log3 $name, 5 , "$name, rec: ".$_; ($b , $c) = split(": " , $_); if ($b && defined($c)) # ist das ein Reading ? { #$b = lc($b); if ($b eq "volume") { $hash->{".volume"} = $c; } # Sonderfall volume $artist = $c if ($b eq "Artist"); if ($b eq "Title") { $sp = index($c, " - "); if (AttrVal($name, "titleSplit", 1) && ($sp>0)) # wer nicht mag solls eben abschalten { $artist = substr($c,0,$sp); readingsBulkUpdate($hash,"Artist",$artist); readingsBulkUpdate($hash,"Title",substr($c,$sp+3)); } else { readingsBulkUpdate($hash,"Title",$c); } # kein Titel Split } #elsif ($b eq "time") #{ # fix für doppeltes time Reading # https://forum.fhem.de/index.php/topic,18517.msg539676.html#msg539676 #if (index($c,":") == -1) {$b = "songtime";} #readingsBulkUpdate($hash,$b,$c); #} else { readingsBulkUpdate($hash,$b,$c); } # irgendwas aber kein Titel } # defined $c } # while } # foreach readingsEndUpdate($hash, 1 ); MPD_get_artist_info($hash, urlEncode($artist)) if ((AttrVal($name, "image_size", 0) > -1) && $artist); } # Ende der Ausgabe Readings und Internals, ab jetzt folgt nur noch Bildschirmausgabe else { # start internes cmd print $sock $commands[1]."\r\n"; Log3 $name, 5 , "$name, mpd_cmd[2] -> ".$commands[1]; my $d; while (<$sock>) { return "mpd_Msg ACK ERROR ".$_ if $_ =~ s/^ACK //; # oops - error. last if $_ =~ /^OK/; # end of output. $sp = index($_, ": "); $b = substr($_,0,$sp); $c = substr($_,$sp+2); if (($b eq "file" ) && ($commands[2] eq "music")) {$hash->{".musiclist"} .= $c; } # Titelliste füllen elsif (($b eq "playlist" ) && ($commands[2] eq "playlists")) {$hash->{".playlists"} .= $c; } # Playliste füllen if ($commands[2] eq "x") { $output .= $_; } } # while if (defined($commands[3])) { #$output =~s/Title:/title:/g; #$output =~s/Id:/id:/g; #$output =~s/Name:/name:/g; #$output =~s/Pos:/pos:/g; #$output =~s/: / : /g; my @arr = split("\n",$output); @arr = sort(@arr); $output = join("\n",@arr); } } # end internes cmd print $sock "close\n"; close($sock); if ($hash->{".playlists"} ne $playlists) # haben sich sich die Listen geändert ? { $hash->{".playlists"} =~ s/\n+\z//; my $plists = $hash->{".playlists"}; $plists =~ tr/\n/\:/; # Tablet UI will diese Art der Liste readingsSingleUpdate($hash,"playlistcollection", $plists,1); Log3 $name ,5 ,"$name, ".$hash->{READINGS}{"playlistcollection"}{VAL}; } return $output; # falls es einen gibt , wenn nicht - auch gut ;) } # end mpd_msg sub MPD_IdleStart($) { my ($name) = @_; return unless(defined($name)); my $hash = $defs{$name}; my $old_event = ""; my $telnetPort = undef; my $output; # Suche das Telnet Device ohne Passwort # Code geklaut aus Blocking.pm :) foreach my $d (sort keys %defs) { my $h = $defs{$d}; next if(!$h->{TYPE} || $h->{TYPE} ne "telnet" || $h->{SNAME}); next if($attr{$d}{SSL} || AttrVal($d, "allowfrom", "127.0.0.1") ne "127.0.0.1"); next if($h->{DEF} !~ m/^\d+( global)?$/); next if($h->{DEF} =~ m/IPV6/); my %cDev = ( SNAME=>$d, TYPE=>$h->{TYPE}, NAME=>$d.time() ); next if(Authenticate(\%cDev, undef) == 2); # Needs password $telnetPort = $defs{$d}{"PORT"}; last; } return $name."|no telnet port without password found" if (!$telnetPort); my $sock = IO::Socket::INET->new( PeerHost => $hash->{HOST}, PeerPort => $hash->{PORT}, Proto => 'tcp', Timeout => $hash->{TIMEOUT}); return $name."|IdleStart: $!" if (!$sock); while (<$sock>) { last if $_ ; } chomp $_; return $name."|not a valid mpd server, welcome string was: ".$_ if $_ !~ /^OK MPD (.+)$/; if ($hash->{".password"} ne "") { # lets try to authenticate with a password print $sock "password ".$hash->{".password"}."\r\n"; while (<$sock>) { last if $_ ; # end of output. } chomp; if ($_ !~ /^OK$/) { print $sock "close\n"; close($sock); return $name."|mpd password auth failed : ".$_; } } # Waits until there is a noteworthy change in one or more of MPD's subsystems. # As soon as there is one, it lists all changed systems in a line in the format changed: SUBSYSTEM, # where SUBSYSTEM is one of the following: # - database: the song database has been modified after update. # - update: a database update has started or finished. If the database was modified during the update, the database event is also emitted. # - stored_playlist: a stored playlist has been modified, renamed, created or deleted # +- playlist: the current playlist has been modified # +- player: the player has been started, stopped or seeked # +- mixer: the volume has been changed # - output: an audio output has been enabled or disabled # +- options: options like repeat, random, crossfade, replay gain # - sticker: the sticker database has been modified. # - subscription: a client has subscribed or unsubscribed to a channel # - message: a message was received on a channel this client is subscribed to; this event is only emitted when the queue is empty my $sock2 = IO::Socket::INET->new( PeerHost => "127.0.0.1", PeerPort => $telnetPort, Proto => 'tcp', Timeout => 2); return $name."|Idle send: ".$! if (!$sock2); print $sock2 "get $name statusRequest\nexit\n"; close ($sock2); print $sock "idle\n"; while (<$sock>) { if ($_) # es hat sich was getan. { chomp $_; if ($_ =~ s/^ACK //) # oops - error. { print $sock "close\n"; close($sock); return $name."|ACK ERROR : ".$_; } $_ =~s/changed: //g; if (($_ ne $old_event) && ($_ ne "OK")) { $output .= ($old_event eq "") ? $_ : "+".$_; $old_event = $_; } else #if ($_ eq "OK") { print $sock "idle\n"; } # OK } # $_ if ((($old_event eq "player") || ($old_event eq "playlist")|| ($old_event eq "mixer") || ($old_event eq "options")) ) # muessen wir den Parentprozess informieren ? { $sock2 = IO::Socket::INET->new( PeerHost => "127.0.0.1", PeerPort => $telnetPort, Proto => 'tcp', Timeout => 2); return $name."|Idle_loop send: ".$! if (!$sock2); print $sock2 "set $name mpd_event $output\nexit\n"; close($sock2); $old_event = ""; $output = ""; } } #while #print $sock "close\n"; close($sock); return $name."|socket error"; } sub MPD_IdleDone($) { my ($string) = @_; return unless(defined($string)); my @r = split("\\|",$string); my $hash = $defs{$r[0]}; my $ret = (defined($r[1])) ? $r[1] : "unknow error"; my $name = $hash->{NAME}; Log3 $name, 5,"$name, IdleDone -> $string"; delete($hash->{helper}{RUNNING_PID}); delete $hash->{IPID}; readingsBeginUpdate($hash); readingsBulkUpdate($hash,"state","error"); readingsBulkUpdate($hash,"error",$ret); readingsBulkUpdate($hash,"presence","absent"); readingsEndUpdate($hash, 1 ); Log3 $name, 3 , "$name, idle error -> $ret"; return if(IsDisabled($name)); RemoveInternalTimer($hash); InternalTimer(gettimeofday()+AttrVal($name, "waits", 60), "MPD_try_idle", $hash, 0); return; } sub MPD_try_idle($) { my ($hash) = @_; my $name = $hash->{NAME}; my $waits = AttrVal($name, "waits", 60); $hash->{helper}{RUNNING_PID} = BlockingCall("MPD_IdleStart",$name, "MPD_IdleDone", $hash) unless(exists($hash->{helper}{RUNNING_PID})); if ($hash->{helper}{RUNNING_PID}) { $hash->{IPID} = $hash->{helper}{RUNNING_PID}{pid}; Log3 $name, 4 , $name.", Idle new PID : ".$hash->{IPID}; RemoveInternalTimer($hash); if ($^O !~ /Win/) # was könnte man bei Windows tun ? { InternalTimer(gettimeofday()+$waits, "MPD_watch_idle", $hash, 0); # starte die Überwachung } return 1; } else { Log3 $name, 2 , $name.", Idle Start failed, waiting $waits seconds for next try"; RemoveInternalTimer($hash); InternalTimer(gettimeofday()+$waits, "MPD_try_idle", $hash, 0); return 0; } } sub MPD_watch_idle($) { # Lebt denn der Idle Prozess überhaupt noch ? my ($hash) = @_; my $name = $hash->{NAME}; RemoveInternalTimer($hash); return if (IsDisabled($name)); return if (!defined($hash->{IPID})); my $waits = AttrVal($name, "waits", 60); my $cmd = "ps -e | grep '".$hash->{IPID}." '"; my $result = qx($cmd); if (index($result,"perl") == -1) { Log3 $name, 2 , $name.", cant find idle PID ".$hash->{IPID}." in process list !"; BlockingKill($hash->{helper}{RUNNING_PID}); delete $hash->{helper}{RUNNING_PID}; delete $hash->{IPID}; InternalTimer(gettimeofday()+2, "MPD_try_idle", $hash, 0); return; } else { Log3 $name, 5 , $name.", idle PID ".$hash->{IPID}." found"; if (($hash->{READINGS}{"presence"}{VAL} eq "present") && ($hash->{STATE} eq "play")) { # Wichtig um das Readings elapsed aktuell zu halten (TabletUI) mpd_cmd($hash, "status"); readingsSingleUpdate($hash,"playlistname",$hash->{".playlist"},1) if ($hash->{READINGS}{"playlistname"}{VAL} ne $hash->{".playlist"}); } } InternalTimer(gettimeofday()+$waits, "MPD_watch_idle", $hash, 0); return; } sub MPD_get_artist_info ($$) { my ($hash, $artist) = @_; my $name = $hash->{NAME}; return undef if ($hash->{'.artist'} eq $artist); $hash->{'.artist'} = $artist; my $data; my $cache = AttrVal($name,"cache",""); # default my $param = { url => lfm.$hash->{'.apikey'}."&artist=".$artist, timeout => 5, hash => $hash, header => "User-Agent: Mozilla/5.0\r\nAccept: application/xml\r\nAccept-Charset: utf-8", method => "GET", callback => \&MPD_lfm_artist_info }; if ((-e "www/$cache/".$hash->{'.artist'}.".xml") && ($cache ne "")) { Log3 $name ,4,"$name, artist file ".$hash->{'.artist'}.".xml already exist"; if (!open (FILE , "www/$cache/".$hash->{'.artist'}.".xml")) { Log3 $name, 2, "$name, error reading ".$hash->{'.artist'}.".xml : $!"; $hash->{XML} = 0; } else { while(){ $data = $data.$_;} close (FILE); MPD_lfm_artist_info($param,"",$data,'local'); } } else # xml von lastfm holen { Log3 $name ,4,"$name, new artist ".$hash->{'.artist'}." , getting file from lastfm"; HttpUtils_NonblockingGet($param); } return undef; } sub MPD_lfm_artist_info(@) { my ($param, $err, $data, $local) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; my $size = AttrVal($name,"image_size",0); # default my $cache = AttrVal($name,"cache",""); return if ($size < 0); if (!$data || $err) { Log3 $name ,3,"$name, error getting artist info from lastfm -> $err"; MPD_artist_image($hash,"/fhem/icons/10px-kreis-rot",""); return undef; } if (!$local) {Log3 $name,4,"$name, new xml data from lastfm";} if ($cache ne "") { # xml lokal speichern ? if (-e "www/$cache/".$hash->{'.artist'}.".xml") { Log3 $name ,5,"$name, artist ".$hash->{'.artist'}." already exist"; $hash->{XML} = 1; } else { if (!open (FILE , ">"."www/$cache/".$hash->{'.artist'}.".xml")) { Log3 $name, 2, "$name, error saving ".$hash->{'.artist'}.".xml : ".$!; $hash->{XML} = 0; #$hash->{'.artist'} = ""; #return; } else { print FILE $data; close(FILE); $hash->{XML} = 1; } } } my $newxml = XML::Simple->new(ForceArray => ['entry', 'link'], KeyAttr => []); my $xml = $newxml->XMLin($data); my $hw="width='32' height='32'"; $hw="width='64' height='64'" if ($size == 1); $hw="width='174' height='174'" if ($size == 2); $hw="width='300' height='300'" if ($size == 3); if ((exists $xml->{'artist'}->{'bio'}->{'summary'}) && AttrVal($name,"artist_summary",0)) { readingsSingleUpdate($hash,"artist_summary",$xml->{'artist'}->{'bio'}->{'summary'},1); } if ((exists $xml->{'artist'}->{'bio'}->{'content'}) && AttrVal($name,"artist_content",0)) { readingsSingleUpdate($hash,"artist_content",$xml->{'artist'}->{'bio'}->{'content'},1); } if (!$cache || !$hash->{XML}) # cache verwenden ? { if (exists $xml->{'artist'}->{'image'}[$size]->{'content'}) { if (index($xml->{'artist'}->{'image'}[$size]->{'content'},"http") < 0) { MPD_artist_image($hash,"/fhem/icons/10px-kreis-rot",""); Log3 $name,1,"$name, falsche info URL : ".$xml->{'artist'}->{'image'}[$size]->{'content'}; return undef; } MPD_artist_image($hash,$xml->{'artist'}->{'image'}[$size]->{'content'},$hw); } else { MPD_artist_image($hash,"/fhem/icons/10px-kreis-rot", ""); Log3 $name,4,"$name, unknown artist"; } return undef; } # kein cache verwenden if (exists $xml->{'artist'}->{'image'}[$size]->{'content'}) { $hash->{'.suffix'} = substr($xml->{'artist'}->{'image'}[$size]->{'content'},-4); my $fname = $hash->{'.artist'}."_$size".$hash->{'.suffix'}; if (-e "www/$cache/".$fname) { Log3 $name ,4,"$name, artist image ".$fname." local found"; MPD_artist_image($hash,"/fhem/$cache/".$fname,$hw); return undef; } Log3 $name ,4,"$name, no local artist image ".$fname." getting from lastfm"; $param = { url => $xml->{'artist'}->{'image'}[$size]->{'content'}, timeout => 5, hash => $hash, method => "GET", callback => \&MPD_lfm_artist_image }; HttpUtils_NonblockingGet($param); MPD_artist_image($hash,"/fhem/icons/10px-kreis-gelb",""); } else { MPD_artist_image($hash,"/fhem/icons/10px-kreis-rot",""); Log3 $name ,4,"$name, image infos missing , delete old xml"; unlink ("www/$cache/".$hash->{'.artist'}.".xml"); } # keine Image Infos vorhanden ! return undef; } sub MPD_artist_image($$$) { my ($hash, $im , $hw) = @_; readingsSingleUpdate($hash,"artist_image_html","",1); readingsSingleUpdate($hash,"artist_image","$im",1); return; } sub MPD_lfm_artist_image(@) { my ($param, $err, $data) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; my $cache = AttrVal($name,"cache",""); my $size = AttrVal($name,"image_size",1); my $hw="width='32' height='32'"; $hw="width='64' height='64'" if ($size == 1); $hw="width='174' height='174'" if ($size == 2); $hw="width='300' height='300'" if ($size == 3); my $fname = $hash->{'.artist'}."_$size".$hash->{'.suffix'}; if($err ne "") { Log3 $name, 3, "$name, error while requesting ".$param->{url}." - $err"; } elsif(($data ne "") && ($data =~ /PNG/i)) { Log3 $name,4,"$name, got new image from lastfm"; if (!open(FILE, "> www/$cache/$fname")) { Log3 $name, 2, "$name, error saving image $fname : ".$!; MPD_artist_image($hash,"/fhem/icons/10px-kreis-rot"," "); return undef; } binmode(FILE); print FILE $data; close(FILE); MPD_artist_image($hash,"/fhem/$cache/".$fname,$hw); return undef; } Log3 $name,3,"$name, empty or invalid image from lastfm"; unlink ("www/$cache/".$hash->{'.artist'}.".xml"); MPD_artist_image($hash,"/fhem/icons/10px-kreis-rot",""); return undef; } sub MPD_NewPlaylist($$) { my ($hash, $list) = @_; my $name = $hash->{NAME}; Log3 $name,5,"$name, new Playlist in -> $list"; $list =~ s/"/\\"/g; $list = "\n".$list; my @artist = ($list=~/\nArtist:\s(.*)\n/g); my @title = ($list=~/\nTitle:\s(.*)\n/g); my @album = ($list=~/\nAlbum:\s(.*)\n/g); my @time = ($list=~/\nTime:\s(.*)\n/g); my @file = ($list=~/\nfile:\s(.*)\n/g); my @track = ($list=~/\nTrack:\s(.*)\n/g); my @albumUri = ($list=~/\nX-AlbumUri:\s(.*)\n/g); # von Mopidy ? my $ret = '['; my $ua = LWP::UserAgent->new( ssl_opts => { verify_hostname => 1 } ); my $lastUri = ''; my $url; for my $i (0 .. $#artist) { if (defined($albumUri[$i])) { if ( $lastUri ne $albumUri[$i]) { my $response = $ua->get("https://embed.spotify.com/oembed/?url=".$albumUri[$i]); my $data = ''; if ( $response->is_success ) { $data = $response->decoded_content; $url = decode_json( $data ); $lastUri = $url->{'thumbnail_url'}; } } } # vesuchen wir es mit Last.fm elsif (AttrVal($name,"image_size",-1) > -1 && (AttrVal($name,"cache","") ne "")) { my $cache = AttrVal($name,"cache",""); my $size = AttrVal($name,"image_size",0); if (-e "www/$cache/".urlEncode($artist[$i])."_".$size.".png") { $lastUri = "/fhem/www/".$cache."/".urlEncode($artist[$i])."_".$size.".png"; } else { $lastUri = "/fhem/icons/1px-spacer"; } } else { $lastUri = "/fhem/icons/1px-spacer"; } $ret .= '{"Artist":"'.$artist[$i].'",'; $ret .= '"Title":'; $ret .= (defined($title[$i])) ? '"'.$title[$i].'",' : '"",'; $ret .= '"Album":'; $ret .= (defined($album[$i])) ? '"'.$album[$i].'",' : '"",'; $ret .= '"Time":'; $ret .= (defined($time[$i])) ? '"'.$time[$i].'",' : '"",'; $ret .= '"File":"'.$file[$i].'",'; $ret .= '"Track":'; $ret .= (defined($track[$i])) ? '"'.$track[$i].'",' : '"",'; $ret .= '"Cover":"'.$lastUri.'"}'; $ret .= ',' if ($i<$#artist); } $ret .= ']'; $ret =~ s/;//g; $ret =~ s/\\n//g; Log3 $name,5,"$name, new Playlist out -> $ret"; readingsSingleUpdate($hash,"playlistinfo",$ret,1); return; } ############################################### sub MPD_html($) { my ($hash)= @_; my $name = $hash->{NAME}; my $playlist = $hash->{".playlist"}; my $playlists = $hash->{".playlists"}; my $musiclist = $hash->{".musiclist"}; my $music = $hash->{".music"}; my $volume = (defined($hash->{".volume"})) ? $hash->{".volume"} : "???"; my $len = (defined($hash->{READINGS}{"playlistlength"}{VAL})) ? $hash->{READINGS}{"playlistlength"}{VAL} : "--"; my $html; my @list; my $sel = ""; my $pos = (defined($hash->{READINGS}{"song"}{VAL}) && $len) ? $hash->{READINGS}{"song"}{VAL} : "--"; $pos .= "/"; $pos .= ($len) ? $len : "0"; $html = "
"; $html .= ""; if ($playlists||$music) { if ($playlists) { $html .= ""; } if ($musiclist) { $html .= ""; } } $html .= ""; $html .= ""; $html .= ""; $html .= ""; $html .= ""; $html .= ""; $html .= ""; $html .= ""; $html .= ""; $html .= ""; $html .= ""; $html .= ""; $html .= ""; $html .= ""; $html .= ""; if ($hash->{".outputs"}) { my @outp = split("\n" , $hash->{".outputs"}); my $oid; my $oname = ""; my $oen = ""; my $osel = ""; foreach (@outp) { my @val = split(": " , $_); $oid = $val[1] if ($val[0] eq "outputid"); $oname = $val[1] if ($val[0] eq "outputname"); $oen = $val[1] if ($val[0] eq "outputenabled"); if ($oen ne "") { $html .= ""; $html .=""; $html .=""; $oen = ""; } } } $html .= "
  
 ".$pos." 
 ".$volume." 
$oid.$oname "; $osel = ($oen eq "1") ? "checked" : ""; $html .="on "; $osel = ($oen ne "1") ? "checked" : ""; $html .="off
"; return $html; } sub MPD_summaryFn($$$$) { my ($FW_wname, $hash, $room, $pageHash) = @_; $hash = $defs{$hash}; my $state = $hash->{STATE}; my $txt = $state; my $name = $hash->{NAME}; my $playlist = $hash->{".playlist"}; my $playlists = $hash->{".playlists"}; my $music = $hash->{".music"}; my $musiclist = $hash->{".musiclist"}; my ($icon,$isHtml,$link,$html,@list,$sel); ($icon, $link, $isHtml) = FW_dev2image($name); $txt = ($isHtml ? $icon : FW_makeImage($icon, $state)) if ($icon); $link = "cmd.$name=set $name $link" if ($link); $txt = "".$txt."" if ($link); my $rname = ""; my $artist = ""; my $title = ""; my $album = ""; my $file = (defined($hash->{READINGS}{"file"}{VAL})) ? $hash->{READINGS}{"file"}{VAL}." 
" : ""; if (defined($hash->{READINGS}{"Title"}{VAL})) { $title = ($hash->{READINGS}{"Title"}{VAL} ne "" ) ? $hash->{READINGS}{"Title"}{VAL}." 
" : "";} if (defined($hash->{READINGS}{"Artist"}{VAL})) { $artist = ($hash->{READINGS}{"Artist"}{VAL} ne "") ? $hash->{READINGS}{"Artist"}{VAL}." 
": "";} if (defined($hash->{READINGS}{"Album"}{VAL})) { $album = ($hash->{READINGS}{"Album"}{VAL} ne "") ? $hash->{READINGS}{"Album"}{VAL}." " : "";} if (defined($hash->{READINGS}{"Name"}{VAL})) { $rname = ($hash->{READINGS}{"Name"}{VAL} ne "") ? $hash->{READINGS}{"Name"}{VAL}." 
" : ""; } $html ="
$txt"; if (($playlists) && $hash->{".sPlayL"}) { $html .= "
"; } if (($musiclist) && $hash->{".sMusicL"}) { $html .= ""; } $html.= "
"; if ($rname.$artist.$title.$album ne "") { $html .= (($state eq "play") || ($state eq "pause")) ? $rname.$artist.$title.$album : " "; if (defined($hash->{READINGS}{"artist_image"}{VAL})) { my $hw = (index($hash->{READINGS}{"artist_image"}{VAL},"icon") == -1) ? " width='32' height='32'" : ""; $html .= "".$hash->{"; } } else { $html .= (($state eq "play") || ($state eq "pause")) ? $file : " "; } $html .= "
"; return $html; } 1; =pod =item device =item summary controls MPD or Mopidy music server =item summary_DE steuert den MPD oder Mopidy Musik Server =begin html

MPD

FHEM module to control a MPD (or Mopidy) like the MPC (MPC = Music Player Command, the command line interface to the Music Player Daemon )
To install a MPD on a Raspberry Pi you will find a lot of documentation at the web e.g. http://www.forum-raspberrypi.de/Thread-tutorial-music-player-daemon-mpd-und-mpc-auf-dem-raspberry-pi in german
FHEM Forum : Modul für MPD ( in german )
Modul requires XML:Simple -> sudo apt-get install libxml-simple-perl
    Define
      define <name> MPD <IP MPD Server | default localhost> <Port MPD Server | default 6600>
      Example:
        define myMPD MPD 192.168.0.99 7000
        
      if FHEM and MPD a running on the same device :
        define myMPD MPD
        

    Set
      set <name> <what>
       
      Currently, the following commands are defined.
       
      play => like MPC play , start playing song in playlist
      clear => like MPC clear , delete MPD playlist
      stop => like MPC stop, stops playing
      pause => like MPC pause
      previous => like MPC previous, play previous song in playlist
      next => like MPC next, play next song in playlist
      random => like MPC random, toggel on/off
      repeat => like MPC repeat, toggel on/off
      toggle => toggles from play to stop or from stop/pause to play
      updateDb => like MPC update
      volume (%) => like MPC volume %, 0 - 100
      volumeUp => inc volume ( + attr volumeStep size )
      volumeDown => dec volume ( - attr volumeStep size )
      playlist (playlist name) => set playlist on MPD Server
      playfile (file) => create playlist + add file to playlist + start playing
      IdleNow => send Idle command to MPD and wait for events to return
      reset => reset MPD Modul
      mpdCMD (cmd) => send a command to MPD Server ( MPD Command Ref )

    Get
      get <name> <what>
       
      Currently, the following commands are defined.
      music => list all MPD music files in MPD databse
      playlists => list all MPD playlist in MPD databse
      playlistsinfo => show current playlist informations
      webrc => HTML output for a simple Remote Control on FHEM webpage e.g :.
             define <name> weblink htmlCode {fhem("get <name> webrc", 1)}
             attr <name> room MPD
          
      statusRequest => get MPD status
      currentsong => get infos from current song in playlist
      outputs => get name,id,status about all MPD output devices in /etc/mpd.conf

    Attributes
    • password , if password in mpd.conf is set
    • loadMusic 1|0 => load titles from MPD database at startup (not supported by modipy)
    • loadPlaylists 1|0 => load playlist names from MPD database at startup
    • volumeStep 1|2|5|10 => Step size for Volume +/- (default 5)
    • titleSplit 1|0 => split title to artist and title if no artist is given in songinfo (e.g. radio-stream default 1)
    • timeout (default 1) => timeout in seconds for TCP connection timeout
    • waits (default 60) => if idle process ends with error, seconds to wait
    • stateMusic 1|0 => show Music DropDown box in web frontend
    • statePlaylists 1|0 => show Playlists DropDown box in web frontend
    • image_size
    • player mpd|mopidy|forked-daapd => which player is controlled by the module

    Readings
      all MPD internal values
=end html =begin html_DE

MPD

    FHEM Modul zur Steuerung des MPD (oder Mopidy) ähnlich dem MPC (MPC = Music Player Command, das Kommando Zeilen Interface für den Music Player Daemon ) (englisch)
    Um den MPD auf einem Raspberry Pi zu installieren finden sich im Internet zahlreiche gute Dokumentaionen z.B. hier
    Thread im FHEM Forum : Modul für MPD
    Das Modul benötigt zwingend XML:Simple, installation z.B. mit sudo apt-get install libxml-simple-perl
    Define
      define <name> MPD <IP MPD Server | default localhost> <Port MPD Server | default 6600>
      Beispiel :
            define myMPD MPD 192.168.0.99 7000
            
        wenn FHEM und der MPD auf dem gleichen PC laufen :
            define myMPD MPD
            

    Set
      set <name> <was>
       
      z.Z. unterstützte Kommandos
       
      play => spielt den aktuellen Titel der MPD internen Playliste
      clear => löscht die MPD interne Playliste
      stop => stoppt die Wiedergabe
      pause => Pause an/aus
      previous => spielt den vorherigen Titel in der Playliste
      next => spielt den nächsten Titel in der Playliste
      random => zufällige Wiedergabe an/aus
      repeat => Wiederholung an/aus
      toggle => wechselt von play nach stop bzw. stop/pause nach play
      volume (%) => ändert die Lautstärke von 0 - 100%
      volumeUp => Lautstärke schrittweise erhöhen , Schrittweite = ( attr volumeStep size )
      volumeDown => Lautstärke schrittweise erniedrigen , Schrittweite = ( attr volumeStep size )
      playlist (playlist name) => lade Playliste aus der MPD Datenbank und starte Wiedergabe mit dem ersten Titel
      playfile (file) => erzeugt eine MPD interne Playliste mit file als Inhalt und spielt dieses ab
      updateDb => wie MPC update, Update der MPD Datenbank
      reset => reset des FHEM MPD Moduls
      mpdCMD (cmd) => sende cmd direkt zum MPD Server ( siehe auch MPD Comm Ref )
      IdleNow => sendet das Kommando idle zum MPD und wartet auf Ereignisse
      clear_readings => löscht sehr viele Readings

    Get
      get <name> <was>
       
      z.Z. unterstützte Kommandos
      music => zeigt alle Dateien der MPD Datenbank
      playlists => zeigt alle Playlisten der MPD Datenbank
      playlistsinfo => zeigt Informationen der aktuellen Playliste
      webrc => HTML Ausgabe einer einfachen Web Fernbedienung Bsp :.
            define <name> weblink htmlCode {fhem("get <name> webrc", 1)}
            attr <name> room MPD
          
      statusRequest => hole aktuellen MPD Status
      currentsong => zeigt Informationen zum aktuellen Titel der MPD internen Playliste
      outputs => zeigt Informationen der definierten MPD Ausgabe Kanäle ( aus /etc/mpd.conf )

    Attribute
    • password => Password falls in der mpd.conf definiert
    • loadMusic 1|0 => lade die MPD Titel beim FHEM Start : mpd.conf - music_directory
    • loadPlaylists 1|0 => lade die MPD Playlisten beim FHEM Start : mpd.conf - playlist_directory
    • volumeStep x => Schrittweite für Volume +/-
    • titleSplit 1|0 => zerlegt die aktuelle Titelangabe am ersten Vorkommen von - (BlankMinusBlank) in die zwei Felder Artist und Titel,
      wenn im abgespielten Titel die Interpreten Information nicht verfügbar ist (sehr oft bei Radio-Streams default 1)
      Liegen keine Titelangaben vor wird die Ausgabe durch den Namen der Radiostation ersetzt
    • timeout (default 1) => Timeoutwert in Sekunden für die Verbindung fhem-mpd
    • waits (default 60) => Überwachungszeit in Sekunden für den Idle Prozess. In Verbindung mit refresh_song der Aktualisierungs Intervall für die aktuellen Songparamter,
      (z.B. um den Fortschrittsbalken bei TabletUI aktuell zu halten)
    • stateMusic 1|0 => zeige Musikliste als DropDown im Webfrontend
    • statePlaylists 1|0 => zeige Playlisten als DropDown im Webfrontend
    • player mpd|mopidy|forked-daapd (default mpd) => welcher Player wird gesteuert
      ACHTUNG : Mopidy unterstützt nicht alle Kommandos des echten MPD ! (siehe Mopidy Dokumentation)
    • Cover Art Funktionen von last.fm :
    • image_size -1|0|1|2|3 (default -1 = keine Interpretenbilder und Infos von last.fm verwenden)
      last.fm stellt verschiedene Bildgroessen zur Verfügung :
      0 = 32x32 , 1 = 64x64 , 2 = 174x174 , 3 = 300x300
    • artist_content 0|1 => stellt Interpreteninformation im Reading artist_content zur Verfügung
    • artist_summary 0|1 => stellt weitere Interpreteninformation im Reading artist_summary zur Verfügung
      Beispiel Anzeige mittels readingsGroup :
            define rg_artist <MPD name>:artist,artist_image_html,artist_summary
            attr rg_artist room MPD
          
    • cache (default lfm => /fhem/www/lfm) Zwischenspeicher für die XML und PNG Dateien
      Wichtig : Der User unter dem der fhem Prozess ausgeführt wird (default fhem) muss Lese und Schreibrechte in diesem Verzeichniss haben !
      Das Verzeichnis sollte auch unterhalb von www liegen, damit der fhem Webserver direkten Zugriff auf die Bilder hat.

    Readings
      - alle MPD internen Werte
      - vom Modul direkt errzeugte Readings :
      playlistinfo : (TabletUI Medialist)
      playlistcollection : (TabletUI)
      playlistname : (TabletUI)
      artist_image : (bei Nutzung von Last.fm)
      artist_image_html : (bei Nutzung von Last.fm)
      artist_content : (bei Nutzung von Last.fm)
      artist_summary : (bei Nutzung von Last.fm)
=end html_DE =cut