diff --git a/FHEM/73_MPD.pm b/FHEM/73_MPD.pm index 27f302be4..93284b2ff 100644 --- a/FHEM/73_MPD.pm +++ b/FHEM/73_MPD.pm @@ -5,7 +5,7 @@ # (c) 2014 Copyright: Wzut # All rights reserved # -# FHEM Forum : http://forum.fhem.de/index.php/topic,18517.0.html +# 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 @@ -21,14 +21,17 @@ # GNU General Public License for more details. ################################################################ +# Version 1.3 - 28.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 +# Version 1.0 - 21.02.14 # add german doc , readings & state times only on change, devStateIcon -# Version 0.95 - 17.02.14 +# Version 0.95 - 17.02.14 # add command set IdleNow -# Version 0.9 - 15.02.14 -# Version 0.8 - 01.02.14 , first version +# Version 0.9 - 15.02.14 +# Version 0.8 - 01.02.14 , first version package main; @@ -41,162 +44,156 @@ use POSIX; use Blocking; # http://www.fhemwiki.de/wiki/Blocking_Call use IO::Socket; use Getopt::Std; - -sub AttrVal($$$); +use HttpUtils; +use XML::Simple qw(:strict); +use HTML::Entities; sub MPD_html($); my %gets = ( - "music:noArg" => "", - "playlists:noArg" => "", - "playlistinfo:noArg" => "", - "webrc:noArg" => "", - "statusRequest:noArg" => "", - "mpdCMD" => "", - "currentsong:noArg" => "", - "outputs:noArg" => "" -); + "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" => "", - "repaet:noArg" => "", - "volume:slider,0,1,100" => "", - "volumeUp:noArg" => "", - "volumeDown:noArg" => "", - "playlist" => "", - "playfile" => "", - "updateDb:noArg" => "", - "interval" => "", - "mpdCMD" => "", - "reset:noArg" => "", - "IdleNow:noArg" => "", - "toggle:noArg" => "" - - ); + "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" => "", + ); -my %readings = ( - "title" => "", - "name" => "", - "playlist" => "", - "file" => "", - "artist" => "", - "album" => "", - "songid" => "", - "nextsong" => "", - "nextsongid" => "", - "song" => "", - "playlistlength" => "", - "xfade" => "", - "mixrampdelay" => "", - "mixrampdb" => "", - "consume" => "", - "elapsed" => "", - "time" => "", - "audio" => "", - "bitrate" => "", - "pos" => "", - "id" => "", - "date" => "", - "genre" => "", - "track" => "", - "state" => "", - "last-modified" => "", - "repeat" => "", - "ramdom" => "", - "single" => "", - "volume" => "", - "error" => "", - "random" => "" -); +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}; + 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->{AttrList} = "useIdle:0,1 interval password loadMusic:0,1 loadPlaylists:0,1 volumeStep:1,2,5,10 titleSplit:1,0 lcdDevice ".$readingFnAttributes; - $hash->{FW_summaryFn} = "MPD_summaryFn"; - + $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 + # 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}; - - $hash->{INTERVAL} = AttrVal($name, "interval", 30); + my ($hash) = @_; + my $name = $hash->{NAME}; - $hash->{".htmlCode"} = ""; - $hash->{".playlist"} = ""; - $hash->{".music"} = ""; - $hash->{".outputs"} = ""; - $hash->{".lasterror"} = ""; - $hash->{".lcd"} = AttrVal($name, "lcdDevice", undef); - $hash->{PRESENT} = 0; - $hash->{VOLUME} = -1; - - ## kommen wir via reset Kommando ? - if ($hash->{".reset"}) - { - $hash->{".reset"} = 0; - RemoveInternalTimer($hash); - if(defined($hash->{helper}{RUNNING_PID})) - { - BlockingKill($hash->{helper}{RUNNING_PID}); - Log 3 , "$name Idle Kill PID : ".$hash->{helper}{RUNNING_PID}{pid}; - delete $hash->{helper}{RUNNING_PID}; - $hash->{IPID} = ""; - Log 3 ,"$name Reset done"; - } - } + if (!$init_done) + { + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+5,"MPD_updateConfig", $hash, 0); + return; + } - my $error = mpd_cmd($hash, "status"); + 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"; + } + } - # Playlisten und Dateien laden ? + 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); + } - if (AttrVal($name, "loadMusic", 0) && !$error) - { $error = mpd_cmd($hash, "i|lsinfo|music"); - Log 3 ,"$name could not load music -> $error" if ($error); - } - if (AttrVal($name, "loadPlaylists", 0) && !$error) - { - $error = mpd_cmd($hash, "i|lsinfo|playlists"); - Log 3 ,"$name could not load playlists -> $error" if ($error); - } - - if (!$error) - { - - $hash->{".outputs"} = mpd_cmd($hash, "i|outputs|x"); - refresh_outputs($hash); + } + else { readingsSingleUpdate($hash,"state","error",1);} - readingsSingleUpdate($hash,"state","Initialized",1); - - } - else { readingsSingleUpdate($hash,"state","not conected",1); } - - - InternalTimer(gettimeofday()+$hash->{INTERVAL}, "MPD_GetUpdate", $hash, 0) if ($hash->{INTERVAL}); - - readingsSingleUpdate($hash,"error",$error,1) if ($error); - -return undef; + return undef; } sub MPD_Define($$) @@ -205,17 +202,23 @@ sub MPD_Define($$) my $name = $hash->{NAME}; my @a = split("[ \t][ \t]*", $def); - return "Usage: define $name " if(int(@a) > 4); + return "Usage: define $name [] []" if(int(@a) > 4); - $hash->{HOST} = (defined($a[2])) ? $a[2] : "localhost"; + $hash->{HOST} = (defined($a[2])) ? $a[2] : "127.0.0.1"; $hash->{PORT} = (defined($a[3])) ? $a[3] : "6600" ; + $hash->{".reset"} = 0; - Log 3, "MPD: Device $name defined."; + 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' unless (exists($attr{$name}{devStateIcon})); + $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); @@ -223,9 +226,10 @@ sub MPD_Define($$) return undef; } -sub MPD_Undef ($$) { +sub MPD_Undef ($$) +{ - my ($hash, $arg) = @_; + my ($hash, $arg) = @_; RemoveInternalTimer($hash); if(defined($hash->{helper}{RUNNING_PID})) @@ -233,52 +237,110 @@ sub MPD_Undef ($$) { BlockingKill($hash->{helper}{RUNNING_PID}); } - return undef; + return undef; } -sub MPD_GetUpdate($) { - my ($hash) = @_; - my $name = $hash->{NAME}; - my $lasterror = $hash->{".lasterror"}; +sub MPD_Attr (@) +{ - my $error = mpd_cmd($hash, "status"); + my ($cmd, $name, $attrName, $attrVal) = @_; + my $hash = $defs{$name}; - if (!$hash->{PRESENT} && $hash->{INTERVAL} eq "0") {$hash->{INTERVAL} = 300;} # neues Intvall falls keine Verbindung zum MPD Server - - if (!$error && (($hash->{STATE} eq "play") || $hash->{READINGS}{"playlistlength"}{VAL})) - { - $error = mpd_cmd($hash, "currentsong"); - } - - readingsBeginUpdate($hash); - #readingsBulkUpdate($hash, "state", $hash->{STATE}); - readingsBulkUpdate($hash, "error", $error) if ($error); - readingsEndUpdate($hash, 1); - - if ($error && ($error ne $lasterror)) + if ($cmd eq "set") { - Log 3 , "$name, $error"; - $hash->{".lasterror"} = $error; - } - + 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; + } - InternalTimer(gettimeofday()+$hash->{INTERVAL}, "MPD_GetUpdate", $hash, 0) if ($hash->{INTERVAL}); - my_lcd($hash) if (defined($hash->{".lcd"})); - - # erster Start bzw neuer Versuch nach Fehler ? - if (AttrVal($name, "useIdle", 0) && !$error) + } + elsif ($cmd eq "del") { - $hash->{helper}{RUNNING_PID} = BlockingCall("MPD_IdleStart", $name."|".$hash->{HOST}."|".$hash->{PORT}, "MPD_IdleDone", $hash) unless(exists($hash->{helper}{RUNNING_PID})); - $hash->{IPID} = $hash->{helper}{RUNNING_PID}; - Log 4 , "$name IdleStart with PID : ".$hash->{helper}{RUNNING_PID}{pid} if($hash->{helper}{RUNNING_PID}); + 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; - + 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,"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($@) { @@ -287,79 +349,99 @@ sub MPD_Set($@) 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") { $ret = MPD_Get($hash, @a); } # pseudo Kommando , gleich mit get mpdCMD + + 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 if ($subcmd eq '---'); # erster Eintrag im select Feld ignorieren - - RemoveInternalTimer($hash); + 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_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, "pause"); } - if ($cmd eq "stop") { $ret = mpd_cmd($hash, "stop"); } - if ($cmd eq "update") { $ret = mpd_cmd($hash, "update"); } + 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, "play") if (($hash->{STATE} eq "stop") || ($hash->{STATE} eq "pause")); - $ret = mpd_cmd($hash, "stop") if ($hash->{STATE} eq "play"); + $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) { - $hash->{READINGS}{"title"}{VAL} = ""; - $hash->{READINGS}{"artist"}{VAL} = ""; - $ret = mpd_cmd($hash, "previous"); - mpd_cmd($hash, "currentsong") if (!$ret); - } else { return; } + 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}) { - $hash->{READINGS}{"title"}{VAL} = ""; - $hash->{READINGS}{"artist"}{VAL} = ""; - $ret = mpd_cmd($hash, "next"); - mpd_cmd($hash, "currentsong") if (!$ret); - } else { return; } + MPD_ClearReadings($hash); + $ret = mpd_cmd($hash, clb."next\n".cle); + } + else { return undef; } } if ($cmd eq "random") { - my $rand = ($hash->{RANDOM}) ? "0" : "1"; - $ret = mpd_cmd($hash, "random $rand"); + my $rand = ($hash->{READINGS}{random}{VAL}) ? "0" : "1"; + $ret = mpd_cmd($hash, clb."random $rand\n".cle); } if ($cmd eq "repeat") { - my $rep = ($hash->{REPEAT}) ? "0" : "1"; - $ret = mpd_cmd($hash, "repeat $rep"); + 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") { - $hash->{READINGS}{"title"}{VAL} = ""; - $hash->{READINGS}{"artist"}{VAL} = ""; - $ret = mpd_cmd($hash, "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; } @@ -370,331 +452,479 @@ sub MPD_Set($@) 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, "setvol $vol_new"); } - + if (defined($vol_new)) + { + $ret = mpd_cmd($hash, clb."setvol $vol_new\n".cle); + } # einfaches Play bzw Play Listenplatz Nr. ? if ($cmd eq "play") { - $ret = mpd_cmd($hash, "play $subcmd"); - if (!$ret) { $ret = mpd_cmd($hash, "currentsong"); } - } - - - if ($cmd eq "interval") - { - return "$name: Set with short interval, must be 0 or greater" if(int($a[2]) < 0); - # update timer - RemoveInternalTimer($hash); - $hash->{INTERVAL} = $a[2]; - InternalTimer(gettimeofday()+$hash->{INTERVAL}, "MPD_GetUpdate", $hash, 0) if ($hash->{INTERVAL}); - return undef; + MPD_ClearReadings($hash); + $ret = mpd_cmd($hash,clb."play $subcmd\n".cle); } if ($cmd eq "IdleNow") { - return "$name: sorry, one Idle process is always running with pid ".$hash->{helper}{RUNNING_PID}{pid} if($hash->{helper}{RUNNING_PID}); - $hash->{helper}{RUNNING_PID} = BlockingCall("MPD_IdleStart", $name."|".$hash->{HOST}."|".$hash->{PORT}, "MPD_IdleDone", $hash) unless(exists($hash->{helper}{RUNNING_PID})); - if($hash->{helper}{RUNNING_PID}) - { - $hash->{IPID} = $hash->{helper}{RUNNING_PID}{pid}; - return "$name: idle process started with PID : ".$hash->{IPID}; } - else { return "$name: idle process start failed !"; } + 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; + # die ersten beiden brauchen wir nicht mehr + shift @a; + shift @a; - # den Rest als ein String - $subcmd = join(" ",@a); + # den Rest als ein String + $subcmd = join(" ",@a); if ($cmd eq "playlist") { return "$name : no name !" if (!$subcmd); - - $hash->{READINGS}{"title"}{VAL} = ""; - $hash->{READINGS}{"artist"}{VAL} = ""; - $hash->{".music"} = ""; - $hash->{".playlist"} = $subcmd; # interne PL Verwaltung + MPD_ClearReadings($hash); - $ret = mpd_cmd($hash, "stop|clear|load $subcmd|play"); - # kein Fehler, dann noch die Song Infos holen - if (!$ret) { $ret = mpd_cmd($hash, "currentsong"); } + $hash->{".music"} = ""; + $hash->{".playlist"} = $subcmd; # interne Playlisten Verwaltung + + $ret = mpd_cmd($hash, clb."stop\nclear\nload \"$subcmd\"\nplay\n".cle); } if ($cmd eq "playfile") { - return "$name : no File !" if (!$subcmd); + return "$name, no File !" if (!$subcmd); - $hash->{READINGS}{"title"}{VAL} = ""; - $hash->{READINGS}{"artist"}{VAL} = ""; + MPD_ClearReadings($hash); - $hash->{".playlist"} = "", - $hash->{".music"} = $subcmd; # interne Song Verwaltung + $hash->{".playlist"} = ""; + $hash->{".music"} = $subcmd; # interne Song Verwaltung - $ret = mpd_cmd($hash, "clear|command_list_begin\nadd \"$subcmd\"\ncommand_list_end|play"); - # kein Fehler, dann noch die Song Infos holen - if (!$ret) { $ret = mpd_cmd($hash, "currentsong"); } + $ret = mpd_cmd($hash, clb."stop\nclear\nadd \"$subcmd\"\nplay\n".cle); } - mpd_cmd($hash, "status"); - - # readingsSingleUpdate($hash,"error",$ret,1) if($ret); ToDo : warum ist error manchmal = "0" ?? + 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; + } - InternalTimer(gettimeofday()+$hash->{INTERVAL}, "MPD_GetUpdate", $hash, 0) if ($hash->{INTERVAL}); - my_lcd($hash) if (defined($hash->{".lcd"})); - -return $ret; + 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); - my $cmd = $a[1]; + $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 "webrc") { return(MPD_html($hash)); } - if ($cmd eq "playlists") { $hash->{".playlists"} = ""; - mpd_cmd($hash, "i|lsinfo|playlists"); - return $name." playlists:\n".$hash->{".playlists"}; } + mpd_cmd($hash, "i|lsinfo|playlists"); + return format_get_output("Playlists",$hash->{".playlists"}); + + } if ($cmd eq "music") { - $hash->{".music"} = ""; - mpd_cmd($hash, "i|lsinfo|music"); - return $name." music:\n".$hash->{".music"}; } - - if ($cmd eq "statusRequest") - { $ret = mpd_cmd($hash, "status"); - if (!$ret && $hash->{READINGS}{"playlistlength"}{VAL}) { $ret = mpd_cmd($hash, "currentsong"); } - if ($ret) { return $ret; } else { return $name." statusRequest:\n".mpd_cmd($hash, "i|status|x"); } + 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 "currentsong") - { mpd_cmd($hash, "currentsong"); - return $name." currentsong:\n".mpd_cmd($hash, "i|currentsong|x"); } + 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") - { $hash->{".outputs"} = mpd_cmd($hash, "i|outputs|x"); - refresh_outputs($hash); - return $name." outsputs:\n".$hash->{".outputs"}; } - - - if ($cmd eq "mpdCMD") { - my $sub; - shift @a; - shift @a; - - $sub = join (" ", @a); - - return $name." ".$sub.":\n".mpd_cmd($hash, "i|$sub|x"); + { + MPD_Outputs_Status($hash); + return format_get_output("Outputs", $hash->{".outputs"}); } - if ($cmd eq "playlistinfo") - { - return $name." playlistinfo:\n".mpd_cmd($hash, "i|playlistinfo|x"); - } - - - return "$name get with unknown argument $cmd, choose one of " . join(" ", sort keys %gets); + 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 my_lcd($) +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 $error = $hash->{READINGS}{"error"}{VAL} ne "" ? $hash->{READINGS}{"error"}{VAL}. " ".$hash->{READINGS}{"error"}{TIME} : ""; - if (($hash->{STATE} eq "play") || ($hash->{STATE} eq "pause")) + 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 $artist = defined($hash->{READINGS}{"artist"}{VAL}) ? $hash->{READINGS}{"artist"}{VAL} : ""; - my $name = defined($hash->{READINGS}{"name"}{VAL}) ? $hash->{READINGS}{"name"}{VAL} : $hash->{".playlist"}; - my $state = $hash->{STATE} eq "pause" ? "P" : "*"; - my $repeat = $hash->{REPEAT} ? "R" : "-"; - my $title = defined($hash->{READINGS}{"title"}{VAL}) ? $hash->{READINGS}{"title"}{VAL} : ""; - my $song = defined($hash->{READINGS}{"song"}{VAL}) ? $hash->{READINGS}{"song"}{VAL}+1 : "--"; - my $len = defined($hash->{READINGS}{"playlistlength"}{VAL}) ? $hash->{READINGS}{"playlistlength"}{VAL} : "--"; - - fhem("set ".$hash->{".lcd"}." writeXY 0,0,20,l $artist"); - fhem("set ".$hash->{".lcd"}." writeXY 0,1,20,l $title"); - fhem("set ".$hash->{".lcd"}." writeXY 0,2,20,l $name"); - fhem("set ".$hash->{".lcd"}." writeXY 0,3,3,r ".$hash->{VOLUME}); - fhem("set ".$hash->{".lcd"}." writeXY 5,3,9,l $song/$len $state $repeat"); - } - else - { - fhem("set ".$hash->{".lcd"}." text $error"); $hash->{READINGS}{"error"}{VAL}=""; - } - - if (($hash->{STATE} eq "play") || ($hash->{STATE} eq "pause") || $error) - {fhem("set ".$hash->{".lcd"}." backlight on"); } else {fhem("set ".$hash->{".lcd"}." backlight off"); } - # Log 4 ,"mylcd"; + 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 $output = ""; + my $sp; + my $artist; my $name = $hash->{NAME}; - my $old_state = $hash->{STATE}; # save state - $hash->{VERSION} = undef; - $hash->{PRESENT} = 0; - $hash->{STATE} = "not connected"; + $hash->{VERSION} = undef; + $hash->{PRESENCE} = "absent"; - my $iaddr = inet_aton($hash->{HOST}) || return "no host: ".$hash->{HOST}; - my $paddr = sockaddr_in($hash->{PORT}, $iaddr); - my $proto = getprotobyname('tcp'); + my $sock = IO::Socket::INET->new( + PeerHost => $hash->{HOST}, + PeerPort => $hash->{PORT}, + Proto => 'tcp', + Timeout => $hash->{TIMEOUT}); - socket(SOCK, PF_INET, SOCK_STREAM, $proto) || return "socket: $!"; - connect(SOCK, $paddr) || return "connect: $!"; - select(SOCK); $| = 1; - while () # MPD rede mit mir , egal was ;) - { - last if $_ ; # end of output. + 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->{PRESENT} = 1; - - my ($b , $c) = split("OK " , $_); + $hash->{PRESENCE} = "present"; + + my ($b , $c) = split("OK MPD " , $_); $hash->{VERSION} = $c; # ok, now we're connected - let's issue the commands. - # old state back - $hash->{STATE} = $old_state; + 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"; - while () + print $sock "$cmd\r\n"; + Log3 $name, 5 , "$name, send-> $cmd"; + + while (<$sock>) { chomp $_; - return "mpd_Msg ACK ERROR ".$_ if $_ =~ s/^ACK //; # oops - error. - last if $_ =~ /^OK/; # end of output. - ($b , $c) = split(": " , $_); - - $b = lc($b); + return "MPD_Msg ACK ERROR ".$_ if $_ =~ s/^ACK //; # oops - error. + last if $_ =~ /^OK/; # end of output. + Log3 $name, 5 , "$name, rec: ".$_; - if ($b && defined($readings{$b})) # ist das ein Internals oder Reading ? + ($b , $c) = split(": " , $_); + + if ($b && defined($c)) # ist das ein Reading ? { - # if ($b eq "state") { $hash->{STATE} = $c; } # Sonderfall state - if ($b eq "volume") { $hash->{VOLUME} = $c; } # Sonderfall volume + $b = lc($b); + if ($b eq "volume") { $hash->{".volume"} = $c; } # Sonderfall volume + + $artist = $c if ($b eq "artist"); + if ($b eq "title") { - my $sp = index($c, " - "); - if (AttrVal($name, "titleSplit", 1) && ($sp>0)) # wer nicht mag solls eben abschaltten + $sp = index($c, " - "); + if (AttrVal($name, "titleSplit", 1) && ($sp>0)) # wer nicht mag solls eben abschalten { - readingsSingleUpdate($hash,"artist",substr($c,0,$sp),1); - readingsSingleUpdate($hash,"title",substr($c,$sp+3),1); - } else { readingsSingleUpdate($hash,"title",$c,1); } # kein Titel Split - - } - elsif ($b eq "state") - { readingsSingleUpdate($hash,"state",$c,1) if ($c ne $hash->{STATE}); } - else { readingsSingleUpdate($hash,$b,$c,1) if ($c ne defined($hash->{READINGS}{$b}{VAL}));} # irgendwas aber kein Titel oder State - } - else { $hash->{uc($b)} = $c; } # Internal - + $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); + #Log3 $name, 4 , "$name, BU -> $b,$c"; + } # irgendwas aber kein Titel + } # defined $c } # while } # foreach -} # end Ausgabe nach Readings oder Internals , ab jetzt Bildschirmausgabe + + 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"; - while () + print $sock $commands[1]."\r\n"; + my $d; + while (<$sock>) { - # chomp $_; - lassen wir das \n ersteinmal noch dran return "mpd_Msg ACK ERROR ".$_ if $_ =~ s/^ACK //; # oops - error. - last if $_ =~ /^OK/; # end of output. - ($b , $c) = split(": " , $_); + 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->{".music"} .= $c; } # Titelliste füllen + 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 - elsif (($b eq "directory" ) && ($commands[2] eq "dir")) {$hash->{".dir"} .= $c; } # Dir liste füllen - elsif ($commands[2] eq "x") { $output .= $_; } + 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) || return "socketclose: $!"; + print $sock "close\n"; + close($sock); return $output; # falls es einen gibt , wenn nicht - auch gut ;) -} # end msg - +} # end mpd_msg sub MPD_IdleStart($) { - my ($string) = @_; - return unless(defined($string)); + my ($name) = @_; + return unless(defined($name)); - my @a = split("\\|" ,$string); + my $hash = $defs{$name}; + my $old_event = ""; + my $telnetPort = undef; + my $output; - my $output = $a[0]; # Name - my $host = $a[1]; - my $port = $a[2]; - my $old_event = ""; + # Suche das Telnet Device ohne Passwort + # Code geklaut aus Blocking.pm :) - my $iaddr = inet_aton($host) || return $output."|E no host: $host"; - my $paddr = sockaddr_in($port, $iaddr); - my $proto = getprotobyname('tcp'); - - socket(SOCK, PF_INET, SOCK_STREAM, $proto) || return $output."|E socket: $!"; - connect(SOCK, $paddr) || return $output."|E connect: $!"; - select(SOCK); $| = 1; - while () - { - last if $_ ; + 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 $output."|E not a valid mpd server, welcome string was: ".$_ if $_ !~ /^OK MPD (.+)$/; + return $name."|not a valid mpd server, welcome string was: ".$_ if $_ !~ /^OK MPD (.+)$/; - print SOCK "idle\r\n"; - while () - { + 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 $_; - return $output."|E mpd_Msg ACK ERROR ".$_ if $_ =~ s/^ACK //; # oops - error. - last if $_ =~ /^OK/; # es hat sich was getan. - if ($_ ne $old_event) { $output .= "|".$_; $old_event = $_; } - } - print SOCK "close\n"; - close(SOCK); + if ($_ =~ s/^ACK //) # oops - error. + { + print $sock "close\n"; + close($sock); + return $name."|ACK ERROR : ".$_; + } + + $_ =~s/changed: //g; - return $output; + 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($) @@ -702,73 +932,304 @@ sub MPD_IdleDone($) my ($string) = @_; return unless(defined($string)); - my ($h,$ret) = split("\\|",$string); - my $hash = $defs{$h}; + my @r = split("\\|",$string); + my $hash = $defs{$r[0]}; + my $ret = (defined($r[1])) ? $r[1] : "unknow error"; my $name = $hash->{NAME}; - Log 4 , "$name IdleDone PID : ".$hash->{helper}{RUNNING_PID}{pid}; + Log3 $name, 5,"$name, IdleDone -> $string"; + delete($hash->{helper}{RUNNING_PID}); - $hash->{IPID} = ""; + delete $hash->{IPID}; - return if($hash->{helper}{DISABLED}); - - if (substr($ret,0,1) ne "E") - { - # ToDO , den $ret noch nach Typen aufdröseln, jetzt muss ersteinmal status und currentsong reichen - my $error = mpd_cmd($hash, "status"); - if (!$error && (($hash->{STATE} eq "play") || $hash->{READINGS}{"playlistlength"}{VAL})) { $error = mpd_cmd($hash, "currentsong");} - - readingsBeginUpdate($hash); - #readingsBulkUpdate($hash, "state", $hash->{STATE}); - readingsBulkUpdate($hash, "error", $error) if ($error); - readingsBulkUpdate($hash, "mpd_event", $ret); - readingsEndUpdate($hash, 1); - - # weiter auf Events warten ? oder wurden inzwischen vllt. via attr geändert ? - if (AttrVal($name, "useIdle", 0) && !$error) - { - $hash->{helper}{RUNNING_PID} = BlockingCall("MPD_IdleStart", $name."|".$hash->{HOST}."|".$hash->{PORT}, "MPD_IdleDone", $hash) unless(exists($hash->{helper}{RUNNING_PID})); - Log 4 , "$name Idle has new PID : ".$hash->{helper}{RUNNING_PID}{pid} if (exists($hash->{helper}{RUNNING_PID})); - $hash->{IPID} = $hash->{helper}{RUNNING_PID}{pid} if (exists($hash->{helper}{RUNNING_PID})); - } + readingsBeginUpdate($hash); + readingsBulkUpdate($hash,"state","error"); + readingsBulkUpdate($hash,"error",$ret); + readingsBulkUpdate($hash,"presence","absent"); + readingsEndUpdate($hash, 1 ); - Log 3 , "$name, $error" if ($error); - my_lcd($hash) if (defined($hash->{".lcd"})); - return; - } + Log3 $name, 3 , "$name, idle error -> $ret"; + return if(IsDisabled($name)); - readingsSingleUpdate($hash,"mpd_event",$ret,1); - Log 3 , "$name MPD idle comes back with error : $ret , MPD Idle is now disabled use reset cmd to restart"; - my_lcd($hash) if (defined($hash->{".lcd"})); + RemoveInternalTimer($hash); + InternalTimer(gettimeofday()+AttrVal($name, "waits", 60), "MPD_try_idle", $hash, 0); + return; } -############################################### - -sub refresh_outputs($) +sub MPD_try_idle($) { - my ($hash) = @_; - my @outp = split("\n" , $hash->{".outputs"}); - my $oname; - my $oen; + my ($hash) = @_; + my $name = $hash->{NAME}; + my $waits = AttrVal($name, "waits", 60); - foreach (@outp) - { - my @val = split(": " , $_); - $oname = $val[1] if ($val[0] eq "outputname"); - if ($val[0] eq "outputenabled") { $oen = ($val[1] eq "1") ? "on" : "off"}; - if ($oen && $oname) {$hash->{uc $oname} = $oen; $oname = ""; $oen = "";} - } + $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); + } + else { Log3 $name, 5 , $name.", idle PID ".$hash->{IPID}." found"; } + + 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 : $!"; + } + 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"; + } + else + { + if (!open (FILE , ">"."www/$cache/".$hash->{'.artist'}.".xml")) + { + Log3 $name, 2, "$name, error saving ".$hash->{'.artist'}.".xml : $!"; + $hash->{'.artist'} = ""; + } + else + { + print FILE $data; + close(FILE); + } + } + } + + 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) # 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")) + { + MPD_artist_image($hash,"/fhem/icons/10px-kreis-rot"," "); + Log3 $name, 2, "$name, error saving image $fname : $!"; + 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_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 $volume = (defined($hash->{".volume"})) ? $hash->{".volume"} : "???"; my $len = (defined($hash->{READINGS}{"playlistlength"}{VAL})) ? $hash->{READINGS}{"playlistlength"}{VAL} : "--"; my $html; my @list; @@ -788,7 +1249,7 @@ sub MPD_html($) { $html .= ""; $html .= ""; - @list = split ("\n",$playlists); + @list = sort(split ("\n",$playlists)); foreach (@list) { $sel = ($_ eq $playlist) ? " selected" : ""; @@ -797,11 +1258,11 @@ sub MPD_html($) { $html .= ""; } - if ($music) { + if ($musiclist) { $html .= "on "; - $osel = !$oen ? "checked" : ""; - $html .="off "; + $html .="$oid.$oname "; + $osel = ($oen eq "1") ? "checked" : ""; + $html .="on "; + $osel = ($oen ne "1") ? "checked" : ""; + $html .="off"; $html .=""; - $oen=undef; + $oen = ""; } - } + } } $html .= ""; @@ -872,44 +1333,88 @@ sub MPD_summaryFn($$$$) { 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 $title = defined($hash->{READINGS}{"title"}{VAL}) ? $hash->{READINGS}{"title"}{VAL} : " "; - my $artist = defined($hash->{READINGS}{"artist"}{VAL})? $hash->{READINGS}{"artist"}{VAL}."
" : " "; - my $rname = defined($hash->{READINGS}{"name"}{VAL}) ? $hash->{READINGS}{"name"}{VAL} : " "; + 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}." 
" : ""; } - if (!$title && !$artist) { $title = $rname; } # besser das als nix $html ="
$txt"; - if ($playlists) + if (($playlists) && $hash->{".sPlayL"}) { $html .= ""; + $html .= "
"; + } + + if (($musiclist) && $hash->{".sMusicL"}) + { + $html .= ""; } - $html .= (($state eq "play") || ($state eq "pause")) ? $artist.$title : " "; - $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 Music Player Deamon (MPD) +=item summary_DE steuert Music Player Deamon (MPD) =begin html @@ -945,7 +1450,7 @@ FHEM Forum : Modul f 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
- repaet => like MPC repeat, 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
@@ -954,9 +1459,8 @@ FHEM Forum :
Modul f 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
- interval => set polling interval of MPD server, overwrites attr interval temp , use 0 to disable polling
reset => reset MPD Modul
- mpdCMD => same as GET mpdCMD
+ mpdCMD (cmd) => send a command to MPD Server (
MPD Command Ref )

