diff --git a/fhem/contrib/97_SB_SERVER.pm b/fhem/contrib/97_SB_SERVER.pm new file mode 100644 index 000000000..d6e13b3d6 --- /dev/null +++ b/fhem/contrib/97_SB_SERVER.pm @@ -0,0 +1,1355 @@ +# ############################################################################ +# +# FHEM Modue for Squeezebox Servers +# +# ############################################################################ +# +# used to interact with Squeezebox server +# +# ############################################################################ +# +# This is absolutley open source. Please feel free to use just as you +# like. Please note, that no warranty is given and no liability +# granted +# +# ############################################################################ +# +# we have the following readings +# power on|off +# version the version of the SB Server +# serversecure is the CLI port protected with a passowrd? +# +# ############################################################################ +# +# we have the following attributes +# alivetimer time frequency to set alive signals +# maxfavorites maximum number of favorites we handle at FHEM +# +# ############################################################################ +# we have the following internals (all UPPERCASE) +# IP the IP of the server +# CLIPORT the port for the CLI interface of the server +# +# ############################################################################ + +package main; +use strict; +use warnings; + +use IO::Socket; +use URI::Escape; +# inlcude for using the perl ping command +use Net::Ping; + + +# this will hold the hash of hashes for all instances of SB_SERVER +my %favorites; +my $favsetstring = "favorites: "; + +# this is the buffer for commands, we queue up when server is power=off +my %SB_SERVER_CmdStack; + +# include this for the self-calling timer we use later on +use Time::HiRes qw(gettimeofday); + +use constant { true => 1, false => 0 }; +use constant { TRUE => 1, FALSE => 0 }; + +# ---------------------------------------------------------------------------- +# Initialisation routine called upon start-up of FHEM +# ---------------------------------------------------------------------------- +sub SB_SERVER_Initialize( $ ) { + my ($hash) = @_; + + require "$attr{global}{modpath}/FHEM/DevIo.pm"; + +# Provider + $hash->{ReadFn} = "SB_SERVER_Read"; + $hash->{WriteFn} = "SB_SERVER_Write"; + $hash->{ReadyFn} = "SB_SERVER_Ready"; + $hash->{Clients} = ":SB_PLAYER:"; + my %matchList= ( + "1:SB_PLAYER" => "^SB_PLAYER:", + ); + $hash->{MatchList} = \%matchList; + +# Normal devices + $hash->{DefFn} = "SB_SERVER_Define"; + $hash->{UndefFn} = "SB_SERVER_Undef"; + $hash->{ShutdownFn} = "SB_SERVER_Shutdown"; + $hash->{GetFn} = "SB_SERVER_Get"; + $hash->{SetFn} = "SB_SERVER_Set"; + $hash->{AttrFn} = "SB_SERVER_Attr"; + + $hash->{AttrList} = "alivetimer maxfavorites "; + $hash->{AttrList} .= "doalivecheck:true,false "; + $hash->{AttrList} .= "maxcmdstack "; + $hash->{AttrList} .= $readingFnAttributes; + +} + +# ---------------------------------------------------------------------------- +# called when defining a module +# ---------------------------------------------------------------------------- +sub SB_SERVER_Define( $$ ) { + my ($hash, $def ) = @_; + + #my $name = $hash->{NAME}; + + Log3( $hash, 4, "SB_SERVER_Define: called" ); + + # first of all close existing connections + DevIo_CloseDev( $hash ); + + my @a = split("[ \t][ \t]*", $def); + + # do we have the right number of arguments? + if( ( @a < 3 ) || ( @a > 7 ) ) { + Log3( $hash, 3, "SB_SERVER_Define: falsche Anzahl an Argumenten" ); + return( "wrong syntax: define SB_SERVER " . + "[USER:username] [PASSWord:password] " . + "[RCC:RCC_Name] [WOL:WOLName]" ); + } + + # remove the name and our type + my $name = shift( @a ); + shift( @a ); + + # assign safe default values + $hash->{IP} = "127.0.0.1"; + $hash->{CLIPORT} = 9090; + $hash->{WOLNAME} = "none"; + $hash->{RCCNAME} = "none"; + $hash->{USERNAME} = "?"; + $hash->{PASSWORD} = "?"; + # parse the user spec + foreach( @a ) { + if( $_ =~ /^(RCC:)(.*)/ ) { + $hash->{RCCNAME} = $2; + next; + } elsif( $_ =~ /^(WOL:)(.*)/ ) { + $hash->{WOLNAME} = $2; + next; + } elsif( $_ =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d{3,5})/ ) { + $hash->{IP} = $1; + $hash->{CLIPORT} = $2; + next; + } elsif( $_ =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/ ) { + $hash->{IP} = $1; + $hash->{CLIPORT} = 9090; + next; + } elsif( $_ =~ /^(USER:)(.*)/ ) { + $hash->{USERNAME} = $2; + } elsif( $_ =~ /^(PASSWORD:)(.*)/ ) { + $hash->{PASSWORD} = $2; + } else { + next; + } + } + + $hash->{LASTANSWER} = "none"; + + # preset our attributes + if( !defined( $attr{$name}{alivetimer} ) ) { + $attr{$name}{alivetimer} = 120; + } + + if( !defined( $attr{$name}{doalivecheck} ) ) { + $attr{$name}{doalivecheck} = "true"; + } + + if( !defined( $attr{$name}{maxfavorites} ) ) { + $attr{$name}{maxfavorites} = 30; + } + + if( !defined( $attr{$name}{maxcmdstack} ) ) { + $attr{$name}{maxcmdstack} = 200; + } + + # Preset our readings if undefined + my $tn = TimeNow(); + + # server on / off + if( !defined( $hash->{READINGS}{power}{VAL} ) ) { + $hash->{READINGS}{power}{VAL} = "?"; + $hash->{READINGS}{power}{TIME} = $tn; + } + + # the server version + if( !defined( $hash->{READINGS}{serverversion}{VAL} ) ) { + $hash->{READINGS}{serverversion}{VAL} = "?"; + $hash->{READINGS}{serverversion}{TIME} = $tn; + } + + # is the CLI port secured with password? + if( !defined( $hash->{READINGS}{serversecure}{VAL} ) ) { + $hash->{READINGS}{serversecure}{VAL} = "?"; + $hash->{READINGS}{serversecure}{TIME} = $tn; + } + + # the status of our server alive check mechanism + if( !defined( $hash->{READINGS}{alivecheck}{VAL} ) ) { + $hash->{READINGS}{alivecheck}{VAL} = "?"; + $hash->{READINGS}{alivecheck}{TIME} = $tn; + } + + + # the maximum number of favorites on the server + if( !defined( $hash->{READINGS}{favoritestotal}{VAL} ) ) { + $hash->{READINGS}{favoritestotal}{VAL} = 0; + $hash->{READINGS}{favoritestotal}{TIME} = $tn; + } + + # is a scan in progress + if( !defined( $hash->{READINGS}{scanning}{VAL} ) ) { + $hash->{READINGS}{scanning}{VAL} = "?"; + $hash->{READINGS}{scanning}{TIME} = $tn; + } + + # the scan in progress + if( !defined( $hash->{READINGS}{scandb}{VAL} ) ) { + $hash->{READINGS}{scandb}{VAL} = "?"; + $hash->{READINGS}{scandb}{TIME} = $tn; + } + + # the scan already completed + if( !defined( $hash->{READINGS}{scanprogressdone}{VAL} ) ) { + $hash->{READINGS}{scanprogressdone}{VAL} = "?"; + $hash->{READINGS}{scanprogressdone}{TIME} = $tn; + } + + # the scan already completed + if( !defined( $hash->{READINGS}{scanprogresstotal}{VAL} ) ) { + $hash->{READINGS}{scanprogresstotal}{VAL} = "?"; + $hash->{READINGS}{scanprogresstotal}{TIME} = $tn; + } + + # did the last scan fail + if( !defined( $hash->{READINGS}{scanlastfailed}{VAL} ) ) { + $hash->{READINGS}{scanlastfailed}{VAL} = "?"; + $hash->{READINGS}{scanlastfailed}{TIME} = $tn; + } + + # number of players connected to us + if( !defined( $hash->{READINGS}{players}{VAL} ) ) { + $hash->{READINGS}{players}{VAL} = "?"; + $hash->{READINGS}{players}{TIME} = $tn; + } + + # number of players connected to mysqueezebox + if( !defined( $hash->{READINGS}{players_mysb}{VAL} ) ) { + $hash->{READINGS}{players_mysb}{VAL} = "?"; + $hash->{READINGS}{players_mysb}{TIME} = $tn; + } + + # number of players connected to other servers in our network + if( !defined( $hash->{READINGS}{players_other}{VAL} ) ) { + $hash->{READINGS}{players_other}{VAL} = "?"; + $hash->{READINGS}{players_other}{TIME} = $tn; + } + + # number of albums in the database + if( !defined( $hash->{READINGS}{db_albums}{VAL} ) ) { + $hash->{READINGS}{db_albums}{VAL} = "?"; + $hash->{READINGS}{db_albums}{TIME} = $tn; + } + + # number of artists in the database + if( !defined( $hash->{READINGS}{db_artists}{VAL} ) ) { + $hash->{READINGS}{db_artists}{VAL} = "?"; + $hash->{READINGS}{db_artists}{TIME} = $tn; + } + + # number of songs in the database + if( !defined( $hash->{READINGS}{db_songs}{VAL} ) ) { + $hash->{READINGS}{db_songs}{VAL} = "?"; + $hash->{READINGS}{db_songs}{TIME} = $tn; + } + + # number of genres in the database + if( !defined( $hash->{READINGS}{db_genres}{VAL} ) ) { + $hash->{READINGS}{db_genres}{VAL} = "?"; + $hash->{READINGS}{db_genres}{TIME} = $tn; + } + + # initialize the command stack + $SB_SERVER_CmdStack{$name}{first_n} = 0; + $SB_SERVER_CmdStack{$name}{last_n} = 0; + $SB_SERVER_CmdStack{$name}{cnt} = 0; + + # assign our IO Device + $hash->{DeviceName} = "$hash->{IP}:$hash->{CLIPORT}"; + + # open the IO device + my $ret = DevIo_OpenDev($hash, 0, "SB_SERVER_DoInit" ); + + # do and update of the status + InternalTimer( gettimeofday() + 10, + "SB_SERVER_DoInit", + $hash, + 0 ); + + Log3( $hash, 4, "SB_SERVER_Define: leaving" ); + + return $ret; +} + + +# ---------------------------------------------------------------------------- +# called when deleting a module +# ---------------------------------------------------------------------------- +sub SB_SERVER_Undef( $$ ) { + my ($hash, $arg) = @_; + my $name = $hash->{NAME}; + + Log3( $hash, 4, "SB_SERVER_Undef: called" ); + + # no idea what this is for. Copied from 10_TCM.pm + # presumably to notify the clients, that the server is gone + foreach my $d (sort keys %defs) { + if( ( defined( $defs{$d} ) ) && + ( defined( $defs{$d}{IODev} ) ) && + ( $defs{$d}{IODev} == $hash ) ) { + delete $defs{$d}{IODev}; + } + } + + # terminate the CLI session + DevIo_SimpleWrite( $hash, "listen 0\n", 0 ); + DevIo_SimpleWrite( $hash, "exit\n", 0 ); + + # close the device + DevIo_CloseDev( $hash ); + + # remove all timers we created + RemoveInternalTimer( $hash ); + + return( undef ); +} + +# ---------------------------------------------------------------------------- +# Shutdown function - called before fhem shuts down +# ---------------------------------------------------------------------------- +sub SB_SERVER_Shutdown( $$ ) { + my ($hash, $dev) = @_; + + Log3( $hash, 4, "SB_SERVER_Shutdown: called" ); + + # terminate the CLI session + DevIo_SimpleWrite( $hash, "listen 0\n", 0 ); + DevIo_SimpleWrite( $hash, "exit\n", 0 ); + + # close the device + DevIo_CloseDev( $hash ); + + # remove all timers we created + RemoveInternalTimer( $hash ); + + return( undef ); +} + + +# ---------------------------------------------------------------------------- +# ReadyFn - called when? +# ---------------------------------------------------------------------------- +sub SB_SERVER_Ready( $ ) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + Log3( $hash, 4, "SB_SERVER_Ready: called" ); + + # we need to re-open the device + if( $hash->{STATE} eq "disconnected" ) { + if( ( ReadingsVal( $name, "power", "on" ) eq "on" ) || + ( ReadingsVal( $name, "power", "on" ) eq "?" ) ) { + # obviously the first we realize the Server is off + # clean up first + RemoveInternalTimer( $hash ); + readingsSingleUpdate( $hash, "power", "off", 1 ); + + # and signal to our clients + SB_SERVER_Broadcast( $hash, "SERVER", "OFF" ); + } + + if( $hash->{TCPDev} ) { + SB_SERVER_DoInit( $hash ); + } + } + + return( DevIo_OpenDev( $hash, 1, "SB_SERVER_DoInit" ) ); +} + + +# ---------------------------------------------------------------------------- +# Get functions +# ---------------------------------------------------------------------------- +sub SB_SERVER_Get( $@ ) { + my ($hash, @a) = @_; + my $name = $hash->{NAME}; + + Log3( $hash, 4, "SB_SERVER_Get: called" ); + + if( @a != 2 ) { + return( "\"get $name\" needs one parameter" ); + } + + return( "?" ); +} + + +# ---------------------------------------------------------------------------- +# Attr functions +# ---------------------------------------------------------------------------- +sub SB_SERVER_Attr( @ ) { + my $cmd = shift( @_ ); + my $name = shift( @_ ); + my @args = @_; + + Log( 1, "SB_SERVER_Attr: called with @args" ); + + if( $cmd eq "set" ) { + if( $args[ 0 ] eq "alivetimer" ) { + + } + } + + # do an update of the status +# InternalTimer( gettimeofday() + AttrVal( $name, "alivetimer", 120 ), +# "SB_SERVER_Alive", +# $hash, +# 0 ); +} + + +# ---------------------------------------------------------------------------- +# Set function +# ---------------------------------------------------------------------------- +sub SB_SERVER_Set( $@ ) { + my ($hash, @a) = @_; + my $name = $hash->{NAME}; + + if( @a < 2 ) { + return( "at least one parameter is needed" ) ; + } + + $name = shift( @a ); + my $cmd = shift( @a ); + + if( $cmd eq "?" ) { + # this one should give us a drop down list + my $res = "Unknown argument ?, choose one of " . + "on renew:noArg abort:noArg cliraw statusRequest:noArg "; + $res .= "rescan:full,playlists "; + + return( $res ); + + } elsif( $cmd eq "on" ) { + if( ReadingsVal( $name, "power", "off" ) eq "off" ) { + # the server is off, try to reactivate it + if( $hash->{WOLNAME} ne "none" ) { + fhem( "set $hash->{WOLNAME} on" ); + } + if( $hash->{RCCNAME} ne "none" ) { + fhem( "set $hash->{RCCNAME} on" ); + } + + } elsif( $cmd eq "renew" ) { + Log3( $hash, 5, "SB_SERVER_Set: renew" ); + DevIo_SimpleWrite( $hash, "listen 1\n", 0 ); + + } elsif( $cmd eq "abort" ) { + DevIo_SimpleWrite( $hash, "listen 0\n", 0 ); + + } elsif( $cmd eq "statusRequest" ) { + DevIo_SimpleWrite( $hash, "serverstatus 0 200\n", 0 ); + + } elsif( $cmd eq "cliraw" ) { + # write raw messages to the CLI interface per player + my $v = join( " ", @a ); + $v .= "\n"; + Log3( $hash, 5, "SB_SERVER_Set: cliraw: $v " ); + IOWrite( $hash, $v ); + + } elsif( $cmd eq "rescan" ) { + IOWrite( $hash, $cmd . " " . $a[ 0 ] . "\n" ); + + } else { + ; + } + + return( undef ); + } +} + +# ---------------------------------------------------------------------------- +# Read +# called from the global loop, when the select for hash->{FD} reports data +# ---------------------------------------------------------------------------- +sub SB_SERVER_Read( $ ) { + my ($hash) = @_; + + Log3( $hash, 5, "+++++++++++++++++++++++++++++++++++++++++++++++++++++" ); + Log3( $hash, 5, "New Squeezebox Server Read cycle starts here" ); + Log3( $hash, 5, "+++++++++++++++++++++++++++++++++++++++++++++++++++++" ); + Log3( $hash, 5, "SB_SERVER_Read: called" ); + + my $buf = DevIo_SimpleRead( $hash ); + + if( !defined( $buf ) ) { + return( "" ); + } + + my $name = $hash->{NAME}; + + # if we have data, the server is on again + if( ReadingsVal( $name, "power", "off" ) ne "on" ) { + readingsSingleUpdate( $hash, "power", "on", 1 ); + if( defined( $SB_SERVER_CmdStack{$name}{cnt} ) ) { + my $maxmsg = $SB_SERVER_CmdStack{$name}{cnt}; + my $out; + for( my $n = 0; $n <= $maxmsg; $n++ ) { + $out = SB_SERVER_CMDStackPop( $hash ); + if( $out ne "empty" ) { + DevIo_SimpleWrite( $hash, $out , 0 ); + } + } + } + + + Log3( $hash, 5, "SB_SERVER_Read($name): please implelement the " . + "sending of the CMDStack." ); + } + + # if there are remains from the last time, append them now + $buf = $hash->{PARTIAL} . $buf; + + $buf = uri_unescape( $buf ); + Log3( $hash, 6, "SB_SERVER_Read: the buf: $buf" ); + + + # if we have received multiline commands, they are split by \n + my @cmds = split( "\n", $buf ); + + # check for last element in string + my $lastchr = substr( $buf, -1, 1 ); + if( $lastchr ne "\n" ) { + #ups, the return doesn't seem to be complete + $hash->{PARTIAL} = $cmds[ $#cmds ]; + # and remove the last element + pop( @cmds ); + Log3( $hash, 5, "SB_SERVER_Read: uncomplete command received" ); + } else { + Log3( $hash, 5, "SB_SERVER_Read: complete command received" ); + $hash->{PARTIAL} = ""; + } + + # and dispatch the rest + foreach( @cmds ) { + # double check complete line + my $lastchar = substr( $_, -1); + SB_SERVER_DispatchCommandLine( $hash, $_ ); + } + + Log3( $hash, 5, "+++++++++++++++++++++++++++++++++++++++++++++++++++++" ); + Log3( $hash, 5, "Squeezebox Server Read cycle ends here" ); + Log3( $hash, 5, "+++++++++++++++++++++++++++++++++++++++++++++++++++++" ); + + return( undef ); +} + + +# ---------------------------------------------------------------------------- +# called by the clients to send data +# ---------------------------------------------------------------------------- +sub SB_SERVER_Write( $$$ ) { + my ( $hash, $fn, $msg ) = @_; + my $name = $hash->{NAME}; + + if( !defined( $fn ) ) { + return( undef ); + } + + Log3( $hash, 4, "SB_SERVER_Write($name): called with FN:$fn" ); + + if( defined( $msg ) ) { + Log3( $hash, 4, "SB_SERVER_Write: MSG:$msg" ); + } + + if( ReadingsVal( $name, "serversecure", "0" ) eq "1" ) { + if( ( $hash->{USERNAME} ne "?" ) && ( $hash->{PASSWORD} ne "?" ) ) { + # we need to send username and passord first + } else { + my $retmsg = "SB_SERVER_Write: Server needs username and " . + "password but you did not specify those. No sending"; + Log3( $hash, 1, $retmsg ); + return( $retmsg ); + } + } + + if( ReadingsVal( $name, "power", "on" ) eq "on" ) { + DevIo_SimpleWrite( $hash, "$fn", 0 ); + } else { + # we are off, so save the command for later + # if maxcmdstack is 0, the function is turned off + if( AttrVal( $name, "maxcmdstack", 100 ) > 0 ) { + SB_SERVER_CMDStackPush( $hash, $fn ); + } + } + +} + + +# ---------------------------------------------------------------------------- +# Initialisation of the CLI connection +# ---------------------------------------------------------------------------- +sub SB_SERVER_DoInit( $ ) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + Log3( $hash, 4, "SB_SERVER_DoInit: called" ); + + if( !$hash->{TCPDev} ) { + Log3( $hash, 5, "SB_SERVER_DoInit: no TCPDev available?" ); + } + + if( $hash->{STATE} eq "disconnected" ) { + # server is off after FHEM start, broadcast to clients + SB_SERVER_Broadcast( $hash, "SERVER", "OFF" ); + return( "" ); + } + + # subscribe us + DevIo_SimpleWrite( $hash, "listen 1\n", 0 ); + + # and get some info on the server + DevIo_SimpleWrite( $hash, "pref authorize ?\n", 0 ); + DevIo_SimpleWrite( $hash, "version ?\n", 0 ); + DevIo_SimpleWrite( $hash, "serverstatus 0 200\n", 0 ); + DevIo_SimpleWrite( $hash, "favorites items 0 " . + AttrVal( $name, "maxfavorites", 100 ) . "\n", 0 ); + + # start the alive checking mechanism + readingsSingleUpdate( $hash, "alivecheck", "?", 0 ); + InternalTimer( gettimeofday() + AttrVal( $name, "alivetimer", 120 ), + "SB_SERVER_Alive", + $hash, + 0 ); + + return( undef ); +} + + +# ---------------------------------------------------------------------------- +# Dispatch every single line of commands +# ---------------------------------------------------------------------------- +sub SB_SERVER_DispatchCommandLine( $$ ) { + my ( $hash, $buf ) = @_; + my $name = $hash->{NAME}; + + Log3( $hash, 4, "SB_SERVER_DispatchCommandLine($name): Line:$buf..." ); + + # try to extract the first answer to the SPACE + my $indx = index( $buf, " " ); + my $id1 = substr( $buf, 0, $indx ); + + # is the first return value a player ID? + # Player ID is MAC adress, hence : included + my @id = split( ":", $id1 ); + + if( @id > 1 ) { + # we have received a return for a dedicated player + + # create the fhem specific unique id + my $playerid = join( "", @id ); + Log3( $hash, 5, "SB_SERVER_DispatchCommandLine: fhem-id: $playerid" ); + + # create the commands + my $cmds = substr( $buf, $indx + 1 ); + Log3( $hash, 5, "SB_SERVER__DispatchCommandLine: commands: $cmds" ); + Dispatch( $hash, "SB_PLAYER:$playerid:$cmds", undef ); + + } else { + # that is a server specific command + SB_SERVER_ParseCmds( $hash, $buf ); + } + + return( undef ); +} + + +# ---------------------------------------------------------------------------- +# parse the server answers that are not intended for players +# ---------------------------------------------------------------------------- +sub SB_SERVER_ParseCmds( $$ ) { + my ( $hash, $instr ) = @_; + + my $name = $hash->{NAME}; + + my @args = split( " ", $instr ); + + $hash->{LASTANSWER} = "@args"; + + my $cmd = shift( @args ); + + if( $cmd eq "version" ) { + readingsSingleUpdate( $hash, "serverversion", $args[ 1 ], 0 ); + + if( ReadingsVal( $name, "power", "off" ) eq "off" ) { + # that also means the server returned from being away + readingsSingleUpdate( $hash, "power", "on", 1 ); + # signal our players + SB_SERVER_Broadcast( $hash, "SERVER", "ON" ); + } + + } elsif( $cmd eq "pref" ) { + if( $args[ 0 ] eq "authorize" ) { + readingsSingleUpdate( $hash, "serversecure", $args[ 1 ], 0 ); + } + + } elsif( $cmd eq "fhemalivecheck" ) { + readingsSingleUpdate( $hash, "alivecheck", "received", 0 ); + Log3( $hash, 4, "SB_SERVER_ParseCmds($name): alivecheck received" ); + + } elsif( $cmd eq "favorites" ) { + if( $args[ 0 ] eq "changed" ) { + Log3( $hash, 4, "SB_SERVER_ParseCmds($name): favorites changed" ); + # we need to trigger the favorites update here + DevIo_SimpleWrite( $hash, "favorites items 0 " . + AttrVal( $name, "maxfavorites", 100 ) . + "\n", 0 ); + } elsif( $args[ 0 ] eq "items" ) { + Log3( $hash, 4, "SB_SERVER_ParseCmds($name): favorites items" ); + # the response to our query of the favorites + SB_SERVER_FavoritesParse( $hash, join( " ", @args ) ); + } else { + } + + } elsif( $cmd eq "serverstatus" ) { + Log3( $hash, 4, "SB_SERVER_ParseCmds($name): server status" ); + SB_SERVER_ParseServerStatus( $hash, \@args ); + + } else { + # unkown + } +} + + +# ---------------------------------------------------------------------------- +# Alivecheck of the server +# ---------------------------------------------------------------------------- +sub SB_SERVER_Alive( $ ) { + my ($hash) = @_; + my $name = $hash->{NAME}; + + Log3( $hash, 4, "SB_SERVER_Alive($name): called" ); + + if( AttrVal( $name, "doalivecheck", "false" ) eq "false" ) { + Log3( $hash, 5, "SB_SERVER_Alive($name): alivechecking is off" ); + return; + } + + # let's ping the server to figure out if he is reachable + # needed for servers that go in hibernate mode + my $p = Net::Ping->new( 'tcp' ); + if( $p->ping( $hash->{IP}, 2 ) ) { + # host is reachable so go on normally + if( ReadingsVal( $name, "power", "on" ) eq "off" ) { + Log3( $hash, 5, "SB_SERVER_Alive($name): ping succesful. " . + "SB-Server is back again." ); + # first time we realized server is away + DevIo_OpenDev( $hash, 1, "SB_SERVER_DoInit" ); + readingsSingleUpdate( $hash, "power", "on", 1 ); + readingsSingleUpdate( $hash, "alivecheck", "?", 0 ); + # signal that to our clients + SB_SERVER_Broadcast( $hash, "SERVER", "ON" ); + } + + if( ReadingsVal( $name, "alivecheck", "received" ) eq "waiting" ) { + # ups, we did not receive any answer in the last minutes + # SB Server potentially dead or shut-down + Log3( $hash, 5, "SB_SERVER_Alive($name): overrun SB-Server dead." ); + + readingsSingleUpdate( $hash, "power", "off", 1 ); + readingsSingleUpdate( $hash, "alivecheck", "?", 0 ); + + # signal that to our clients + SB_SERVER_Broadcast( $hash, "SERVER", "OFF" ); + + # close the device + DevIo_CloseDev( $hash ); + + # remove all timers we created + RemoveInternalTimer( $hash ); + } else { + # just send something to the SB-Server. It will echo it + # if we receive the echo, the server is still alive + DevIo_SimpleWrite( $hash, "fhemalivecheck\n", 0 ); + + readingsSingleUpdate( $hash, "alivecheck", "waiting", 0 ); + } + + } else { + # the server is away and therefore presumably in hibernate / suspend + Log3( $hash, 5, "SB_SERVER_Alive($name): ping timeout. " . + "SB-Server in hibernate / suspend?." ); + + if( ReadingsVal( $name, "power", "off" ) eq "on" ) { + # first time we realized server is away + readingsSingleUpdate( $hash, "power", "off", 1 ); + readingsSingleUpdate( $hash, "alivecheck", "?", 0 ); + + # signal that to our clients + SB_SERVER_Broadcast( $hash, "SERVER", "OFF" ); + + # close the device + DevIo_CloseDev( $hash ); + # remove all timers we created + RemoveInternalTimer( $hash ); + } + + } + + # close our ping mechanism again + $p->close( ); + + # do an update of the status + InternalTimer( gettimeofday() + AttrVal( $name, "alivetimer", 120 ), + "SB_SERVER_Alive", + $hash, + 0 ); +} + + +# ---------------------------------------------------------------------------- +# Broadcast a message to all clients +# ---------------------------------------------------------------------------- +sub SB_SERVER_Broadcast( $$@ ) { + my( $hash, $cmd, $msg, $bin ) = @_; + my $name = $hash->{NAME}; + my $iodevhash; + + Log3( $hash, 4, "SB_SERVER_Broadcast: called" ); + + if( !defined( $bin ) ) { + $bin = 0; + } + + foreach my $mydev ( keys %defs ) { + # the hash to the IODev as defined at the client + if( defined( $defs{$mydev}{IODev} ) ) { + $iodevhash = $defs{$mydev}{IODev}; + } else { + $iodevhash = undef; + } + + if( defined( $iodevhash ) ) { + if( ( defined( $defs{$mydev}{TYPE} ) ) && + ( defined( $iodevhash->{NAME} ) ) ){ + + if( ( $defs{$mydev}{TYPE} eq "SB_PLAYER" ) && + ( $iodevhash->{NAME} eq $name ) ) { + # we found a valid entry + my $clienthash = $defs{$mydev}; + my $namebuf = $clienthash->{NAME}; + + SB_PLAYER_RecBroadcast( $clienthash, $cmd, $msg, $bin ); + } + } + } + } + + return; +} + + +# ---------------------------------------------------------------------------- +# Handle the return for a serverstatus query +# ---------------------------------------------------------------------------- +sub SB_SERVER_ParseServerStatus( $$ ) { + my( $hash, $dataptr ) = @_; + + my $name = $hash->{NAME}; + + # typically the start index being a number + if( $dataptr->[ 0 ] =~ /^([0-9])*/ ) { + shift( @{$dataptr} ); + } else { + Log3( $hash, 5, "SB_SERVER_ParseServerStatus($name): entry is " . + "not the start number" ); + return; + } + + # typically the max index being a number + if( $dataptr->[ 0 ] =~ /^([0-9])*/ ) { + shift( @{$dataptr} ); + } else { + Log3( $hash, 5, "SB_SERVER_ParseServerStatus($name): entry is " . + "not the end number" ); + return; + } + + my $datastr = join( " ", @{$dataptr} ); + # replace funny stuff + $datastr =~ s/info total albums/infototalalbums/g; + $datastr =~ s/info total artists/infototalartists/g; + $datastr =~ s/info total songs/infototalsongs/g; + $datastr =~ s/info total genres/infototalgenres/g; + $datastr =~ s/sn player count/snplayercount/g; + $datastr =~ s/other player count/otherplayercount/g; + $datastr =~ s/player count/playercount/g; + + Log3( $hash, 5, "SB_SERVER_ParseServerStatus($name): data to parse: " . + $datastr ); + + my @data1 = split( " ", $datastr ); + + # the rest of the array should now have the data, we're interested in + readingsBeginUpdate( $hash ); + + # set default values for stuff not always send + readingsBulkUpdate( $hash, "scanning", "no" ); + readingsBulkUpdate( $hash, "scandb", "?" ); + readingsBulkUpdate( $hash, "scanprogressdone", "0" ); + readingsBulkUpdate( $hash, "scanprogresstotal", "0" ); + readingsBulkUpdate( $hash, "scanlastfailed", "none" ); + + my $addplayers = true; + my %players; + my $currentplayerid = "none"; + + # needed for scanning the MAC Adress + my $d = "[0-9A-Fa-f]"; + my $dd = "$d$d"; + + # needed for scanning the IP adress + my $e = "[0-9]"; + my $ee = "$e$e"; + + foreach( @data1 ) { + if( $_ =~ /^(lastscan:)([0-9]*)/ ) { + # we found the lastscan entry + my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = + localtime( $2 ); + $year = $year + 1900; + readingsBulkUpdate( $hash, "scan_last", "$mday-$mon-$year " . + "$hour:$min:$sec" ); + next; + } elsif( $_ =~ /^(scanning:)([0-9]*)/ ) { + readingsBulkUpdate( $hash, "scanning", $2 ); + next; + } elsif( $_ =~ /^(version:)([0-9\.]*)/ ) { + readingsBulkUpdate( $hash, "serverversion", $2 ); + next; + } elsif( $_ =~ /^(playercount:)([0-9]*)/ ) { + readingsBulkUpdate( $hash, "players", $2 ); + next; + } elsif( $_ =~ /^(snplayercount:)([0-9]*)/ ) { + readingsBulkUpdate( $hash, "players_mysb", $2 ); + $currentplayerid = "none"; + $addplayers = false; + next; + } elsif( $_ =~ /^(otherplayercount:)([0-9]*)/ ) { + readingsBulkUpdate( $hash, "players_other", $2 ); + $currentplayerid = "none"; + $addplayers = false; + next; + } elsif( $_ =~ /^(infototalalbums:)([0-9]*)/ ) { + readingsBulkUpdate( $hash, "db_albums", $2 ); + next; + } elsif( $_ =~ /^(infototalartists:)([0-9]*)/ ) { + readingsBulkUpdate( $hash, "db_artists", $2 ); + next; + } elsif( $_ =~ /^(infototalsongs:)([0-9]*)/ ) { + readingsBulkUpdate( $hash, "db_songs", $2 ); + next; + } elsif( $_ =~ /^(infototalgenres:)([0-9]*)/ ) { + readingsBulkUpdate( $hash, "db_genres", $2 ); + next; + } elsif( $_ =~ /^(playerid:)($dd[:|-]$dd[:|-]$dd[:|-]$dd[:|-]$dd[:|-]$dd)/ ) { + my $id = join( "", split( ":", $2 ) ); + if( $addplayers = true ) { + $players{$id}{ID} = $id; + $players{$id}{MAC} = $2; + $currentplayerid = $id; + } + next; + } elsif( $_ =~ /^(name:)(.*)/ ) { + if( $currentplayerid ne "none" ) { + $players{$currentplayerid}{name} = $2; + } + next; + } elsif( $_ =~ /^(displaytype:)(.*)/ ) { + if( $currentplayerid ne "none" ) { + $players{$currentplayerid}{displaytype} = $2; + } + next; + } elsif( $_ =~ /^(model:)(.*)/ ) { + if( $currentplayerid ne "none" ) { + $players{$currentplayerid}{model} = $2; + } + next; + } elsif( $_ =~ /^(power:)([0|1])/ ) { + if( $currentplayerid ne "none" ) { + $players{$currentplayerid}{power} = $2; + } + next; + } elsif( $_ =~ /^(canpoweroff:)([0|1])/ ) { + if( $currentplayerid ne "none" ) { + $players{$currentplayerid}{canpoweroff} = $2; + } + next; + } elsif( $_ =~ /^(connected:)([0|1])/ ) { + if( $currentplayerid ne "none" ) { + $players{$currentplayerid}{connected} = $2; + } + next; + } elsif( $_ =~ /^(isplayer:)([0|1])/ ) { + if( $currentplayerid ne "none" ) { + $players{$currentplayerid}{isplayer} = $2; + } + next; + } elsif( $_ =~ /^(ip:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{3,5})/ ) { + if( $currentplayerid ne "none" ) { + $players{$currentplayerid}{IP} = $2; + } + next; + } elsif( $_ =~ /^(seq_no:)(.*)/ ) { + # just to take care of the keyword + next; + } else { + # no keyword found, so let us assume it is part of the player name + if( $currentplayerid ne "none" ) { + $players{$currentplayerid}{name} .= $_; + } + + } + } + + readingsEndUpdate( $hash, 1 ); + + foreach my $player ( keys %players ) { + if( defined( $players{$player}{isplayer} ) ) { + if( $players{$player}{isplayer} eq "0" ) { + Log3( $hash, 1, "not a player" ); + next; + } + } + + # if the player is not yet known, it will be created + if( defined( $players{$player}{ID} ) ) { + Dispatch( $hash, "SB_PLAYER:$players{$player}{ID}:NONE", undef ); + } else { + Log3( $hash, 1, "not defined" ); + next; + } + + if( defined( $players{$player}{name} ) ) { + Dispatch( $hash, "SB_PLAYER:$players{$player}{ID}:" . + "name $players{$player}{name}", undef ); + } + + if( defined( $players{$player}{IP} ) ) { + Dispatch( $hash, "SB_PLAYER:$players{$player}{ID}:" . + "player ip $players{$player}{IP}", undef ); + } + + if( defined( $players{$player}{model} ) ) { + Dispatch( $hash, "SB_PLAYER:$players{$player}{ID}:" . + "player model $players{$player}{model}", undef ); + } + + if( defined( $players{$player}{canpoweroff} ) ) { + Dispatch( $hash, "SB_PLAYER:$players{$player}{ID}:" . + "player canpoweroff $players{$player}{canpoweroff}", + undef ); + } + + if( defined( $players{$player}{power} ) ) { + Dispatch( $hash, "SB_PLAYER:$players{$player}{ID}:" . + "power $players{$player}{power}", undef ); + } + + if( defined( $players{$player}{connected} ) ) { + Dispatch( $hash, "SB_PLAYER:$players{$player}{ID}:" . + "connected $players{$player}{connected}", undef ); + } + + if( defined( $players{$player}{displaytype} ) ) { + Dispatch( $hash, "SB_PLAYER:$players{$player}{ID}:" . + "displaytype $players{$player}{displaytype}", undef ); + } + } + + return; +} + + +# ---------------------------------------------------------------------------- +# Parse the return values of the favorites items +# ---------------------------------------------------------------------------- +sub SB_SERVER_FavoritesParse( $$ ) { + my ( $hash, $str ) = @_; + + my $name = $hash->{NAME}; + + # flush the existing list + foreach my $titi ( keys %{$favorites{$name}} ) { + delete( $favorites{$name}{$titi} ); + } + + # split up the string we got + my @data = split( " ", $str ); + + # eliminate the first entries of the response + # some more comment + # typically 'items' + if( $data[ 0 ] =~ /^(items)*/ ) { + my $notneeded = shift( @data ); + } + + # typically the start index being a number + if( $data[ 0 ] =~ /^([0-9])*/ ) { + my $notneeded = shift( @data ); + } + + # typically the start index being a number + my $maxwanted = 100; + if( $data[ 0 ] =~ /^([0-9])*/ ) { + $maxwanted = int( shift( @data ) ); + } + + # find the maximum number of favorites. That is typically at the + # end of the server response. So check there first + my $totals = 0; + my $lastdata = $data[ $#data ]; + if( $lastdata =~ /^(count:)([0-9]*)/ ) { + $totals = $2; + # remove the last element from the array + pop( @data ); + } else { + my $i = 0; + my $delneeded = false; + foreach( @data ) { + if( $_ =~ /^(count:)([0-9]*)/ ) { + $totals = $2; + $delneeded = true; + last; + } else { + $i++; + } + + # delete the element from the list + if( $delneeded == true ) { + splice( @data, $i, 1 ); + } + } + } + readingsSingleUpdate( $hash, "favoritestotal", $totals, 0 ); + + + my $favname = ""; + if( $data[ 0 ] =~ /^(title:)(.*)/ ) { + $favname = $2; + shift( @data ); + } + readingsSingleUpdate( $hash, "favoritesname", $favname, 0 ); + + # check if we got all the favoites with our response + if( $totals > $maxwanted ) { + # we asked for too less data, there are more favorites defined + } + + # treat the rest of the string + my $namestarted = false; + my $firstone = true; + + my $namebuf = ""; + my $idbuf = ""; + my $hasitemsbuf = false; + my $isaudiobuf = ""; + + foreach ( @data ) { + if( $_ =~ /^(id:|ID:)([A-Za-z0-9\.]*)/ ) { + # we found an ID, that is typically the start of a new session + # so save the old session first + if( $firstone == false ) { + if( $hasitemsbuf == false ) { + # derive our hash entry + my $entryuid = SB_SERVER_FavoritesName2UID( $namebuf ); + $favorites{$name}{$entryuid} = { + ID => $idbuf, + Name => $namebuf, }; + $namebuf = ""; + $isaudiobuf = ""; + $hasitemsbuf = false; + } else { + # that is a folder we found, but we don't handle that + } + } + + $firstone = false; + $idbuf = $2; + + # if there has been a name found before, end it now + if( $namestarted == true ) { + $namestarted = false; + } + + } elsif( $_ =~ /^(isaudio:)([0|1]?)/ ) { + $isaudiobuf = $2; + if( $namestarted == true ) { + $namestarted = false; + } + + } elsif( $_ =~ /^(hasitems:)([0|1]?)/ ) { + if( int( $2 ) == 0 ) { + $hasitemsbuf = false; + } else { + $hasitemsbuf = true; + } + + if( $namestarted == true ) { + $namestarted = false; + } + + } elsif( $_ =~ /^(type:)([a|u|d|i|o]*)/ ) { + if( $namestarted == true ) { + $namestarted = false; + } + + } elsif( $_ =~ /^(name:)([0-9a-zA-Z]*)/ ) { + $namebuf = $2; + $namestarted = true; + + } else { + # no regexp matched, so it must be part of the name + if( $namestarted == true ) { + $namebuf .= " " . $_; + } + } + } + + # capture the last element also + if( ( $namebuf ne "" ) && ( $idbuf ne "" ) ) { + if( $hasitemsbuf == false ) { + my $entryuid = join( "", split( " ", $namebuf ) ); + $favorites{$name}{$entryuid} = { + ID => $idbuf, + Name => $namebuf, }; + } else { + # that is a folder we found, but we don't handle that + } + } + + # make all client create e new favorites list + SB_SERVER_Broadcast( $hash, "FAVORITES", + "FLUSH dont care", undef ); + + # find all the names and broadcast to our clients + $favsetstring = "favorites:"; + foreach my $titi ( keys %{$favorites{$name}} ) { + Log3( $hash, 5, "SB_SERVER_ParseFavorites($name): " . + "ID:" . $favorites{$name}{$titi}{ID} . + " Name:" . $favorites{$name}{$titi}{Name} . "$titi" ); + $favsetstring .= "$titi,"; + SB_SERVER_Broadcast( $hash, "FAVORITES", + "ADD $name $favorites{$name}{$titi}{ID} " . + "$titi", undef ); + } + #chop( $favsetstring ); + #$favsetstring .= " "; +} + + +# ---------------------------------------------------------------------------- +# generate a UID for the hash entry from the name +# ---------------------------------------------------------------------------- +sub SB_SERVER_FavoritesName2UID( $ ) { + my $namestr = shift( @_ ); + + # eliminate spaces + $namestr = join( "", split( " ", $namestr ) ); + + # this defines the regexp. Please add new stuff with the seperator | + my $tobereplaced = '[Ä|ä|Ö|öÜ|ü|\[|\]|\{|\}|\(|\)|\\\\|' . + '\/|\'|\.|\"|\^|°|\$|\||%|@]|ü|&'; + + $namestr =~ s/$tobereplaced//g; + + return( $namestr ); +} + +# ---------------------------------------------------------------------------- +# push a command to the buffer +# ---------------------------------------------------------------------------- +sub SB_SERVER_CMDStackPush( $$ ) { + my ( $hash, $cmd ) = @_; + + my $name = $hash->{NAME}; + + my $n = $SB_SERVER_CmdStack{$name}{last_n}; + + if( $n > AttrVal( $name, "maxcmdstack", 200 ) ) { + Log3( $hash, 5, "SB_SERVER_CMDStackPush($name): limit reached" ); + return; + } + + $SB_SERVER_CmdStack{$name}{$n} = $cmd; + + $n = $n + 1; + + $SB_SERVER_CmdStack{$name}{last_n} = $n; + + # update overall number of entries + $SB_SERVER_CmdStack{$name}{cnt} = $SB_SERVER_CmdStack{$name}{last_n} - + $SB_SERVER_CmdStack{$name}{first_n} + 1; +} + +# ---------------------------------------------------------------------------- +# pop a command from the buffer +# ---------------------------------------------------------------------------- +sub SB_SERVER_CMDStackPop( $ ) { + my ( $hash ) = @_; + + my $name = $hash->{NAME}; + + my $n = $SB_SERVER_CmdStack{$name}{first_n}; + + my $res = ""; + # return the first element of the list + if( defined( $SB_SERVER_CmdStack{$name}{$n} ) ) { + $res = $SB_SERVER_CmdStack{$name}{$n}; + } else { + $res = "empty"; + } + + # and now remove the first element + + delete( $SB_SERVER_CmdStack{$name}{$n} ); + + $n = $n + 1; + + if ( $n <= $SB_SERVER_CmdStack{$name}{first_n} ) { + $SB_SERVER_CmdStack{$name}{first_n} = $n; + # update overall number of entries + $SB_SERVER_CmdStack{$name}{cnt} = $SB_SERVER_CmdStack{$name}{last_n} - + $SB_SERVER_CmdStack{$name}{first_n} + 1; + } else { + # end of list reached + $SB_SERVER_CmdStack{$name}{last_n} = 0; + $SB_SERVER_CmdStack{$name}{first_n} = 0; + $SB_SERVER_CmdStack{$name}{cnt} = 0; + } + + return( $res ); +} + + +1; + +=pod + =begin html + + + =end html + =cut diff --git a/fhem/contrib/98_SB_PLAYER.pm b/fhem/contrib/98_SB_PLAYER.pm new file mode 100644 index 000000000..c15d0161d --- /dev/null +++ b/fhem/contrib/98_SB_PLAYER.pm @@ -0,0 +1,1250 @@ +# ############################################################################ +# +# FHEM Modue for Squeezebox Players +# +# ############################################################################ +# +# used to interact with Squeezebox Player +# +# ############################################################################ +# +# This is absolutley open source. Please feel free to use just as you +# like. Please note, that no warranty is given and no liability +# granted +# +# ############################################################################ +# +# we have the following readings +# state not yet implemented +# +# ############################################################################ +# +# we have the following attributes +# timer the time frequency how often we check +# volumeStep the volume delta when sending the up or down command +# timeout the timeout in seconds for the TCP connection +# +# ############################################################################ +# we have the following internals (all UPPERCASE) +# PLAYERIP the IP adress of the player in the network +# PLAYERID the unique identifier of the player. Mostly the MAC +# SERVER based on the IP and the port as given +# IP the IP of the server +# PORT the Port of the Server +# CLIPORT the port for the CLI interface of the server +# PLAYERNAME the name of the Player +# CONNECTION the connection status to the server +# CANPOWEROFF is the player supporting power off commands +# MODEL the model of the player +# DISPLAYTYPE what sort of display is there, if any +# +# ############################################################################ + +package main; +use strict; +use warnings; + +use IO::Socket; +use URI::Escape; + + +# include this for the self-calling timer we use later on +use Time::HiRes qw(gettimeofday); + +# the list of favorites +my %SB_PLAYER_Favs; + + +# ---------------------------------------------------------------------------- +# Initialisation routine called upon start-up of FHEM +# ---------------------------------------------------------------------------- +sub SB_PLAYER_Initialize( $ ) { + my ($hash) = @_; + + # the commands we provide to FHEM + # installs the respecitive call-backs for FHEM. The call back in quotes + # must be realised as a sub later on in the file + $hash->{DefFn} = "SB_PLAYER_Define"; + $hash->{UndefFn} = "SB_PLAYER_Undef"; + $hash->{ShutdownFn} = "SB_PLAYER_Shutdown"; + $hash->{SetFn} = "SB_PLAYER_Set"; + $hash->{GetFn} = "SB_PLAYER_Get"; + + # for the two step approach + $hash->{Match} = "^SB_PLAYER:"; + $hash->{ParseFn} = "SB_PLAYER_Parse"; + + # the attributes we have. Space separated list of attribute values in + # the form name:default1,default2 + $hash->{AttrList} = "volumeStep ttslanguage:de,en,fr "; + $hash->{AttrList} .= "ttslink "; + $hash->{AttrList} .= "donotnotify:true,false "; + $hash->{AttrList} .= "idismac:true,false "; + $hash->{AttrList} .= "serverautoon:true,false "; + $hash->{AttrList} .= "fadeinsecs "; + $hash->{AttrList} .= $readingFnAttributes; +} + + +# ---------------------------------------------------------------------------- +# Definition of a module instance +# called when defining an element via fhem.cfg +# ---------------------------------------------------------------------------- +sub SB_PLAYER_Define( $$ ) { + my ( $hash, $def ) = @_; + + my $name = $hash->{NAME}; + + my @a = split("[ \t][ \t]*", $def); + + # do we have the right number of arguments? + if( @a != 3 ) { + Log3( $hash, 1, "SB_PLAYER_Define: falsche Anzahl an Argumenten" ); + return( "wrong syntax: define SB_PLAYER " ); + } + + # needed for manual creation of the Player; autocreate checks in ParseFn + if( SB_PLAYER_IsValidMAC( $a[ 2 ] ) == 1 ) { + # the MAC adress is valid + $hash->{PLAYERMAC} = $a[ 2 ]; + } else { + my $msg = "SB_PLAYER_Define: playerid ist keine MAC Adresse " . + "im Format xx:xx:xx:xx:xx:xx oder xx-xx-xx-xx-xx-xx"; + Log3( $hash, 1, $msg ); + return( $msg ); + } + + Log3( $hash, 5, "SB_PLAYER_Define successfully called" ); + + # remove the : from the ID + my @idbuf = split( ":", $hash->{PLAYERMAC} ); + my $uniqueid = join( "", @idbuf ); + + # our unique id + $hash->{FHEMUID} = $uniqueid; + # do the alarms fade in + $hash->{ALARMSFADEIN} = "?"; + # the number of alarms of the player + $hash->{ALARMSCOUNT} = 2; + + # for the two step approach + $modules{SB_PLAYER}{defptr}{$uniqueid} = $hash; + AssignIoPort( $hash ); + + # preset the internals + # can the player power off + $hash->{CANPOWEROFF} = "?"; + # graphical or textual display + $hash->{DISPLAYTYPE} = "?"; + # which model do we see? + $hash->{MODEL} = "?"; + # what's the ip adress of the player + $hash->{PLAYERIP} = "?"; + # the name of the player as assigned by the server + $hash->{PLAYERNAME} = "?"; + # the last alarm we did set + $hash->{LASTALARM} = 1; + # the reference to the favorites list + $hash->{FAVREF} = " "; + # the command for selecting a favorite + $hash->{FAVSET} = "favorites"; + # the entry in the global hash table + $hash->{FAVSTR} = "not,yet,defined "; + # last received answer from the server + $hash->{LASTANSWER} = "none"; + + + # preset the attributes + if( !defined( $attr{$name}{volumeStep} ) ) { + $attr{$name}{volumeStep} = 10; + } + + if( !defined( $attr{$name}{fadeinsecs} ) ) { + $attr{$name}{fadeinsecs} = 10; + } + + if( !defined( $attr{$name}{donotnotify} ) ) { + $attr{$name}{donotnotify} = "true"; + } + + if( !defined( $attr{$name}{ttslanguage} ) ) { + $attr{$name}{ttslanguage} = "de"; + } + + if( !defined( $attr{$name}{idismac} ) ) { + $attr{$name}{idismac} = "true"; + } + + if( !defined( $attr{$name}{ttslink} ) ) { + $attr{$name}{ttslink} = "http://translate.google.com/translate_tts?"; + } + + if( !defined( $attr{$name}{serverautoon} ) ) { + $attr{$name}{serverautoon} = "true"; + } + + # Preset our readings if undefined + my $tn = TimeNow(); + + # according to development guidelines of FHEM AV Module + if( !defined( $hash->{READINGS}{presence}{VAL} ) ) { + $hash->{READINGS}{presence}{VAL} = "?"; + $hash->{READINGS}{presence}{TIME} = $tn; + } + + # according to development guidelines of FHEM AV Module + if( !defined( $hash->{READINGS}{power}{VAL} ) ) { + $hash->{READINGS}{power}{VAL} = "?"; + $hash->{READINGS}{power}{TIME} = $tn; + } + + # the last unkown command + if( !defined( $hash->{READINGS}{lastunkowncmd}{VAL} ) ) { + $hash->{READINGS}{lastunkowncmd}{VAL} = "none"; + $hash->{READINGS}{lastunkowncmd}{TIME} = $tn; + } + + # the last unkown IR command + if( !defined( $hash->{READINGS}{lastir}{VAL} ) ) { + $hash->{READINGS}{lastir}{VAL} = "?"; + $hash->{READINGS}{lastir}{TIME} = $tn; + } + + # the id of the alarm we create + if( !defined( $hash->{READINGS}{alarmid1}{VAL} ) ) { + $hash->{READINGS}{alarmid1}{VAL} = "none"; + $hash->{READINGS}{alarmid1}{TIME} = $tn; + } + + if( !defined( $hash->{READINGS}{alarmid2}{VAL} ) ) { + $hash->{READINGS}{alarmid2}{VAL} = "none"; + $hash->{READINGS}{alarmid2}{TIME} = $tn; + } + + # values according to standard + if( !defined( $hash->{READINGS}{playStatus}{VAL} ) ) { + $hash->{READINGS}{playStatus}{VAL} = "?"; + $hash->{READINGS}{playStatus}{TIME} = $tn; + } + + if( !defined( $hash->{READINGS}{currentArtist}{VAL} ) ) { + $hash->{READINGS}{currentArtist}{VAL} = "?"; + $hash->{READINGS}{currentArtist}{TIME} = $tn; + } + + if( !defined( $hash->{READINGS}{currentAlbum}{VAL} ) ) { + $hash->{READINGS}{currentAlbum}{VAL} = "?"; + $hash->{READINGS}{currentAlbum}{TIME} = $tn; + } + + if( !defined( $hash->{READINGS}{currentTitle}{VAL} ) ) { + $hash->{READINGS}{currentTitle}{VAL} = "?"; + $hash->{READINGS}{currentTitle}{TIME} = $tn; + } + + # for the FHEM AV Development Guidelinses + # we use this to store the currently playing ID to later on return to + if( !defined( $hash->{READINGS}{currentMedia}{VAL} ) ) { + $hash->{READINGS}{currentMedia}{VAL} = "?"; + $hash->{READINGS}{currentMedia}{TIME} = $tn; + } + + if( !defined( $hash->{READINGS}{currentPlaylistName}{VAL} ) ) { + $hash->{READINGS}{currentPlaylistName}{VAL} = "?"; + $hash->{READINGS}{currentPlaylistName}{TIME} = $tn; + } + + if( !defined( $hash->{READINGS}{currentPlaylisturl}{VAL} ) ) { + $hash->{READINGS}{currentPlaylistUrl}{VAL} = "?"; + $hash->{READINGS}{currentPlaylistUrl}{TIME} = $tn; + } + + if( !defined( $hash->{READINGS}{volume}{VAL} ) ) { + $hash->{READINGS}{volume}{VAL} = 0; + $hash->{READINGS}{volume}{TIME} = $tn; + } + + if( !defined( $hash->{READINGS}{volumeStraight}{VAL} ) ) { + $hash->{READINGS}{volumeStraight}{VAL} = "?"; + $hash->{READINGS}{volumeStraight}{TIME} = $tn; + } + + if( !defined( $hash->{READINGS}{connected}{VAL} ) ) { + $hash->{READINGS}{connected}{VAL} = "?"; + $hash->{READINGS}{connected}{TIME} = $tn; + } + + if( !defined( $hash->{READINGS}{signalstrength}{VAL} ) ) { + $hash->{READINGS}{signalstrength}{VAL} = "?"; + $hash->{READINGS}{currentTitle}{TIME} = $tn; + } + + if( !defined( $hash->{READINGS}{shuffle}{VAL} ) ) { + $hash->{READINGS}{shuffle}{VAL} = "?"; + $hash->{READINGS}{currentTitle}{TIME} = $tn; + } + + if( !defined( $hash->{READINGS}{repeat}{VAL} ) ) { + $hash->{READINGS}{repeat}{VAL} = "?"; + $hash->{READINGS}{currentTitle}{TIME} = $tn; + } + + if( !defined( $hash->{READINGS}{state}{VAL} ) ) { + $hash->{READINGS}{state}{VAL} = "?"; + $hash->{READINGS}{state}{TIME} = $tn; + } + + # check our + + # do and update of the status + InternalTimer( gettimeofday() + 10, + "SB_PLAYER_GetStatus", + $hash, + 0 ); + + return( undef ); +} + + +# ---------------------------------------------------------------------------- +# called from the global dispatch if new data is available +# ---------------------------------------------------------------------------- +sub SB_PLAYER_Parse( $$ ) { + my ( $iohash, $msg ) = @_; + + # we expect the data to be in the following format + # xxxxxxxxxxxx cmd1 cmd2 cmd3 ... + # where xxxxxxxxxxxx is derived from xx:xx:xx:xx:xx:xx + # that needs to be done by the server + + Log3( $iohash, 5, "SB_PLAYER_Parse: called with $msg" ); + + # storing the last in an array is necessery, for tagged responses + my ( $modtype, $id, @data ) = split(":", $msg, 3 ); + + Log3( $iohash, 5, "SB_PLAYER_Parse: type:$modtype, ID:$id CMD:@data" ); + + if( $modtype ne "SB_PLAYER" ) { + # funny stuff happens at the disptach function + Log3( $iohash, 5, "SB_PLAYER_Parse: wrong type given." ); + } + + # let's see what we got. Split the data at the space + # necessery, for tagged responses + my @args = split( " ", join( " ", @data ) ); + my $cmd = shift( @args ); + + + my $hash = $modules{SB_PLAYER}{defptr}{$id}; + if( !$hash ) { + Log3( undef, 3, "SB_PLAYER Unknown device with ID $id, " . + "please define it"); + + # do the autocreate; derive the unique id (MAC adress) + my @playermac = ( $id =~ m/.{2}/g ); + my $idbuf = join( ":", @playermac ); + + Log3( undef, 3, "SB_PLAYER Dervived the following MAC $idbuf " ); + + if( SB_PLAYER_IsValidMAC( $idbuf ) == 1 ) { + # the MAC Adress is valid + Log3( undef, 3, "SB_PLAYER_Parse: the unkown ID $id is a valid " . + "MAC Adress" ); + # this line supports autocreate + return( "UNDEFINED SB_PLAYER_$id SB_PLAYER $idbuf" ); + } else { + # the MAC adress is not valid + Log3( undef, 3, "SB_PLAYER_Parse: the unkown ID $id is NOT " . + "a valid MAC Adress" ); + return( undef ); + } + } + + # so the data is for us + my $name = $hash->{NAME}; + + Log3( $hash, 5, "SB_PLAYER_Parse: $name CMD:$cmd ARGS:@args..." ); + + # what ever we have received, signal it + $hash->{LASTANSWER} = "$cmd @args"; + + # signal the update to FHEM + readingsBeginUpdate( $hash ); + + if( $cmd eq "mixer" ) { + if( $args[ 0 ] eq "volume" ) { + # update the volume + if( scalar( $args[ 1 ] ) > 0 ) { + readingsSingleUpdate( $hash, "volume", + scalar( $args[ 1 ] ), 0 ); + } else { + readingsSingleUpdate( $hash, "volume", + "muted", 0 ); + } + readingsSingleUpdate( $hash, "volumeStraight", + scalar( $args[ 1 ] ), 0 ); + } + + } elsif( $cmd eq "play" ) { + readingsSingleUpdate( $hash, "playStatus", "playing", 0 ); + + } elsif( $cmd eq "stop" ) { + readingsSingleUpdate( $hash, "playStatus", "stopped", 0 ); + + } elsif( $cmd eq "pause" ) { + readingsBulkUpdate( $hash, "playStatus", "paused" ); + + } elsif( $cmd eq "mode" ) { + Log3( $hash, 1, "Playmode: $args[ 0 ]" ); + # alittle more complex to fulfill FHEM Development guidelines + if( $args[ 0 ] eq "play" ) { + readingsSingleUpdate( $hash, "playStatus", "playing", 0 ); + } elsif( $args[ 0 ] eq "stop" ) { + readingsSingleUpdate( $hash, "playStatus", "stopped", 0 ); + } elsif( $args[ 0 ] eq "pause" ) { + readingsSingleUpdate( $hash, "playStatus", "paused", 0 ); + } else { + readingsSingleUpdate( $hash, "playStatus", $args[ 0 ], 0 ); + } + + } elsif( $cmd eq "newmetadata" ) { + # the song has changed, but we are easy and just ask the player + # sending the requests causes endless loop + #IOWrite( $hash, "$hash->{PLAYERMAC} artist ?\n" ); + #IOWrite( $hash, "$hash->{PLAYERMAC} album ?\n" ); + #IOWrite( $hash, "$hash->{PLAYERMAC} title ?\n" ); + + } elsif( $cmd eq "playlist" ) { + if( $args[ 0 ] eq "newsong" ) { + # the song has changed, but we are easy and just ask the player + IOWrite( $hash, "$hash->{PLAYERMAC} artist ?\n" ); + IOWrite( $hash, "$hash->{PLAYERMAC} album ?\n" ); + IOWrite( $hash, "$hash->{PLAYERMAC} title ?\n" ); + + # the id is in the last return. ID not reported for radio stations + # so this will go wrong for e.g. Bayern 3 + if( $args[ $#args ] =~ /(^[0-9]{1,3})/g ) { + readingsBulkUpdate( $hash, "currentMedia", $1 ); + } + } elsif( $args[ 0 ] eq "cant_open" ) { + #TODO: needs to be handled + } elsif( $args[ 0 ] eq "open" ) { + $args[ 2 ] =~ /^(file:)(.*)/g; + if( defined( $2 ) ) { + readingsBulkUpdate( $hash, "currentMedia", $2 ); + } + } elsif( $args[ 0 ] eq "repeat" ) { + if( $args[ 1 ] eq "0" ) { + readingsBulkUpdate( $hash, "repeat", "off" ); + } elsif( $args[ 1 ] eq "1") { + readingsBulkUpdate( $hash, "repeat", "one" ); + } elsif( $args[ 1 ] eq "2") { + readingsBulkUpdate( $hash, "repeat", "all" ); + } else { + readingsBulkUpdate( $hash, "repeat", "?" ); + } + } elsif( $args[ 0 ] eq "shuffle" ) { + if( $args[ 1 ] eq "0" ) { + readingsBulkUpdate( $hash, "shuffle", "off" ); + } elsif( $args[ 1 ] eq "1") { + readingsBulkUpdate( $hash, "shuffle", "song" ); + } elsif( $args[ 1 ] eq "2") { + readingsBulkUpdate( $hash, "shuffle", "album" ); + } else { + readingsBulkUpdate( $hash, "shuffle", "?" ); + } + } elsif( $args[ 0 ] eq "name" ) { + shift( @args ); + readingsBulkUpdate( $hash, "currentPlaylistName", + join( " ", @args ) ); + } elsif( $args[ 0 ] eq "url" ) { + shift( @args ); + readingsBulkUpdate( $hash, "currentPlaylistUrl", + join( " ", @args ) ); + + } else { + } + # chekc if this caused going to play, as not send automatically + IOWrite( $hash, "$hash->{PLAYERMAC} mode ?\n" ); + + } elsif( $cmd eq "playlistcontrol" ) { + #playlistcontrol cmd:load artist_id:22 count:4 + + } elsif( $cmd eq "connected" ) { + readingsBulkUpdate( $hash, "connected", $args[ 0 ] ); + readingsBulkUpdate( $hash, "presence", "present" ); + + } elsif( $cmd eq "name" ) { + $hash->{PLAYERNAME} = join( " ", @args ); + + } elsif( $cmd eq "title" ) { + readingsBulkUpdate( $hash, "currentTitle", join( " ", @args ) ); + + } elsif( $cmd eq "artist" ) { + readingsBulkUpdate( $hash, "currentArtist", join( " ", @args ) ); + + } elsif( $cmd eq "album" ) { + readingsBulkUpdate( $hash, "currentAlbum", join( " ", @args ) ); + + } elsif( $cmd eq "player" ) { + if( $args[ 0 ] eq "model" ) { + $hash->{MODEL} = $args[ 1 ]; + } elsif( $args[ 0 ] eq "canpoweroff" ) { + $hash->{CANPOWEROFF} = $args[ 1 ]; + } elsif( $args[ 0 ] eq "ip" ) { + $hash->{PLAYERIP} = "$args[ 1 ]"; + if( defined( $args[ 2 ] ) ) { + $hash->{PLAYERIP} .= ":$args[ 2 ]"; + } + + } else { + } + + } elsif( $cmd eq "power" ) { + if( $args[ 0 ] eq "1" ) { + readingsSingleUpdate( $hash, "state", "on", 1 ); + readingsSingleUpdate( $hash, "power", "on", 0 ); + } else { + readingsSingleUpdate( $hash, "state", "off", 1 ); + readingsSingleUpdate( $hash, "power", "off", 0 ); + readingsSingleUpdate( $hash, "presence", "absent", 0 ); + } + + } elsif( $cmd eq "displaytype" ) { + $hash->{DISPLAYTYPE} = $args[ 0 ]; + + } elsif( $cmd eq "signalstrength" ) { + if( $args[ 0 ] eq "0" ) { + readingsBulkUpdate( $hash, "signalstrength", "wired" ); + } else { + readingsBulkUpdate( $hash, "signalstrength", "$args[ 0 ]" ); + } + + } elsif( $cmd eq "alarm" ) { + if( $args[ 0 ] eq "sound" ) { + # fired when an alarm goes off + } elsif( $args[ 0 ] eq "end" ) { + # fired when an alarm ends + } elsif( $args[ 0 ] eq "snooze" ) { + # fired when an alarm is snoozed by the user + } elsif( $args[ 0 ] eq "snooze_end" ) { + # fired when an alarm comes back from snooze + } elsif( $args[ 0 ] eq "add" ) { + # fired when an alarm has been added. + # this setup goes wrong, when an alarm is defined manually + # the last entry in the array shall contain th id + my $idstr = $args[ $#args ]; + if( $idstr =~ /^(id:)([0-9a-zA-Z\.]+)/g ) { + readingsBulkUpdate( $hash, "alarmid$hash->{LASTALARM}", $2 ); + } else { + } + } else { + } + + + } elsif( $cmd eq "alarms" ) { + SB_PLAYER_ParseAlarms( $hash, @args ); + + } elsif( $cmd eq "showbriefly" ) { + # to be ignored, we get two hashes + + } elsif( $cmd eq "unkownir" ) { + readingsSingleUpdate( $hash, "lastir", $args[ 0 ], 1 ); + + } elsif( $cmd eq "status" ) { + # TODO + Log3( $hash, 5, "SB_PLAYER_Parse($name): please implement the " . + "parser for the status answer" ); + + } elsif( $cmd eq "prefset" ) { + if( $args[ 0 ] eq "server" ) { + if( $args[ 1 ] eq "currentSong" ) { + readingsBulkUpdate( $hash, "currentMedia", $args[ 2 ] ); + } + } else { + readingsSingleUpdate( $hash, "lastunkowncmd", + $cmd . " " . join( " ", @args ), 1 ); + } + + + } elsif( $cmd eq "NONE" ) { + # we shall never end up here, as cmd=NONE is used by the server for + # autocreate + + } else { + # unkown command, we push it to the last command thingy + readingsSingleUpdate( $hash, "lastunkowncmd", + $cmd . " " . join( " ", @args ), 1 ); + } + + # and signal the end of the readings update + + if( AttrVal( $name, "donotnotify", "false" ) eq "true" ) { + readingsEndUpdate( $hash, 0 ); + } else { + readingsEndUpdate( $hash, 1 ); + } + + Log3( $hash, 5, "SB_PLAYER_Parse: $name: leaving" ); + + return( $name ); +} + +# ---------------------------------------------------------------------------- +# Undefinition of an SB_PLAYER +# called when undefining (delete) and element +# ---------------------------------------------------------------------------- +sub SB_PLAYER_Undef( $$$ ) { + my ($hash, $arg) = @_; + + Log3( $hash, 5, "SB_PLAYER_Undef: called" ); + + RemoveInternalTimer( $hash ); + + # to be reviewed if that works. + # check for uc() + # what is $hash->{DEF}? + delete $modules{SB_PLAYER}{defptr}{uc($hash->{DEF})}; + + return( undef ); +} + +# ---------------------------------------------------------------------------- +# Shutdown function - called before fhem shuts down +# ---------------------------------------------------------------------------- +sub SB_PLAYER_Shutdown( $$ ) { + my ($hash, $dev) = @_; + + RemoveInternalTimer( $hash ); + + Log3( $hash, 5, "SB_PLAYER_Shutdown: called" ); + + return( undef ); +} + + +# ---------------------------------------------------------------------------- +# Get of a module +# called upon get cmd, arg1, arg2, .... +# ---------------------------------------------------------------------------- +sub SB_PLAYER_Get( $@ ) { + my ($hash, @a) = @_; + + #my $name = $hash->{NAME}; + + Log3( $hash, 1, "SB_PLAYER_Get: called with @a" ); + + my $name = shift( @a ); + my $cmd = shift( @a ); + + # if( int( @a ) != 2 ) { + # my $msg = "SB_PLAYER_Get: $name: wrong number of arguments"; + # Log3( $hash, 5, $msg ); + # return( $msg ); + # } + + if( $cmd eq "volume" ) { + return( scalar( ReadingsVal( "$name", "volumeStraight", 25 ) ) ); + } else { + my $msg = "SB_PLAYER_Get: $name: unkown argument"; + Log3( $hash, 5, $msg ); + return( $msg ); + } + + return( undef ); +} + +# ---------------------------------------------------------------------------- +# Set of a module +# called upon set cmd, arg1, arg2, .... +# ---------------------------------------------------------------------------- +sub SB_PLAYER_Set( $@ ) { + my ( $hash, $name, $cmd, @arg ) = @_; + + #my $name = $hash->{NAME}; + + Log3( $hash, 5, "SB_PLAYER_Set: called with $cmd" ); + + # check if we have received a command + if( !defined( $cmd ) ) { + my $msg = "$name: set needs at least one parameter"; + Log3( $hash, 3, $msg ); + return( $msg ); + } + + # now parse the commands + if( $cmd eq "?" ) { + # this one should give us a drop down list + my $res = "Unknown argument ?, choose one of " . + "on off stop:noArg play:noArg pause:noArg " . + "volume:slider,0,1,100 " . + "volumeUp:noArg volumeDown:noArg " . + "mute:noArg repeat:off,one,all show statusRequest:noArg " . + "shuffle:on,off next:noArg prev:noArg playlist sleep " . + "alarm1 alarm2 allalarms:enable,disable cliraw talk "; + # add the favorites + $res .= $hash->{FAVSET} . ":" . $hash->{FAVSTR} . " "; + + return( $res ); + } + + # as we have some other command, we need to turn on the server + #if( AttrVal( $name, "serverautoon", "true" ) eq "true" ) { +# SB_PLAYER_ServerTurnOn( $hash ); +# } + + + if( ( $cmd eq "Stop" ) || ( $cmd eq "STOP" ) || ( $cmd eq "stop" ) ) { + IOWrite( $hash, "$hash->{PLAYERMAC} stop\n" ); + + } elsif( ( $cmd eq "Play" ) || ( $cmd eq "PLAY" ) || ( $cmd eq "play" ) ) { + my $secbuf = AttrVal( $name, "fadeinsecs", 10 ); + IOWrite( $hash, "$hash->{PLAYERMAC} play $secbuf\n" ); + + } elsif( ( $cmd eq "Pause" ) || ( $cmd eq "PAUSE" ) || ( $cmd eq "pause" ) ) { + my $secbuf = AttrVal( $name, "fadeinsecs", 10 ); + if( @arg == 1 ) { + if( $arg[ 0 ] eq "1" ) { + # pause the player + IOWrite( $hash, "$hash->{PLAYERMAC} pause 1 $secbuf\n" ); + } else { + # unpause the player + IOWrite( $hash, "$hash->{PLAYERMAC} pause 0 $secbuf\n" ); + } + } else { + IOWrite( $hash, "$hash->{PLAYERMAC} pause $secbuf\n" ); + } + + } elsif( ( $cmd eq "next" ) || ( $cmd eq "NEXT" ) || ( $cmd eq "Next" ) || + ( $cmd eq "channelUp" ) || ( $cmd eq "CHANNELUP" ) ) { + IOWrite( $hash, "$hash->{PLAYERMAC} playlist jump %2B1\n" ); + + } elsif( ( $cmd eq "prev" ) || ( $cmd eq "PREV" ) || ( $cmd eq "Prev" ) || + ( $cmd eq "channelDown" ) || ( $cmd eq "CHANNELDOWN" ) ) { + IOWrite( $hash, "$hash->{PLAYERMAC} playlist jump %2D1\n" ); + + } elsif( ( $cmd eq "volume" ) || ( $cmd eq "VOLUME" ) || + ( $cmd eq "Volume" ) ||( $cmd eq "volumeStraight" ) ) { + if( @arg != 1 ) { + my $msg = "SB_PLAYER_Set: no arguments for Vol given."; + Log3( $hash, 3, $msg ); + return( $msg ); + } + # set the volume to the desired level. Needs to be 0..100 + # no error checking here, as the server does this + IOWrite( $hash, "$hash->{PLAYERMAC} mixer volume $arg[ 0 ]\n" ); + + } elsif( $cmd eq $hash->{FAVSET} ) { + if( defined( $SB_PLAYER_Favs{$name}{$arg[0]}{ID} ) ) { + my $fid = $SB_PLAYER_Favs{$name}{$arg[0]}{ID}; + IOWrite( $hash, "$hash->{PLAYERMAC} favorites playlist " . + "play item_id:$fid\n" ); + } + + + } elsif( ( $cmd eq "volumeUp" ) || ( $cmd eq "VOLUMEUP" ) || + ( $cmd eq "VolumeUp" ) ) { + #SB_PLAYER_HTTPWrite( $hash, "mixer", "volume", + #"%2B$attr{$name}{volumeStep} " ); + my $volstr = sprintf( "+%02d", $attr{$name}{volumeStep} ); + IOWrite( $hash, "$hash->{PLAYERMAC} mixer volume $volstr\n" ); + + } elsif( ( $cmd eq "volumeDown" ) || ( $cmd eq "VOLUMEDOWN" ) || + ( $cmd eq "VolumeDown" ) ) { + #SB_PLAYER_HTTPWrite( $hash, "mixer", "volume", + # "%2D$attr{$name}{volumeStep}" ); + my $volstr = sprintf( "-%02d", $attr{$name}{volumeStep} ); + IOWrite( $hash, "$hash->{PLAYERMAC} mixer volume $volstr\n" ); + + } elsif( ( $cmd eq "mute" ) || ( $cmd eq "MUTE" ) || ( $cmd eq "Mute" ) ) { + IOWrite( $hash, "$hash->{PLAYERMAC} mixer muting toggle\n" ); + + } elsif( $cmd eq "on" ) { + if( $hash->{CANPOWEROFF} eq "0" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} play\n" ); + } else { + IOWrite( $hash, "$hash->{PLAYERMAC} power 1\n" ); + } + + } elsif( $cmd eq "off" ) { + # off command to go here + if( $hash->{CANPOWEROFF} eq "0" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} stop\n" ); + } else { + IOWrite( $hash, "$hash->{PLAYERMAC} power 0\n" ); + } + + } elsif( ( $cmd eq "repeat" ) || ( $cmd eq "REPEAT" ) || + ( $cmd eq "Repeat" ) ) { + if( @arg != 1 ) { + my $msg = "SB_PLAYER_Set: no arguments for repeat given."; + Log3( $hash, 3, $msg ); + return( $msg ); + } + if( $arg[ 0 ] eq "off" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} playlist repeat 0\n" ); + } elsif( $arg[ 0 ] eq "one" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} playlist repeat 1\n" ); + } elsif( $arg[ 0 ] eq "all" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} playlist repeat 2\n" ); + } else { + my $msg = "SB_PLAYER_Set: unknown argument for repeat given."; + Log3( $hash, 3, $msg ); + return( $msg ); + } + + } elsif( ( $cmd eq "shuffle" ) || ( $cmd eq "SHUFFLE" ) || + ( $cmd eq "Shuffle" ) ) { + if( @arg != 1 ) { + my $msg = "SB_PLAYER_Set: no arguments for shuffle given."; + Log3( $hash, 3, $msg ); + return( $msg ); + } + if( $arg[ 0 ] eq "off" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} playlist shuffle 0\n" ); + } elsif( $arg[ 0 ] eq "on" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} playlist shuffle 1\n" ); + } else { + my $msg = "SB_PLAYER_Set: unknown argument for shuffle given."; + Log3( $hash, 3, $msg ); + return( $msg ); + } + + } elsif( ( $cmd eq "show" ) || + ( $cmd eq "SHOW" ) || + ( $cmd eq "Show" ) ) { + # set show line1:text line2:text duration:ss + my $v = join( " ", @arg ); + my @buf = split( "line1:", $v ); + @buf = split( "line2:", $buf[ 1 ] ); + my $line1 = uri_escape( $buf[ 0 ] ); + @buf = split( "duration:", $buf[ 1 ] ); + my $line2 = uri_escape( $buf[ 0 ] ); + my $duration = $buf[ 1 ]; + my $cmdstr = "$hash->{PLAYERMAC} display $line1 $line2 $duration\n"; + IOWrite( $hash, $cmdstr ); + + } elsif( ( $cmd eq "talk" ) || + ( $cmd eq "TALK" ) || + ( $cmd eq "talk" ) ) { + my $outstr = AttrVal( $name, "ttslink", "none" ); + $outstr .= "tl=" . AttrVal( $name, "ttslanguage", "de" ) . "&q="; + $outstr .= join( "+", @arg ); + $outstr = uri_escape( $outstr ); + + Log3( $hash, 5, "SB_PLAYER_Set: talk: $name: $outstr" ); + + # example for making it speak some google text-to-speech + IOWrite( $hash, "$hash->{PLAYERMAC} playlist play " . $outstr . "\n" ); + + } elsif( ( $cmd eq "playlist" ) || + ( $cmd eq "PLAYLIST" ) || + ( $cmd eq "Playlist" ) ) { + if( ( @arg != 2 ) && ( @arg != 3 ) ) { + my $msg = "SB_PLAYER_Set: no arguments for Playlist given."; + Log3( $hash, 3, $msg ); + return( $msg ); + } + if( @arg == 1 ) { + if( $arg[ 0 ] eq "track" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} playlist loadtracks " . + "track.titlesearch:$arg[ 1 ]\n" ); + } elsif( $arg[ 0 ] eq "album" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} playlist loadtracks " . + "album.titlesearch:$arg[ 1 ]\n" ); + } elsif( $arg[ 0 ] eq "artist" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} playlist loadtracks " . + "contributor.titlesearch:$arg[ 1 ]\n" ); + } else { + } + + } elsif( @arg == 3 ) { + Log3( $hash, 5, "SB_PLAYER_Set($name): implement identifiers with " . + "spaces etc. inside" ); + # the spaces might need %20 so we might need some more here + # please introduce a fromat like genre:xxx album:xxx artist:xxx + # and then run the results through uri_escape + IOWrite( $hash, "$hash->{PLAYERMAC} playlist loadalbum $arg[ 0 ] " . + "$arg[ 1 ] $arg[ 2 ]\n" ); + } else { + # what the f... we checked beforehand + } + + } elsif( $cmd eq "allalarms" ) { + if( $arg[ 0 ] eq "enable" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} alarm enableall\n" ); + } elsif( $arg[ 0 ] eq "disable" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} alarm disableall\n" ); + } else { + } + + + } elsif( index( $cmd, "alarm" ) != -1 ) { + my $alarmno = int( substr( $cmd, 5 ) ) + 0; + Log3( $hash, 5, "SB_PLAYER_Set: $name: alarmid:$alarmno" ); + return( SB_PLAYER_Alarm( $hash, $alarmno, @arg ) ); + + } elsif( ( $cmd eq "sleep" ) || ( $cmd eq "SLEEP" ) || + ( $cmd eq "Sleep" ) ) { + # split the time string up + my @buf = split( ":", $arg[ 0 ] ); + if( scalar( @buf ) != 3 ) { + my $msg = "SB_PLAYER_Set: please use hh:mm:ss for sleep time."; + Log3( $hash, 3, $msg ); + return( $msg ); + } + my $secs = ( $buf[ 0 ] * 3600 ) + ( $buf[ 1 ] * 60 ) + $buf[ 2 ]; + IOWrite( $hash, "$hash->{PLAYERMAC} sleep $secs\n" ); + return( undef ); + + } elsif( ( $cmd eq "cliraw" ) || ( $cmd eq "CLIRAW" ) || + ( $cmd eq "Cliraw" ) ) { + # write raw messages to the CLI interface per player + my $v = join( " ", @arg ); + + Log3( $hash, 5, "SB_PLAYER_Set: cliraw: $v " ); + IOWrite( $hash, "$hash->{PLAYERMAC} $v\n" ); + return( undef ); + + } elsif( $cmd eq "statusRequest" ) { + RemoveInternalTimer( $hash ); + SB_PLAYER_GetStatus( $hash ); + + + } else { + my $msg = "SB_PLAYER_Set: unsupported command given"; + Log3( $hash, 3, $msg ); + return( $msg ); + } + + return( undef ); + +} + + +# ---------------------------------------------------------------------------- +# set Alarms of the Player +# ---------------------------------------------------------------------------- +sub SB_PLAYER_Alarm( $$@ ) { + my ( $hash, $n, @arg ) = @_; + + my $name = $hash->{NAME}; + + if( ( $n != 1 ) && ( $n != 2 ) ) { + Log3( $hash, 1, "SB_PLAYER_Alarm: $name: wrong ID given. Must be 1|2" ); + return; + } + + my $id = ReadingsVal( "$name", "alarmid$n", "none" ); + + Log3( $hash, 5, "SB_PLAYER_Alarm: $name: ID:$id, N:$n" ); + my $cmdstr = ""; + + if( $arg[ 0 ] eq "set" ) { + # set alarm set 0..6 hh:mm:ss playlist + if( ( @arg != 4 ) && ( @arg != 3 ) ) { + my $msg = "SB_PLAYER_Set: not enough arguments for alarm given."; + Log3( $hash, 3, $msg ); + return( $msg ); + } + + if( $id ne "none" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} alarm delete $id\n" ); + readingsSingleUpdate( $hash, "alarmid$n", "none", 0 ); + } + + my $dow = $arg[ 1 ]; + + # split the time string up + my @buf = split( ":", $arg[ 2 ] ); + if( scalar( @buf ) != 3 ) { + my $msg = "SB_PLAYER_Set: please use hh:mm:ss for alarm time."; + Log3( $hash, 3, $msg ); + return( $msg ); + } + my $secs = ( $buf[ 0 ] * 3600 ) + ( $buf[ 1 ] * 60 ) + $buf[ 2 ]; + + $cmdstr = "$hash->{PLAYERMAC} alarm add dow:$dow repeat:0 enabled:1"; + if( defined( $arg[ 3 ] ) ) { + $cmdstr .= " playlist:" . $arg[ 3 ]; + } + $cmdstr .= " time:$secs\n"; + + IOWrite( $hash, $cmdstr ); + + $hash->{LASTALARM} = $n; + + } elsif( $arg[ 0 ] eq "enable" ) { + if( $id ne "none" ) { + $cmdstr = "$hash->{PLAYERMAC} alarm update id:$id "; + $cmdstr .= "enabled:1\n"; + IOWrite( $hash, $cmdstr ); + } + + } elsif( $arg[ 0 ] eq "disable" ) { + if( $id ne "none" ) { + $cmdstr = "$hash->{PLAYERMAC} alarm update id:$id "; + $cmdstr .= "enabled:0\n"; + IOWrite( $hash, $cmdstr ); + } + + } elsif( $arg[ 0 ] eq "volume" ) { + if( $id ne "none" ) { + $cmdstr = "$hash->{PLAYERMAC} alarm update id:$id "; + $cmdstr .= "volume:" . $arg[ 1 ] . "\n"; + IOWrite( $hash, $cmdstr ); + } + + } elsif( $arg[ 0 ] eq "delete" ) { + if( $id ne "none" ) { + $cmdstr = "$hash->{PLAYERMAC} alarm delete id:$id\n"; + IOWrite( $hash, $cmdstr ); + readingsSingleUpdate( $hash, "alarmid$n", "none", 1 ); + } + + } else { + my $msg = "SB_PLAYER_Set: unkown argument for alarm given."; + Log3( $hash, 3, $msg ); + return( $msg ); + } + + return( undef ); +} + + +# ---------------------------------------------------------------------------- +# Status update - just internal use and invoked by the timer +# ---------------------------------------------------------------------------- +sub SB_PLAYER_GetStatus( $ ) { + my ($hash) = @_; + my $name = $hash->{NAME}; + my $strbuf = ""; + + Log3( $hash, 5, "SB_PLAYER_GetStatus: called" ); + + # we fire the respective questions and parse the answers in parse + IOWrite( $hash, "$hash->{PLAYERMAC} mode ?\n" ); + IOWrite( $hash, "$hash->{PLAYERMAC} artist ?\n" ); + IOWrite( $hash, "$hash->{PLAYERMAC} album ?\n" ); + IOWrite( $hash, "$hash->{PLAYERMAC} title ?\n" ); + IOWrite( $hash, "$hash->{PLAYERMAC} mixer volume ?\n" ); + IOWrite( $hash, "$hash->{PLAYERMAC} signalstrength ?\n" ); + IOWrite( $hash, "$hash->{PLAYERMAC} playlist shuffle ?\n" ); + IOWrite( $hash, "$hash->{PLAYERMAC} playlist repeat ?\n" ); + IOWrite( $hash, "$hash->{PLAYERMAC} playlist name ?\n" ); + IOWrite( $hash, "$hash->{PLAYERMAC} playlist url ?\n" ); + + # the other values below are provided by our server. we don't + # need to ask again + if( $hash->{PLAYERIP} eq "?" ) { + # the server doesn't care about us + IOWrite( $hash, "$hash->{PLAYERMAC} player ip ?\n" ); + } + if( $hash->{MODEL} eq "?" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} player model ?\n" ); + } + + if( $hash->{CANPOWEROFF} eq "?" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} player canpoweroff ?\n" ); + } + + if( $hash->{PLAYERNAME} eq "?" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} name ?\n" ); + } + + if( ReadingsVal( $name, "state", "?" ) eq "?" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} power ?\n" ); + } + + if( ReadingsVal( $name, "connected", "?" ) eq "?" ) { + IOWrite( $hash, "$hash->{PLAYERMAC} connected ?\n" ); + } + + # do and update of the status + InternalTimer( gettimeofday() + 300, + "SB_PLAYER_GetStatus", + $hash, + 0 ); + + Log3( $hash, 5, "SB_PLAYER_GetStatus: leaving" ); + + return( ); +} + + +# ---------------------------------------------------------------------------- +# called from the IODev for Broadcastmessages +# ---------------------------------------------------------------------------- +sub SB_PLAYER_RecBroadcast( $$@ ) { + my ( $hash, $cmd, $msg, $bin ) = @_; + + my $name = $hash->{NAME}; + + Log3( $hash, 5, "SB_PLAYER_Broadcast($name): called with $msg" ); + + # let's see what we got. Split the data at the space + my @args = split( " ", $msg ); + + if( $cmd eq "SERVER" ) { + # a message from the server + if( $args[ 0 ] eq "OFF" ) { + # the server is off, so are we + RemoveInternalTimer( $hash ); + readingsSingleUpdate( $hash, "state", "off", 1 ); + } elsif( $args[ 0 ] eq "ON" ) { + # the server is back + # do and update of the status + InternalTimer( gettimeofday() + 10, + "SB_PLAYER_GetStatus", + $hash, + 0 ); + } else { + # unkown broadcast message + } + } elsif( $cmd eq "FAVORITES" ) { + if( $args[ 0 ] eq "ADD" ) { + # format: ADD IODEVname ID shortentry + $SB_PLAYER_Favs{$name}{$args[3]}{ID} = $args[ 2 ]; + if( $hash->{FAVSTR} eq "" ) { + $hash->{FAVSTR} = $args[ 3 ]; + } else { + $hash->{FAVSTR} .= "," . $args[ 3 ]; + } + + } elsif( $args[ 0 ] eq "FLUSH" ) { + undef( %{$SB_PLAYER_Favs{$name}} ); + $hash->{FAVSTR} = ""; + + } else { + } + } else { + + } + +} + + +# ---------------------------------------------------------------------------- +# parse the return on the alarms status +# ---------------------------------------------------------------------------- +sub SB_PLAYER_ParseAlarams( $@ ) { + my ( $hash, @data ) = @_; + + my $name = $hash->{NAME}; + + if( $data[ 0 ] =~ /^([0-9])*/ ) { + shift( @data ); + } + + if( $data[ 0 ] =~ /^([0-9])*/ ) { + shift( @data ); + } + + if( $data[ 0 ] =~ /^(fade:)([0|1]?)/ ) { + shift( @data ); + if( $2 eq "0" ) { + $hash->{ALARMSFADEIN} = "yes"; + } else { + $hash->{ALARMSFADEIN} = "no"; + } + + } + + if( $data[ 0 ] =~ /^(count:)([0-9].*)/ ) { + shift( @data ); + $hash->{ALARMSCOUNT} = scalar( $2 ); + } + + if( $hash->{ALARMSCOUNT} > 2 ) { + Log3( $hash, 2, "SB_PLAYER_Alarms($name): Player has more than " . + "two alarms. So not fully under control by FHEM" ); + } + +} + + + +# ---------------------------------------------------------------------------- +# used for checking, if the string contains a valid MAC adress +# ---------------------------------------------------------------------------- +sub SB_PLAYER_IsValidMAC( $ ) { + my $instr = shift( @_ ); + + my $d = "[0-9A-Fa-f]"; + my $dd = "$d$d"; + + if( $instr =~ /($dd([:-])$dd(\2$dd){4})/og ) { + return( 1 ); + } else { + return( 0 ); + } +} + +# ---------------------------------------------------------------------------- +# used to turn on our server +# ---------------------------------------------------------------------------- +sub SB_PLAYER_ServerTurnOn( $ ) { + my ( $hash ) = @_; + my $name = $hash->{NAME}; + + my $servername; + + Log3( $hash, 5, "SB_PLAYER_ServerTurnOn($name): please implement me" ); + + return; + + fhem( "set $servername on" ); +} + + +# DO NOT WRITE BEYOND THIS LINE +1; + +=pod + =begin html + + +

SB_PLAYER

+
    + Define a Squeezebox Player. Help needs to be done still. +

    + + + Define +
      + define <name> SB_PLAYER +

      + + Example: +
        +
      +
    +
    + + + Set +
      + set <name> <value>
      + Set any value. +
    +
    + + + Get
      N/A

    + + + Attributes +
      +
    • setList
      + Space separated list of commands, which will be returned upon "set name ?", + so the FHEMWEB frontend can construct a dropdown and offer on/off + switches. Example: attr SB_PLAYERName setList on off +
    • +
    • readingFnAttributes
    • +
    +
    + +
+ + =end html + =cut diff --git a/fhem/contrib/README b/fhem/contrib/README index 0ebb461e5..e4d9c330e 100755 --- a/fhem/contrib/README +++ b/fhem/contrib/README @@ -6,6 +6,8 @@ Support for the heatpump controller LUXTRONIK2 used by Alpha-Innotec and Siemens and probably some other vendors, too. LUX2.gplot is one example plotting template. +- 97_SB_SERVER.pm & 98_SB_PLAYER + Squeezebox module from bugster_de - 70_SCIVT.pm Support for an SCD series solar controler device. Details see http://english.ivt-hirschau.de/content.php?parent_id=CAT_64&doc_id=DOC_118