@@ -973,7 +1477,6 @@ FHEM Forum : Modul f attr <name> room MPD statusRequest => get MPD status
- mpdCMD (cmd) => send a command to MPD Server (
MPD Command Ref )
currentsong => get infos from current song in playlist
outputs => get name,id,status about all MPD output devices in /etc/mpd.conf
@@ -981,13 +1484,18 @@ FHEM Forum : Modul f Attributes
    -
  • interval = polling interval at MPD server, use 0 to disable polling (default 30)
  • -
  • password (not ready yet) if password on MPD server is set
  • -
  • loadMusic 0|1 = load titles from MPD database at startup
  • -
  • loadPlaylists 0|1 = load playlist names from MPD database at startup
  • -
  • volumeStep 1|2|5|10 = Step size for Volume +/- (default 5)
  • -
  • useIdle 0|1 = send Idle command to MPD and wait for MPD events needs MPD Version 0.16.0 or greater
  • -
  • titleSplit 1|0 = split title to artist and title if no artist is given in songinfo (e.g. radio-stream)
  • +
  • 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 @@ -1029,24 +1537,23 @@ FHEM Forum : Modul f
 
z.Z. unterstützte Kommandos
 
- play => spielt den aktuellen Titel der geladenen Playliste
- clear => löscht die Playliste
+ 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
- repaet => Wiederholung 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 temoräre Playliste mit file und spielt dieses ab
+ playfile (file) => erzeugt eine MPD interne Playliste mit file als Inhalt und spielt dieses ab
updateDb => wie MPC update, Update der MPD Datenbank
- interval => in Sekunden bis neue aktuelle Informationen vom MPD geholt werden. Überschreibt die Einstellung von attr interval Ein Wert von 0 deaktiviert diese Funktion
reset => reset des FHEM MPD Moduls
- mpdCMD => gleiche Funktion wie get mpdCMD
+ mpdCMD (cmd) => sende cmd direkt zum MPD Server ( siehe auch
MPD Comm Ref )
IdleNow => sendet das Kommando idle zum MPD und wartet auf Ereignisse - siehe auch Attribut useIdle

@@ -1064,26 +1571,28 @@ FHEM Forum : Modul f attr <name> room MPD statusRequest => hole aktuellen MPD Status
- mpdCMD (cmd) => sende cmd direkt zum MPD Server ( siehe auch
MPD Comm Ref )
- currentsong => zeigt Informationen zum aktuellen Titel in der Playliste
+ currentsong => zeigt Informationen zum aktuellen Titel der MPD internen Playliste
outputs => zeigt Informationen der definierten MPD Ausgabe Kanäle ( aus /etc/mpd.conf )

Attribute
    -
  • interval 0..x => polling Interval des MPD Servers, 0 zum abschalten oder in Verbindung mit useIdle
  • -
  • password => (z.Z. nicht umgesetzt)
  • -
  • loadMusic 0|1 => lade die MPD Titel beim FHEM Start
  • -
  • loadPlaylists 0|1 => lade die MPD Playlisten beim FHEM Start
  • +
  • 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 +/-
  • -
  • useIdle 0|1 => sendet das Kommando idle zum MPD und wartet auf Ereignisse - benötigt MPD Version 0.16.0 oder höher
    - Wenn useIdle benutzt wird kann das Polling auf einen hohen Wert (300-600) gesetzt werden oder gleich ganz abgeschaltet werden.
    - FHEM startet einen Hintergrundprozess und wartet auf Änderungen des MPD , wie z.B Titelwechsel im Stream, start/stop, etc.
    - So lassen sich relativ zeitnah andere Geräte an/aus schalten oder z.B. eine LCD Anzeige aktualisieren ohne erst den nächsten Polling Intervall abwarten zu müssen !
  • -
  • titleSplit 1|0 = zerlegt die aktuelle Titelangabe am ersten Vorkommen von - (BlankMinusBlank) in die zwei Felder Artist und Titel,
    - wenn im abgespielten Titel die Artist Information nicht verfügbar ist (sehr oft bei Radio-Streams)
    - Liegen keine Titelangaben vor wird die Ausgabe durch den Namen der Radiostation erstetzt
  • +
  • titleSplit 1|0 => zerlegt die aktuelle Titelangabe am ersten Vorkommen von - (BlankMinusBlank) in die zwei Felder Artist und Titel,
    + wenn im abgespielten Titel die Artist 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) => Wartezeit in Sekunden bis zum Start eines neuen Idle Prozess im Fehlerfall
  • +
  • stateMusic 1|0 => zeige Musikliste als DropDown im Webfrontend
  • +
  • statePlaylists 1|0 => zeige Playlisten als DropDown im Webfrontend
  • +
  • image_size -1|0|1|2|3 (default -1 = zeige kein Interpretenbild von lastfm)
    + lastfm stellt verschiedene Bildgroessen zur Verfügung :
    + 0 = 32x32 , 1 = 64x64 , 2 = 174x174 , 3 = 300x300
  • +
  • player mpd|mopidy|forked-daapd (default mpd) => welcher Player wird gesteuert

Readings