# ############################################################################ # $Id$ # # FHEM Module for Squeezebox Servers # # ############################################################################ # # used to interact with Squeezebox server # # ############################################################################ # # Written by bugster_de # # Contributions from: Siggi85, Oliv06, ChrisD, Eberhard # # ############################################################################ # # 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 password? # # ############################################################################ # # 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; # include for using the perl ping command use Net::Ping; use Encode qw(decode encode); # CD 0009 hinzugefügt #use Text::Unidecode; no if $] >= 5.017011, warnings => 'experimental::smartmatch'; # 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; my @SB_SERVER_AL_PLS; my @SB_SERVER_FAVS; my @SB_SERVER_PLS; my @SB_SERVER_SM; # include this for the self-calling timer we use later on use Time::HiRes qw(gettimeofday time); use constant { true => 1, false => 0 }; use constant { TRUE => 1, FALSE => 0 }; use constant SB_SERVER_VERSION => '0040'; my $SB_SERVER_hasDataDumper = 1; # CD 0024 sub SB_SERVER_RemoveInternalTimers($); # ---------------------------------------------------------------------------- # 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->{NotifyFn} = "SB_SERVER_Notify"; $hash->{AttrList} = "alivetimer maxfavorites "; $hash->{AttrList} .= "doalivecheck:true,false "; $hash->{AttrList} .= "maxcmdstack "; $hash->{AttrList} .= "httpport "; $hash->{AttrList} .= "enablePlugins "; $hash->{AttrList} .= "ignoredIPs ignoredMACs internalPingProtocol:icmp,tcp,udp,syn,stream,none "; # CD 0021 none hinzugefügt $hash->{AttrList} .= $readingFnAttributes; # CD 0024 eval "use Data::Dumper"; $SB_SERVER_hasDataDumper = 0 if($@); } # CD 0032 start sub SB_SERVER_SetAttrList( $ ) { my ($hash) = @_; my $attrList; $attrList = "alivetimer maxfavorites "; $attrList .= "doalivecheck:true,false "; $attrList .= "maxcmdstack "; $attrList .= "httpport "; $attrList .= "ignoredIPs ignoredMACs internalPingProtocol:icmp,tcp,udp,syn,stream,none "; # CD 0021 none hinzugefügt my $applist="enablePlugins"; if (defined($hash->{helper}{apps})) { $applist.=":multiple-strict"; foreach my $app ( sort keys %{$hash->{helper}{apps}} ) { $applist.=",$app"; } } $attrList .= "$applist "; $attrList .= $readingFnAttributes; $modules{$defs{$hash->{NAME}}{TYPE}}{AttrList}=$attrList; } # CD 0032 end # ---------------------------------------------------------------------------- # 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] " . # CD 0007 changed PASSWord to PASSWORD "[RCC:RCC_Name] [WOL:WOLName] [PRESENCE:PRESENCEName]" ); # CD 0007 added PRESENCE } # 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->{PRESENCENAME} = "none"; # CD 0007 $hash->{RCCNAME} = "none"; $hash->{USERNAME} = "?"; $hash->{PASSWORD} = "?"; my ($user,$password); my @newDef; # parse the user spec foreach( @a ) { if( $_ =~ /^(RCC:)(.*)/ ) { $hash->{RCCNAME} = $2; push @newDef,$_; next; } elsif( $_ =~ /^(WOL:)(.*)/ ) { $hash->{WOLNAME} = $2; push @newDef,$_; next; } elsif( $_ =~ /^(PRESENCE:)(.*)/ ) { # CD 0007 $hash->{PRESENCENAME} = $2; # CD 0007 push @newDef,$_; next; # CD 0007 } elsif( $_ =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d{3,5})/ ) { $hash->{IP} = $1; $hash->{CLIPORT} = $2; push @newDef,$_; next; } elsif( $_ =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/ ) { $hash->{IP} = $1; $hash->{CLIPORT} = 9090; push @newDef,$_; next; } elsif( $_ =~ /^(USER:)(.*)/ ) { $user=$2 if($2 ne 'yes'); $hash->{USERNAME} = 'yes'; push @newDef,'USER:yes'; } elsif( $_ =~ /^(PASSWORD:)(.*)/ ) { $password=$2 if($2 ne 'yes'); $hash->{PASSWORD} = 'yes'; push @newDef,'PASSWORD:yes'; } else { push @newDef,$_; next; } } # CD 0031 if(defined($user) && defined($password)) { SB_SERVER_storePassword($hash,$user,$password); $hash->{DEF} = join(' ',@newDef); } $hash->{LASTANSWER} = "none"; # used for alive checking of the CLI interface $hash->{ALIVECHECK} = "?"; # the status of the CLI connection (on / off) $hash->{CLICONNECTION} = "?"; # 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; } # the port of the HTTP interface as needed for the coverart url if( !defined( $attr{$name}{httpport} ) ) { $attr{$name}{httpport} = "9000"; } # 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 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; $hash->{CMDSTACK}=0; # CD 0007 # assign our IO Device $hash->{DeviceName} = "$hash->{IP}:$hash->{CLIPORT}"; $hash->{helper}{pingCounter}=0; # CD 0004 $hash->{helper}{lastPRESENCEstate}='?'; # CD 0023 $hash->{helper}{onAfterAliveCheck}=0; # CD 0038 # CD 0009 set module version, needed for reload $hash->{helper}{SB_SERVER_VERSION}=SB_SERVER_VERSION; if (!defined($hash->{OLDDEF})) { # CD 0024 SB_SERVER_LoadSyncGroups($hash) if($SB_SERVER_hasDataDumper==1); SB_SERVER_LoadServerStates($hash) if($SB_SERVER_hasDataDumper==1); SB_SERVER_FixSyncGroupNames($hash); # CD 0027 SB_SERVER_UpdateSgReadings($hash); # CD 0027 } else { # CD 0038 if( ReadingsVal($name, "state", "unknown") eq "opened" ) { DevIo_SimpleWrite( $hash, "listen 0\n", 0 ); } $hash->{helper}{disableReconnect}=1; SB_SERVER_RemoveInternalTimers( $hash ); readingsSingleUpdate( $hash, "power", "off", 1 ); SB_SERVER_Broadcast( $hash, "SERVER", "OFF" ); DevIo_Disconnected( $hash ); SB_SERVER_setStates($hash, "disconnected"); } # open the IO device my $ret; # CD wait for init_done if ($init_done>0){ delete($hash->{NEXT_OPEN}) if($hash->{NEXT_OPEN}); # CD 0007 reconnect immediately after modify # CD 0016 start #if( ReadingsVal($name, "state", "unknown") eq "opened" ) { # CD 0038 state statt STATE verwenden # DevIo_CloseDev( $hash ); # readingsSingleUpdate( $hash, "power", "?", 0 ); # #$hash->{STATE}="disconnected"; #} # CD 0016 end if (defined($hash->{OLDDEF})) { InternalTimer( gettimeofday() + 1, "SB_SERVER_tcb_Alive", "SB_SERVER_Alive:$name", 0 ); $ret=undef; } else { $ret= DevIo_OpenDev($hash, 0, "SB_SERVER_DoInit" ); } } # do and update of the status # CD disabled #InternalTimer( gettimeofday() + 10, # "SB_SERVER_Alive", # $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 SB_SERVER_RemoveInternalTimers( $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 SB_SERVER_RemoveInternalTimers( $hash ); return( undef ); } # ---------------------------------------------------------------------------- # ReadyFn - called when? # ---------------------------------------------------------------------------- sub SB_SERVER_Ready( $ ) { my ($hash) = @_; my $name = $hash->{NAME}; #Log3( $hash, 4, "SB_SERVER_Ready: called" ); # check for bad/missing password if (defined($hash->{helper}{SB_SERVER_LMS_Status})) { if (time()-($hash->{helper}{SB_SERVER_LMS_Status})<2) { if( ( $hash->{USERNAME} ne "?" ) && ( $hash->{PASSWORD} ne "?" ) ) { $hash->{LASTANSWER}='invalid username or password ?'; Log( 1, "SB_SERVER_Ready($name): invalid username or password ?" ); } else { $hash->{LASTANSWER}='missing username and password ?'; Log( 1, "SB_SERVER_Ready($name): missing username and password ?" ); } $hash->{NEXT_OPEN}=time()+60; } delete($hash->{helper}{SB_SERVER_LMS_Status}); } # we need to re-open the device if( ReadingsVal($name, "state", "unknown") eq "disconnected" ) { # CD 0038 state statt STATE verwenden if( ( ReadingsVal( $name, "power", "on" ) eq "on" ) || ( ReadingsVal( $name, "power", "on" ) eq "?" ) ) { # obviously the first we realize the Server is off # clean up first if ($hash->{helper}{onAfterAliveCheck}==0) { # CD 0038 SB_SERVER_RemoveInternalTimers( $hash ); readingsSingleUpdate( $hash, "power", "off", 1 ); $hash->{CLICONNECTION} = "off"; # CD 0007 # and signal to our clients SB_SERVER_Broadcast( $hash, "SERVER", "OFF" ); } } # CD added init_done if ($init_done>0) { # CD 0007 faster reconnect after WOL, use PRESENCE my $reconnect=0; if(defined($hash->{helper}{WOLFastReconnectUntil})) { $hash->{TIMEOUT}=1; if (time() > $hash->{helper}{WOLFastReconnectNext}) { delete($hash->{NEXT_OPEN}) if($hash->{NEXT_OPEN}); $hash->{helper}{WOLFastReconnectNext}=time()+15; $reconnect=1; } if (time() > $hash->{helper}{WOLFastReconnectUntil}) { delete($hash->{TIMEOUT}); delete($hash->{helper}{WOLFastReconnectUntil}); delete($hash->{helper}{WOLFastReconnectNext}); } } if( ReadingsVal( $hash->{PRESENCENAME}, "state", "present" ) eq "present" ) { $reconnect=1; } if (($reconnect==1)&&(!defined($hash->{helper}{disableReconnect}))) { return( DevIo_OpenDev( $hash, 1, "SB_SERVER_DoInit") ); } else { return undef; } } else { return undef; } } } # ---------------------------------------------------------------------------- # 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 $hash = $defs{$name}; my @args = @_; Log( 4, "SB_SERVER_Attr($name): called with @args" ); if( $args[ 0 ] eq "alivetimer" ) { if( $cmd eq "set" ) { # CD 0021 start RemoveInternalTimer( "SB_SERVER_Alive:$name"); InternalTimer( gettimeofday() + $args[ 1 ], "SB_SERVER_tcb_Alive", "SB_SERVER_Alive:$name", 0 ); # CD 0021 end } } elsif( $args[ 0 ] eq "httpport" ) { # CD 0015 bei Änderung des Ports diesen an Clients schicken if( $cmd eq "set" ) { SB_SERVER_Broadcast( $hash, "SERVER", "IP " . $hash->{IP} . ":" . $args[ 1 ] ); } } elsif( $args[ 0 ] eq "enablePlugins" ) { # CD 0070 bei Änderung Status abfragen if( $cmd eq "set" ) { if($init_done>0) { if(defined($hash->{helper}{apps})) { my @enabledApps=split(',',$args[ 1 ]); foreach my $app (@enabledApps) { if(defined($hash->{helper}{apps}{$app})) { DevIo_SimpleWrite( $hash, ($hash->{helper}{apps}{$app}{cmd})." items 0 200\n", 0 ); } } } } } else { if(defined($hash->{helper}{apps})) { my @enabledApps=split(',',AttrVal($name,'enablePlugins','')); foreach my $app (@enabledApps) { if(defined($hash->{helper}{apps}{$app})) { SB_SERVER_Broadcast( $hash, "PLAYLISTS", "FLUSH ".($hash->{helper}{apps}{$app}{cmd}), undef ); SB_SERVER_Broadcast( $hash, "FAVORITES", "FLUSH ".($hash->{helper}{apps}{$app}{cmd}), undef ); } } } delete($hash->{helper}{apps}) if(defined($hash->{helper}{apps})); delete($hash->{helper}{appcmd}) if(defined($hash->{helper}{appcmd})); DevIo_SimpleWrite( $hash, "apps 0 200\n", 0 ); } } return; # 0033 betateilchen/mahowi } # ---------------------------------------------------------------------------- # Set function # ---------------------------------------------------------------------------- sub SB_SERVER_Set( $@ ) { my ($hash, @a) = @_; my $name = $hash->{NAME}; Log( 4, "SB_SERVER_Set($name): called" ); 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 "; #$res .= "addToFHEMUpdate:noArg removeFromFHEMUpdate:noArg "; # CD 0019 $res .= "syncGroup "; # CD 0024 $res .= "save "; # CD 0025 #$res .= "getData "; # CD 0030 my $out=""; if (defined($hash->{helper}{savedServerStates})) { foreach my $pl ( keys %{$hash->{helper}{savedServerStates}} ) { $out.=$pl."," unless ($pl=~/xxxSgTalkxxx/); # CD 0027 xxxSgTalkxxx hinzugefügt } $out=~s/,$//; } $res .= "recall:$out "; 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" ); $hash->{helper}{WOLFastReconnectUntil}=time()+120; # CD 0007 $hash->{helper}{WOLFastReconnectNext}=time()+30; # CD 0007 } if( $hash->{RCCNAME} ne "none" ) { fhem( "set $hash->{RCCNAME} on" ); } } } elsif( $cmd eq "renew" ) { Log3( $hash, 5, "SB_SERVER_Set: renew" ); delete $hash->{helper}{disableReconnect} if (defined($hash->{helper}{disableReconnect})); # CD 0038 if( ReadingsVal( $name, "state", "unknown" ) eq "opened" ) { # CD 0038 if((defined($a[0])) && ($a[0] eq 'soft')) { DevIo_SimpleWrite( $hash, "listen 1\n", 0 ); } else { DevIo_SimpleWrite( $hash, "listen 0\n", 0 ); $hash->{helper}{disableReconnect}=1; SB_SERVER_RemoveInternalTimers( $hash ); readingsSingleUpdate( $hash, "power", "off", 1 ); SB_SERVER_Broadcast( $hash, "SERVER", "OFF" ); DevIo_Disconnected( $hash ); InternalTimer( gettimeofday() + 5, "SB_SERVER_tcb_Alive", "SB_SERVER_Alive:$name", 0 ); } } else { # CD 0038 force open readingsSingleUpdate( $hash, "power", "off", 1 ); delete($hash->{NEXT_OPEN}) if($hash->{NEXT_OPEN}); SB_SERVER_Alive($hash); } } elsif( $cmd eq "abort" ) { DevIo_SimpleWrite( $hash, "listen 0\n", 0 ) if( ReadingsVal( $name, "state", "unknown" ) eq "opened" ); if((defined($a[0])) && ($a[0] eq 'soft')) { } else { $hash->{helper}{disableReconnect}=1; DevIo_Disconnected( $hash ); readingsSingleUpdate( $hash, "power", "off", 1 ); SB_SERVER_Broadcast( $hash, "SERVER", "OFF" ); SB_SERVER_RemoveInternalTimers( $hash ); } } elsif( $cmd eq "statusRequest" ) { Log3( $hash, 5, "SB_SERVER_Set: statusRequest" ); DevIo_SimpleWrite( $hash, "version ?\n", 0 ); DevIo_SimpleWrite( $hash, "serverstatus 0 200\n", 0 ); DevIo_SimpleWrite( $hash, "favorites items 0 " . AttrVal( $name, "maxfavorites", 100 ) . " want_url:1\n", # CD 0009 url mit abfragen 0 ); DevIo_SimpleWrite( $hash, "playlists 0 200\n", 0 ); DevIo_SimpleWrite( $hash, "alarm playlists 0 300\n", 0 ); # CD 0011 # CD 0032 start DevIo_SimpleWrite( $hash, "apps 0 200\n", 0 ); if(defined($hash->{helper}{apps})) { my @enabledApps=split(',',AttrVal($name,'enablePlugins','')); foreach my $app (@enabledApps) { if(defined($hash->{helper}{apps}{$app})) { DevIo_SimpleWrite( $hash, ($hash->{helper}{apps}{$app}{cmd})." items 0 200\n", 0 ); } } } # CD 0032 end } 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 " ); DevIo_SimpleWrite( $hash, $v, 0 ); # CD 0016 IOWrite in DevIo_SimpleWrite geändert } elsif( $cmd eq "rescan" ) { DevIo_SimpleWrite( $hash, $cmd . " " . $a[ 0 ] . "\n", 0 ); # CD 0016 IOWrite in DevIo_SimpleWrite geändert # CD 0018 start } elsif( $cmd eq "addToFHEMUpdate" ) { fhem("update add https://raw.githubusercontent.com/ChrisD70/FHEM-Modules/master/autoupdate/sb/controls_squeezebox.txt"); } elsif( $cmd eq "removeFromFHEMUpdate" ) { fhem("update delete https://raw.githubusercontent.com/ChrisD70/FHEM-Modules/master/autoupdate/sb/controls_squeezebox.txt"); # CD 0018 end # CD 0024 start } elsif( $cmd eq "syncGroup" ) { return( "at least one parameter is needed" ) if( @a < 1 ); my $updateReadings=0; my $subcmd=shift( @a ); if($subcmd eq 'addp') { return( "not enough parameters" ) if( @a < 2 ); return( "too many parameters" ) if( @a > 2 ); # CD 0027 my $players=$a[0]; my $statename=$a[1]; $statename =~ s/[,;:]/_/g; # CD 0027 Sonderzeichen ersetzen SB_SERVER_BuildPlayerList($hash) if(!defined($hash->{helper}{players})); if(!defined($hash->{helper}{syncGroups}{$statename})) { $hash->{helper}{syncGroups}{$statename}{0}{fhemname}=''; $hash->{helper}{syncGroups}{$statename}{0}{lmsname}=''; $hash->{helper}{syncGroups}{$statename}{0}{mac}=''; $hash->{helper}{syncGroups}{$statename}{0}{c}=1; $updateReadings=1; } for my $pl (split(",",$players)) { my $found=0; foreach my $e ( keys %{$hash->{helper}{syncGroups}{$statename}} ) { if($e ne '') { if(defined($hash->{helper}{syncGroups}{$statename}{$e})) { if(($pl eq $hash->{helper}{syncGroups}{$statename}{$e}{fhemname}) or ($pl eq $hash->{helper}{syncGroups}{$statename}{$e}{lmsname})) { $found=1; last; } } } } if($found==0) { if(defined($hash->{helper}{players}) && defined($hash->{helper}{players}{$pl})) { my $c=$hash->{helper}{syncGroups}{$statename}{0}{c}; $hash->{helper}{syncGroups}{$statename}{$c}{fhemname}=$hash->{helper}{players}{$pl}{fhemname}; $hash->{helper}{syncGroups}{$statename}{$c}{lmsname}=$hash->{helper}{players}{$pl}{lmsname}; $hash->{helper}{syncGroups}{$statename}{$c}{mac}=$hash->{helper}{players}{$pl}{mac}; if($hash->{helper}{syncGroups}{$statename}{0}{fhemname} eq '') { $hash->{helper}{syncGroups}{$statename}{0}{fhemname}=$hash->{helper}{players}{$pl}{fhemname}; $hash->{helper}{syncGroups}{$statename}{0}{lmsname}=$hash->{helper}{players}{$pl}{lmsname}; $hash->{helper}{syncGroups}{$statename}{0}{mac}=$hash->{helper}{players}{$pl}{mac}; } $hash->{helper}{syncGroups}{$statename}{0}{c}+=1; $updateReadings=1; # CD 0028 } } } } elsif($subcmd eq 'removep') { return( "not enough parameters" ) if( @a < 2 ); return( "too many parameters" ) if( @a > 2 ); # CD 0027 my $players=$a[0]; my $statename=$a[1]; $statename =~ s/[,;:]/_/g; # CD 0027 Sonderzeichen ersetzen if((!defined($hash->{helper}{syncGroups}))||(!defined($hash->{helper}{syncGroups}{$statename}))) { return( "sync group $statename not found" ); } SB_SERVER_BuildPlayerList($hash) if(!defined($hash->{helper}{players})); for my $pl (split(",",$players)) { foreach my $e ( keys %{$hash->{helper}{syncGroups}{$statename}} ) { if($e ne '') { if(defined($hash->{helper}{syncGroups}{$statename}{$e})) { if(($pl eq $hash->{helper}{syncGroups}{$statename}{$e}{fhemname}) or ($pl eq $hash->{helper}{syncGroups}{$statename}{$e}{lmsname})) { if($e eq '0') { # master ? $hash->{helper}{syncGroups}{$statename}{0}{fhemname}=''; $hash->{helper}{syncGroups}{$statename}{0}{lmsname}=''; $hash->{helper}{syncGroups}{$statename}{0}{mac}=''; } else { delete($hash->{helper}{syncGroups}{$statename}{$e}); } $updateReadings=1; # CD 0028 } } } } } # neuen Master suchen ? if($hash->{helper}{syncGroups}{$statename}{0}{fhemname} eq '') { foreach my $e ( keys %{$hash->{helper}{syncGroups}{$statename}} ) { if(($e ne '')&&($e ne '0')) { $hash->{helper}{syncGroups}{$statename}{0}{fhemname}=$hash->{helper}{syncGroups}{$statename}{$e}{fhemname}; $hash->{helper}{syncGroups}{$statename}{0}{lmsname}=$hash->{helper}{syncGroups}{$statename}{$e}{lmsname}; $hash->{helper}{syncGroups}{$statename}{0}{mac}=$hash->{helper}{syncGroups}{$statename}{$e}{mac}; last; } } } } elsif($subcmd eq 'masterp') { return( "not enough parameters" ) if( @a < 2 ); return( "too many parameters" ) if( @a > 2 ); # CD 0027 my $pl=$a[0]; my $statename=$a[1]; $statename =~ s/[,;:]/_/g; # CD 0027 Sonderzeichen ersetzen if((!defined($hash->{helper}{syncGroups}))||(!defined($hash->{helper}{syncGroups}{$statename}))) { return( "sync group $statename not found" ); } foreach my $e ( keys %{$hash->{helper}{syncGroups}{$statename}} ) { if(($e ne '')&&($e ne '0')) { if(defined($hash->{helper}{syncGroups}{$statename}{$e})) { if(($pl eq $hash->{helper}{syncGroups}{$statename}{$e}{fhemname}) or ($pl eq $hash->{helper}{syncGroups}{$statename}{$e}{lmsname})) { $hash->{helper}{syncGroups}{$statename}{0}{fhemname}=$hash->{helper}{syncGroups}{$statename}{$e}{fhemname}; $hash->{helper}{syncGroups}{$statename}{0}{lmsname}=$hash->{helper}{syncGroups}{$statename}{$e}{lmsname}; $hash->{helper}{syncGroups}{$statename}{0}{mac}=$hash->{helper}{syncGroups}{$statename}{$e}{mac}; $updateReadings=1; } } } } } elsif($subcmd eq 'load') { my $poweron=0; if($a[0] eq 'poweron') { $poweron=1; shift(@a); } return( "not enough parameters" ) if( @a == 0 ); my $statename=$a[0]; $statename =~ s/[,;:]/_/g; # CD 0027 Sonderzeichen ersetzen SB_SERVER_LoadSyncGroup($hash, $statename, $poweron); # CD 0027 } elsif($subcmd eq 'delete') { my $statename=$a[0]; $statename =~ s/[,;:]/_/g; # CD 0027 Sonderzeichen ersetzen delete($hash->{helper}{syncGroups}{$statename}) if(defined($hash->{helper}{syncGroups}{$statename})); delete($defs{$name}{READINGS}{"sg$statename"}) if(defined($defs{$name}{READINGS}{"sg$statename"})); # CD 0027 $updateReadings=1; # CD 0027 start } elsif($subcmd eq 'deleteall') { foreach my $e ( keys %{$hash->{helper}{syncGroups}} ) { if($e ne '') { if(defined($hash->{helper}{syncGroups}{$e})) { delete($hash->{helper}{syncGroups}{$e}); delete($defs{$name}{READINGS}{"sg$e"}) if(defined($defs{$name}{READINGS}{"sg$e"})); $updateReadings=1; } } } } elsif($subcmd eq 'talk') { return "talk already in progress" if(defined($hash->{helper}{sgTalkActivePlayer})); my $poweron=0; if($a[0] eq 'poweron') { $poweron=1; shift(@a); } return( "not enough parameters" ) if( @a < 2 ); my $statename=shift(@a); $statename =~ s/[,;:]/_/g; if((!defined($hash->{helper}{syncGroups}))||(!defined($hash->{helper}{syncGroups}{$statename}))) { return( "sync group $statename not found" ); } # Zustand speichern, Gruppe laden, talk verzögert an Player absetzen SB_SERVER_Save($hash, 'xxxSgTalkxxx'); SB_SERVER_LoadSyncGroup($hash, $statename, $poweron); $hash->{helper}{sgTalkPlayers}=ReadingsVal($name,"sg$statename","-"); if($hash->{helper}{sgTalkPlayers} eq '-') { Log3( $hash, 2, "SB_SERVER_Set($name): sgtalk: no players found for group $statename"); return "no players found"; } $hash->{helper}{sgTalkActivePlayer}='waiting for power on'; $hash->{helper}{sgTalkData}=join(' ', @a); $hash->{helper}{sgTalkTimeoutPowerOn}=time()+3; $hash->{helper}{sgTalkGroup}=$statename; # CD 0031 RemoveInternalTimer( "StartTalk:$name"); InternalTimer( gettimeofday() + 0.01, "SB_SERVER_tcb_StartTalk", "StartTalk:$name", 0 ); } elsif($subcmd eq 'resettts') { SB_SERVER_Recall($hash,'xxxSgTalkxxx del'); delete $hash->{helper}{sgTalkActivePlayer}; } elsif($subcmd eq 'fixnames') { SB_SERVER_FixSyncGroupNames($hash); SB_SERVER_UpdateSgReadings($hash); # CD 0027 end # CD 0031 start } elsif($subcmd eq 'volume') { return( "not enough parameters" ) if( @a != 2 ); my $statename=$a[0]; $statename =~ s/[,;:]/_/g; # CD 0027 Sonderzeichen ersetzen my $vol=$a[1]; if (SB_SERVER_isSyncGroupActive($hash,$statename)==1) { foreach my $e ( keys %{$hash->{helper}{syncGroups}{$statename}} ) { if(($e ne '')&&($e ne '0')) { if(defined($hash->{helper}{syncGroups}{$statename}{$e})) { fhem("set ".$hash->{helper}{syncGroups}{$statename}{$e}{fhemname}." volume ".$vol." nosync"); } } } } else { return "Sync group $statename not active"; } # CD 0031 end # CD 0031 nur zu Testzwecken } elsif($subcmd eq 'isActive') { return( "not enough parameters" ) if( @a == 0 ); my $statename=$a[0]; $statename =~ s/[,;:]/_/g; # CD 0027 Sonderzeichen ersetzen return SB_SERVER_isSyncGroupActive($hash, $statename); } else { return( "unknown command $subcmd" ); } # CD 0031 end # CD 0025 start if($updateReadings==1) { SB_SERVER_UpdateSgReadings($hash); } # CD 0025 end # CD 0024 end # CD 0025 start } elsif( $cmd eq "save" ) { if(defined($a[0])) { SB_SERVER_Save($hash, $a[0]); } else { SB_SERVER_Save($hash, ""); } } elsif( $cmd eq "recall" ) { if(defined($a[0])) { SB_SERVER_Recall($hash, $a[0]); } else { SB_SERVER_Recall($hash, ""); } # CD 0025 end # CD 0030 start } elsif( $cmd eq "getData" ) { return( "not enough parameters" ) if( @a < 3 ); $hash->{helper}{getData}{$a[2]}{format}=$a[0]; $hash->{helper}{getData}{$a[2]}{reading}=$a[1]; if($a[2] eq 'artists') { DevIo_SimpleWrite( $hash, "artists 0 5000\n", 0 ); } # CD 0030 end } else { ; } return( undef ); } # CD 0027 start sub SB_SERVER_tcb_StartTalk($) { my($in ) = shift; my(undef,$name) = split(':',$in); my $hash = $defs{$name}; return unless defined($hash->{helper}{sgTalkActivePlayer}); # alle gesynced ? if (SB_SERVER_isSyncGroupActive($hash,$hash->{helper}{sgTalkGroup})==1) { # eingeschalteten Player suchen my @pls=split(',',$hash->{helper}{sgTalkPlayers}); foreach my $pl (@pls) { if(ReadingsVal($pl,'power','0') eq 'on') { $hash->{helper}{sgTalkActivePlayer}=$pl; my $phash = $defs{$pl}; # CD 0029 $phash->{helper}{sgTalkActive}=1; # CD 0029 fhem "set $pl talk " . $hash->{helper}{sgTalkData}; last; } } } # keinen eingeschalteten Player gefunden if($hash->{helper}{sgTalkTimeoutPowerOn}{helper}{sgTalkActivePlayer}; } else { # warten... if($hash->{helper}{sgTalkActivePlayer} eq 'waiting for power on') { RemoveInternalTimer( "StartTalk:$name"); InternalTimer( gettimeofday() + 0.2, "SB_SERVER_tcb_StartTalk", "StartTalk:$name", 0 ); } } } # CD 0031 start sub SB_SERVER_isSyncGroupActive($$) { my ($hash,$statename) = @_; my $name = $hash->{NAME}; if((!defined($hash->{helper}{syncGroups}))||(!defined($hash->{helper}{syncGroups}{$statename}))) { return 0; } my $master=$hash->{helper}{syncGroups}{$statename}{0}{lmsname}; my $isActive=1; foreach my $e ( keys %{$hash->{helper}{syncGroups}{$statename}} ) { if(($e ne '')&&($e ne '0')) { if(defined($hash->{helper}{syncGroups}{$statename}{$e})) { $isActive=0 if (InternalVal($hash->{helper}{syncGroups}{$statename}{$e}{fhemname},'SYNCMASTERPN','?') ne $master) } } } return $isActive; } # CD 0031 end sub SB_SERVER_LoadSyncGroup($$$) { my ($hash,$statename,$poweron) = @_; my $name = $hash->{NAME}; Log3( $hash, 3, "SB_SERVER_LoadSyncGroup($name): load: $statename, poweron: $poweron"); if((!defined($hash->{helper}{syncGroups}))||(!defined($hash->{helper}{syncGroups}{$statename}))) { return( "sync group $statename not found" ); } # unsync all foreach my $e ( keys %{$hash->{helper}{syncGroups}{$statename}} ) { if(($e ne '')&&($e ne '0')) { if(defined($hash->{helper}{syncGroups}{$statename}{$e})) { SB_SERVER_Write( $hash, $hash->{helper}{syncGroups}{$statename}{$e}{mac}." sync -\n", "" ); } } } # sync with new master foreach my $e ( keys %{$hash->{helper}{syncGroups}{$statename}} ) { if(($e ne '')&&($e ne '0')) { if(defined($hash->{helper}{syncGroups}{$statename}{$e})) { if($hash->{helper}{syncGroups}{$statename}{$e}{mac} ne $hash->{helper}{syncGroups}{$statename}{0}{mac}) { SB_SERVER_Write( $hash, $hash->{helper}{syncGroups}{$statename}{0}{mac}." sync ".$hash->{helper}{syncGroups}{$statename}{$e}{mac}."\n", "" ); } SB_SERVER_Write( $hash, $hash->{helper}{syncGroups}{$statename}{$e}{mac}." power 1\n", "" ) if($poweron==1); } } } } sub SB_SERVER_FixSyncGroupNames($) { my ($hash) = @_; my $name = $hash->{NAME}; foreach my $e ( keys %{$hash->{helper}{syncGroups}} ) { if($e ne '') { if(defined($hash->{helper}{syncGroups}{$e})) { if($e =~ /[,;:\s]/) { my $n=$e; $n =~ s/[,;:\s]/_/g; $hash->{helper}{syncGroups}{$n}=$hash->{helper}{syncGroups}{$e}; delete $hash->{helper}{syncGroups}{$e}; } } } } } sub SB_SERVER_UpdateSgReadings($) { my ($hash) = @_; my $name = $hash->{NAME}; my $sg=''; readingsBeginUpdate( $hash ); foreach my $e ( keys %{$hash->{helper}{syncGroups}} ) { if($e ne '') { if(defined($hash->{helper}{syncGroups}{$e})) { $sg.="$e,"; my $sgd=''; foreach my $p ( keys %{$hash->{helper}{syncGroups}{$e}} ) { if($p ne '') { if(defined($hash->{helper}{syncGroups}{$e}{$p})) { if(($hash->{helper}{syncGroups}{$e}{$p}{fhemname} ne $hash->{helper}{syncGroups}{$e}{0}{fhemname})||($p eq '0')) { # CD 0029 $sgd.=$hash->{helper}{syncGroups}{$e}{$p}{fhemname} . ","; } } } } $sgd =~ s/,$//; if($sgd eq '') { delete($defs{$name}{READINGS}{"sg$e"}) if(defined($defs{$name}{READINGS}{"sg$e"})); } else { if(ReadingsVal($name,"sg$e","x") ne $sgd) { readingsBulkUpdate( $hash, "sg$e", $sgd ); } } } } } $sg =~ s/,$//; if($sg eq '') { delete($defs{$name}{READINGS}{"syncGroups"}) if(defined($defs{$name}{READINGS}{"syncGroups"})); } else { readingsBulkUpdate( $hash, "syncGroups", $sg ); } readingsEndUpdate( $hash, 1 ); } # CD 0027 end # ---------------------------------------------------------------------------- # Read # called from the global loop, when the select for hash->{FD} reports data # ---------------------------------------------------------------------------- sub SB_SERVER_Read( $ ) { my ($hash) = @_; my $name = $hash->{NAME}; #my $start = time; # CD 0019 Log3( $hash, 4, "SB_SERVER_Read($name): called" ); Log3( $hash, 5, "+++++++++++++++++++++++++++++++++++++++++++++++++++++" ); Log3( $hash, 5, "New Squeezebox Server Read cycle starts here" ); Log3( $hash, 5, "+++++++++++++++++++++++++++++++++++++++++++++++++++++" ); my $buf = DevIo_SimpleRead( $hash ); if( !defined( $buf ) ) { return( "" ); } # 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 implement the " . # CD 0009 Meldung deaktiviert # "sending of the CMDStack." ); # CD 0009 Meldung deaktiviert } #my $t1 = time; # CD 0020 # 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" ); # CD TEST 6 # CD 0021 start - Server lebt noch, alivetimer neu starten RemoveInternalTimer( "SB_SERVER_Alive:$name"); InternalTimer( gettimeofday() + AttrVal( $name, "alivetimer", 10 ), "SB_SERVER_tcb_Alive", "SB_SERVER_Alive:$name", 0 ); # CD 0021 end #my $t2 = time; # CD 0020 # 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} = ""; } #my $t3 = time; # CD 0020 # and dispatch the rest foreach( @cmds ) { #my $t31=time; # CD 0020 # double check complete line my $lastchar = substr( $_, -1); SB_SERVER_DispatchCommandLine( $hash, $_ ); # CD 0020 start #if((time-$t31)>0.2) { # Log3($hash,0,"SB_SERVER_Read($name), time:".int((time-$t31)*1000)." cmd: ".$_); #} # CD 0020 end } #my $t4 = time; # CD 0020 # CD 0009 check for reload of newer version $hash->{helper}{SB_SERVER_VERSION}=0 if (!defined($hash->{helper}{SB_SERVER_VERSION})); # CD 0012 if ($hash->{helper}{SB_SERVER_VERSION} ne SB_SERVER_VERSION) { Log3( $hash, 1,"SB_SERVER_Read: SB_SERVER_VERSION changed from ".$hash->{helper}{SB_SERVER_VERSION}." to ".SB_SERVER_VERSION); # CD 0012 $hash->{helper}{SB_SERVER_VERSION}=SB_SERVER_VERSION; DevIo_SimpleWrite( $hash, "version ?\n", 0 ); DevIo_SimpleWrite( $hash, "serverstatus 0 200\n", 0 ); DevIo_SimpleWrite( $hash, "favorites items 0 " . AttrVal( $name, "maxfavorites", 100 ) . " want_url:1\n", # CD 0009 url mit abfragen 0 ); DevIo_SimpleWrite( $hash, "playlists 0 200\n", 0 ); } # CD 0009 end Log3( $hash, 5, "+++++++++++++++++++++++++++++++++++++++++++++++++++++" ); Log3( $hash, 5, "Squeezebox Server Read cycle ends here" ); Log3( $hash, 5, "+++++++++++++++++++++++++++++++++++++++++++++++++++++" ); # CD 0019 start #my $end = time; #if (($end - $start)>1) { # Log3( $hash, 0, "SB_SERVER_Read($name), times: ".int(($t1 - $start)*1000)." ".int(($t2 - $t1)*1000)." ".int(($t3 - $t2)*1000)." ".int(($t4 - $t3)*1000)." ".int(($end - $start)*1000)." nCmds: ".$#cmds ); #} # CD 0019 end return( undef ); } # CD 0027 start sub SB_SERVER_tcb_RecallAfterTalk($) { my($in ) = shift; my(undef,$name) = split(':',$in); my $hash = $defs{$name}; return unless defined($hash->{helper}{sgTalkActivePlayer}); SB_SERVER_Recall($hash,'xxxSgTalkxxx del'); delete $hash->{helper}{sgTalkActivePlayer}; } # CD 0027 end # ---------------------------------------------------------------------------- # called by the clients to send data # ---------------------------------------------------------------------------- sub SB_SERVER_Write( $$$ ) { my ( $hash, $fn, $msg ) = @_; my $name = $hash->{NAME}; Log3( $hash, 4, "SB_SERVER_Write($name): called with FN:$fn" ); # unless($fn=~m/\?/); # CD TEST 4 if( !defined( $fn ) ) { return( undef ); } if( defined( $msg ) ) { Log3( $hash, 4, "SB_SERVER_Write: MSG:$msg" ); } # CD 0012 fhemrelay Meldungen nicht an den LMS schicken sondern direkt an Dispatch übergeben if($fn =~ m/fhemrelay/) { # CD 0027 start if ($fn =~ m/ttsdone/) { my @a=split(' ',$fn); # sg talk auf Player aktiv ? if(defined($hash->{helper}{sgTalkActivePlayer})) { if($a[0] eq $hash->{helper}{sgTalkActivePlayer}) { # recall auslösen RemoveInternalTimer( "RecallAfterTalk:$name"); InternalTimer( gettimeofday() + 0.01, "SB_SERVER_tcb_RecallAfterTalk", "RecallAfterTalk:$name", 0 ); } } } else { # CD 0027 end SB_SERVER_DispatchCommandLine( $hash, $fn ); } return( undef ); } if( ReadingsVal( $name, "serversecure", "0" ) eq "1" ) { if( ( $hash->{USERNAME} ne "?" ) && ( $hash->{PASSWORD} ne "?" ) ) { # we need to send username and password 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($name): called" ); if( !$hash->{TCPDev} ) { Log3( $hash, 2, "SB_SERVER_DoInit: no TCPDev available?" ); # CD 0009 level 5->2 DevIo_CloseDev( $hash ); } my $state=ReadingsVal($name, "state", "unknown"); Log3( $hash, 3, "SB_SERVER_DoInit($name): state: " . $state . " power: ". ReadingsVal( $name, "power", "X" )); # CD 0009 level 2 -> 3 # CD 0038 state statt STATE verwenden if( $state eq "disconnected" ) { # CD 0038 state statt STATE verwenden # server is off after FHEM start, broadcast to clients if( ( ReadingsVal( $name, "power", "on" ) eq "on" ) || ( ReadingsVal( $name, "power", "on" ) eq "?" ) ) { Log3( $hash, 3, "SB_SERVER_DoInit($name): " . # CD 0009 level 2 -> 3 "SB-Server in hibernate / suspend?." ); # obviously the first we realize the Server is off readingsSingleUpdate( $hash, "power", "off", 1 ); # and signal to our clients SB_SERVER_Broadcast( $hash, "SERVER", "OFF" ); SB_SERVER_Broadcast( $hash, "SERVER", "IP " . $hash->{IP} . ":" . AttrVal( $name, "httpport", "9000" ) ); } return( 1 ); } elsif( $state eq "opened" ) { # CD 0038 state statt STATE verwenden $hash->{ALIVECHECK} = "?"; $hash->{CLICONNECTION} = "on"; if( ( ReadingsVal( $name, "power", "on" ) eq "off" ) || ( ReadingsVal( $name, "power", "on" ) eq "?" ) || ($hash->{helper}{onAfterAliveCheck}==1)) { # CD 0038 Log3( $hash, 3, "SB_SERVER_DoInit($name): " . # CD 0009 level 2 -> 3 "SB-Server is back again." ); # CD 0007 cleanup if(defined($hash->{helper}{WOLFastReconnectUntil})) { delete($hash->{TIMEOUT}); delete($hash->{helper}{WOLFastReconnectUntil}); delete($hash->{helper}{WOLFastReconnectNext}); } $hash->{helper}{pingCounter}=0; # CD 0007 SB_SERVER_Broadcast( $hash, "SERVER", "IP " . $hash->{IP} . ":" . AttrVal( $name, "httpport", "9000" ) ); $hash->{helper}{doBroadcast}=1; # CD 0007 SB_SERVER_LMS_Status( $hash ); if( AttrVal( $name, "doalivecheck", "false" ) eq "false" ) { readingsSingleUpdate( $hash, "power", "on", 1 ); #SB_SERVER_Broadcast( $hash, "SERVER", "ON" ); # CD 0007 return( 0 ); } elsif( AttrVal( $name, "doalivecheck", "false" ) eq "true" ) { # start the alive checking mechanism # CD 0020 SB_SERVER_tcb_Alive verwenden RemoveInternalTimer( "SB_SERVER_Alive:$name"); InternalTimer( gettimeofday() + AttrVal( $name, "alivetimer", 10 ), "SB_SERVER_tcb_Alive", "SB_SERVER_Alive:$name", 0 ); return( 0 ); } else { Log3( $hash, 2, "SB_SERVER_DoInit: doalivecheck has " . "wrong value" ); return( 1 ); } } } else { # what the f... Log3( $hash, 2, "SB_SERVER_DoInit: unclear status reported" ); return( 1 ); } #Log3( $hash, 3, "SB_SERVER_DoInit: something went wrong!" ); # CD 0008 nur für Testzwecke 0009 deaktiviert #return(0); # CD 0008 nur für Testzwecke 0009 deaktiviert return( 1 ); } # CD 0032 start # ---------------------------------------------------------------------------- # Parse return of app items query # ---------------------------------------------------------------------------- sub SB_SERVER_ParseAppResponse( $$ ) { my ( $hash, $buf ) = @_; my $name = $hash->{NAME}; my $appresponse=0; if($buf=~m/items/) { my @data=split(' ',$buf); #Log 0,$buf; if(defined($hash->{helper}{appcmd}) && defined($hash->{helper}{appcmd}{$data[1]})) { my $appcmd=$data[1]; my $appname=$hash->{helper}{appcmd}{$appcmd}{name}; my $nameactive=0; my $save=0; if($buf=~m/item_id:/) { # app subitems ... my $id="0"; my $pname=""; my $dest=0; my $broadcastPlaylists=0; my $broadcastFavorites=0; foreach (@data) { if( $_ =~ /^(item_id:)(.*)/ ) { if(defined($hash->{helper}{appcmd}{$appcmd}{playlistsId}) && ($hash->{helper}{appcmd}{$appcmd}{playlistsId} eq $2)) { $dest=1; } if(defined($hash->{helper}{appcmd}{$appcmd}{favoritesId}) && ($hash->{helper}{appcmd}{$appcmd}{favoritesId} eq $2)) { $dest=2; } } elsif( $_ =~ /^(id:)(.*)/ ) { # new entry if($save==1) { if($dest==1) { if(defined($hash->{helper}{appcmd}{$appcmd}{playlists}) && defined($hash->{helper}{appcmd}{$appcmd}{playlists}{$id})) { $broadcastPlaylists=1 if($hash->{helper}{appcmd}{$appcmd}{playlists}{$id}{name} ne $pname); } else { $broadcastPlaylists=1; } $hash->{helper}{appcmd}{$appcmd}{playlists}{$id}{name}=$pname; } if($dest==2) { # Bug in squeezecloud ? Element 3.2 funktioniert nicht if($appcmd eq 'squeezecloud') { if($id-int($id)>0.19) { $id+=0.1; } } if(defined($hash->{helper}{appcmd}{$appcmd}{favorites}) && defined($hash->{helper}{appcmd}{$appcmd}{favorites}{$id})) { $broadcastFavorites=1 if($hash->{helper}{appcmd}{$appcmd}{favorites}{$id}{name} ne $pname); } else { $broadcastFavorites=1; } $hash->{helper}{appcmd}{$appcmd}{favorites}{$id}{name}=$pname; } $save=0; } $id=$2; $nameactive=0; next; } elsif( $_ =~ /^(name:)(.*)/ ) { $pname=$2; $save=1; $nameactive=1; next; } elsif( $_ =~ /^(type:)(.*)/ ) { $nameactive=0; } else { $pname.=" $_" if($nameactive==1); } } if($save==1) { if($dest==1) { if(defined($hash->{helper}{appcmd}{$appcmd}{playlists}) && defined($hash->{helper}{appcmd}{$appcmd}{playlists}{$id})) { $broadcastPlaylists=1 if($hash->{helper}{appcmd}{$appcmd}{playlists}{$id}{name} ne $pname); } else { $broadcastPlaylists=1; } $hash->{helper}{appcmd}{$appcmd}{playlists}{$id}{name}=$pname; } if($dest==2) { # Bug in squeezecloud ? Element 3.2 funktioniert nicht if($appcmd eq 'squeezecloud') { if($id-int($id)>0.19) { $id+=0.1; } } if(defined($hash->{helper}{appcmd}{$appcmd}{favorites}) && defined($hash->{helper}{appcmd}{$appcmd}{favorites}{$id})) { $broadcastFavorites=1 if($hash->{helper}{appcmd}{$appcmd}{favorites}{$id}{name} ne $pname); } else { $broadcastFavorites=1; } $hash->{helper}{appcmd}{$appcmd}{favorites}{$id}{name}=$pname; } } if($broadcastPlaylists==1) { SB_SERVER_Broadcast( $hash, "PLAYLISTS", "FLUSH $appcmd", undef ); RemoveInternalTimer( "SB_SERVER_tcb_SendPlaylists:$name"); foreach my $pl ( keys %{$hash->{helper}{appcmd}{$appcmd}{playlists}} ) { my $plname=$hash->{helper}{appcmd}{$appcmd}{playlists}{$pl}{name}; $plname=~s/ /_/g; $plname=~s/[^[:ascii:]]//g; #$plname=unidecode($plname); my $uniquename = SB_SERVER_FavoritesName2UID( $plname ); push @SB_SERVER_PLS, "ADD $plname $pl $uniquename $appcmd"; } if(scalar(@SB_SERVER_PLS)>0) { InternalTimer( gettimeofday() + 0.01, "SB_SERVER_tcb_SendPlaylists", "SB_SERVER_tcb_SendPlaylists:$name", 0 ); } } if($broadcastFavorites==1) { SB_SERVER_Broadcast( $hash, "FAVORITES", "FLUSH $appcmd", undef ); RemoveInternalTimer( "SB_SERVER_tcb_SendFavorites:$name"); foreach my $pl ( keys %{$hash->{helper}{appcmd}{$appcmd}{favorites}} ) { my $plname=$hash->{helper}{appcmd}{$appcmd}{favorites}{$pl}{name}; $plname=~s/ /_/g; $plname=~s/[^[:ascii:]]//g; #$plname=unidecode($plname); my $uniquename = SB_SERVER_FavoritesName2UID( $plname ); push @SB_SERVER_FAVS, "ADD $name $pl $uniquename url $appcmd $plname"; } if(scalar(@SB_SERVER_FAVS)>0) { InternalTimer( gettimeofday() + 0.01, "SB_SERVER_tcb_SendFavorites", "SB_SERVER_tcb_SendFavorites:$name", 0 ); } } } else { # app items ... my $id=0; my $iname=""; my $type=""; my $isaudio=0; my $hasitems=0; foreach (@data) { if( $_ =~ /^(id:)(.*)/ ) { # new entry if($save==1) { $hash->{helper}{appcmd}{$appcmd}{items}{$id}{name}=$iname; $hash->{helper}{appcmd}{$appcmd}{items}{$id}{type}=$type; $hash->{helper}{appcmd}{$appcmd}{items}{$id}{isaudio}=$isaudio; $hash->{helper}{appcmd}{$appcmd}{items}{$id}{hasitems}=$hasitems; $hash->{helper}{appcmd}{$appcmd}{playlistsId}=$id if($iname eq 'Playlists'); $hash->{helper}{appcmd}{$appcmd}{playlistsId}=$id if($iname eq 'Wiedergabelisten'); # CD 0035 $hash->{helper}{appcmd}{$appcmd}{playlistsId}=$id if($iname eq 'Listes de lecture'); # CD 0036 $hash->{helper}{appcmd}{$appcmd}{favoritesId}=$id if($iname eq 'Likes'); $hash->{helper}{appcmd}{$appcmd}{favoritesId}=$id if($iname eq 'Favorites'); $save=0; } $id=$2; $nameactive=0; next; } elsif( $_ =~ /^(type:)(.*)/ ) { $type=$2; $save=1; $nameactive=0; next; } elsif( $_ =~ /^(isaudio:)(.*)/ ) { $isaudio=$2; $save=1; $nameactive=0; next; } elsif( $_ =~ /^(hasitems:)(.*)/ ) { $hasitems=$2; $save=1; $nameactive=0; next; } elsif( $_ =~ /^(name:)(.*)/ ) { $iname=$2; $save=1; $nameactive=1; next; } elsif( $_ =~ /^(count:)(.*)/ ) { if($2==0) { delete $hash->{helper}{appcmd}{$appcmd}{items} if defined($hash->{helper}{appcmd}{$appcmd}{items}); $save=0; Log3( $hash, 2, "SB_SERVER_ParseAppResponse($name): no valid data for $appname: $buf" ); last; } } else { $iname.=" $_" if($nameactive==1); } } if($save==1) { $hash->{helper}{appcmd}{$appcmd}{items}{$id}{name}=$iname; $hash->{helper}{appcmd}{$appcmd}{items}{$id}{type}=$type; $hash->{helper}{appcmd}{$appcmd}{items}{$id}{isaudio}=$isaudio; $hash->{helper}{appcmd}{$appcmd}{items}{$id}{hasitems}=$hasitems; $hash->{helper}{appcmd}{$appcmd}{playlistsId}=$id if($iname eq 'Playlists'); $hash->{helper}{appcmd}{$appcmd}{favoritesId}=$id if($iname eq 'Likes'); $hash->{helper}{appcmd}{$appcmd}{favoritesId}=$id if($iname eq 'Favorites'); } if(defined($hash->{helper}{appcmd}{$appcmd}{playlistsId})) { DevIo_SimpleWrite( $hash, "$appcmd items 0 200 item_id:".($hash->{helper}{appcmd}{$appcmd}{playlistsId})."\n", 0 ); } if(defined($hash->{helper}{appcmd}{$appcmd}{favoritesId})) { DevIo_SimpleWrite( $hash, "$appcmd items 0 200 item_id:".($hash->{helper}{appcmd}{$appcmd}{favoritesId})." want_url:1\n", 0 ); } } $appresponse=1; } } return $appresponse; } # CD 0032 end # ---------------------------------------------------------------------------- # 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 ) { # CD 0032 start # check for app response if(SB_SERVER_ParseAppResponse($hash,$buf)==0) { # 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}; Log3( $hash, 4, "SB_SERVER_ParseCmds($name): called" ); my @args = split( " ", $instr ); $hash->{LASTANSWER} = "@args"; my $cmd = shift( @args ); # CD 0007 start if (defined($hash->{helper}{doBroadcast})) { SB_SERVER_Broadcast( $hash, "SERVER", "ON" ); SB_SERVER_Broadcast( $hash, "SERVER", "IP " . $hash->{IP} . ":" . AttrVal( $name, "httpport", "9000" ) ); delete ($hash->{helper}{doBroadcast}); } # CD 0007 end 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" ); SB_SERVER_Broadcast( $hash, "SERVER", "IP " . $hash->{IP} . ":" . AttrVal( $name, "httpport", "9000" ) ); } } elsif( $cmd eq "pref" ) { if( $args[ 0 ] eq "authorize" ) { readingsSingleUpdate( $hash, "serversecure", $args[ 1 ], 0 ); if( $args[ 1 ] eq "1" ) { # username and password is required # CD 0007 zu spät, login muss als erstes gesendet werden, andernfalls bricht der Server die Verbindung sofort ab if( ( $hash->{USERNAME} ne "?" ) && ( $hash->{PASSWORD} ne "?" ) ) { my ($user,$password)=SB_SERVER_readPassword($hash); # CD 0031 if(defined($user)) { DevIo_SimpleWrite( $hash, "login " . $user . " " . $password . "\n", 0 ); } else { Log3( $hash, 3, "SB_SERVER_ParseCmds($name): login " . "required but no username and password specified" ); } } else { Log3( $hash, 3, "SB_SERVER_ParseCmds($name): login " . "required but no username and password specified" ); } # next step is to wait for the answer of the LMS server } elsif( $args[ 1 ] eq "0" ) { # no username password required, go ahead directly #SB_SERVER_LMS_Status( $hash ); } else { Log3( $hash, 3, "SB_SERVER_ParseCmds($name): unkown " . "result for authorize received. Should be 0 or 1" ); } } } elsif( $cmd eq "login" ) { if( ( $args[ 1 ] eq $hash->{USERNAME} ) && ( $args[ 2 ] eq "******" ) ) { # login has been succesful, go ahead SB_SERVER_LMS_Status( $hash ); } } elsif( $cmd eq "fhemalivecheck" ) { $hash->{ALIVECHECK} = "received"; 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 ) . " want_url:1\n", 0 ); # CD 0009 url mit abfragen DevIo_SimpleWrite( $hash, "alarm playlists 0 300\n", 0 ); # CD 0011 } 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 ); } elsif( $cmd eq "playlists" ) { Log3( $hash, 4, "SB_SERVER_ParseCmds($name): playlists" ); # CD 0004 Playlisten neu anfragen bei Änderung if(($args[0] eq "rename")||($args[0] eq "delete")) { DevIo_SimpleWrite( $hash, "playlists 0 200\n", 0 ); DevIo_SimpleWrite( $hash, "alarm playlists 0 300\n", 0 ); # CD 0011 } else { SB_SERVER_ParseServerPlaylists( $hash, \@args ); } } elsif( $cmd eq "client" ) { # CD 0011 start } elsif( $cmd eq "alarm" ) { if( $args[0] eq "playlists" ) { SB_SERVER_ParseServerAlarmPlaylists( $hash, \@args ); } # CD 0011 end # CD 0016 start } elsif( $cmd eq "rescan" ) { if( $args[0] eq "done" ) { DevIo_SimpleWrite( $hash, "serverstatus 0 200\n", 0 ); # CD 0036 start - refresh favorites and playlists after rescan DevIo_SimpleWrite( $hash, "favorites items 0 " . AttrVal( $name, "maxfavorites", 100 ) . " want_url:1\n", 0 ); DevIo_SimpleWrite( $hash, "playlists 0 200\n", 0 ); DevIo_SimpleWrite( $hash, "alarm playlists 0 300\n", 0 ); # CD 0036 end # CD 0039 start readingsBeginUpdate( $hash ); readingsBulkUpdate( $hash, "scanning", "no"); readingsBulkUpdate( $hash, "scanprogressdone", "0" ); readingsBulkUpdate( $hash, "scanprogresstotal", "0" ); readingsBulkUpdate( $hash, "scandb", "?" ); # CD 0040 start if(defined $hash->{helper}{scanstart}) { readingsBulkUpdate( $hash, "scanduration", int(time()-$hash->{helper}{scanstart})); delete $hash->{helper}{scanstart}; } # CD 0040 end readingsEndUpdate( $hash, 1 ); } else { readingsSingleUpdate( $hash, "scanning", "yes", 1 ); $hash->{helper}{scanstart}=time(); # CD 0040 readingsSingleUpdate( $hash, "scanduration", 0, 1 ); # CD 0040 } } elsif( $cmd eq "scanner" ) { if((defined $args[0]) && ($args[0] eq 'notify')) { if((defined $args[1]) && (substr($args[1],0,8) eq 'progress')) { my @params=split '\|\|',$args[1]; if((defined $params[1]) && ($params[1] eq 'importer')) { readingsBeginUpdate( $hash ); readingsBulkUpdate( $hash, "scanprogressdone", $params[3] ) if defined $params[3]; readingsBulkUpdate( $hash, "scanprogresstotal", $params[4] ) if defined $params[4]; readingsBulkUpdate( $hash, "scandb", $params[2] ) if defined $params[2]; # CD 0040 start if(defined $hash->{helper}{scanstart}) { readingsBulkUpdate( $hash, "scanduration", int(time()-$hash->{helper}{scanstart})); } # CD 0040 end readingsEndUpdate( $hash, 1 ); } } } # CD 0039 end # CD 0016 end # CD 0030 start } elsif( $cmd eq "artists" ) { if(defined($hash->{helper}{getData}) && defined($hash->{helper}{getData}{artists})) { my ($dev,$reading)=split(':',$hash->{helper}{getData}{artists}{reading}); if ($hash->{helper}{getData}{artists}{format} eq 'raw') { if(defined($defs{$dev})) { readingsSingleUpdate( $defs{$dev}, $reading, $instr, 1 ); } } else { my $artistname=""; my $jout="["; my $lout=""; my $iout=""; my $artistid=0; foreach( @args ) { if( $_ =~ /^(artist:)(.*)/ ) { $artistname=$2; next; } elsif( $_ =~ /^(id:)([0-9]*)/ ) { # start new entry if($artistname ne "") { $artistname=~s/\"/\\\"/g; $jout.="{\"Artist\":\"".$artistname."\",\"Id\":\"".$artistid."\"},"; $lout.="\"".$artistname."\":"; $iout.=$artistid.":"; } $artistid=$2; next; } elsif( $_ =~ /^(count:)([0-9]*)/ ) { next; } elsif( $artistname ne "" ) { $artistname=~s/\"/\\\"/g; $artistname.=" ".$_; next; } } if($artistname ne "") { $jout.="{\"Artist\":\"".$artistname."\"}"; $lout.="\"".$artistname."\""; $iout.=$artistid; } $jout.="]"; if(defined($defs{$dev})) { readingsSingleUpdate( $defs{$dev}, $reading, $jout, 1 ) if ($hash->{helper}{getData}{artists}{format} eq 'json'); readingsSingleUpdate( $defs{$dev}, $reading, $lout, 1 ) if ($hash->{helper}{getData}{artists}{format} eq 'delimited'); readingsSingleUpdate( $defs{$dev}, $reading."_index", $iout, 1 ) if ($hash->{helper}{getData}{artists}{format} eq 'delimited'); } } delete $hash->{helper}{getData}{artists}; } # CD 0030 end # CD 0032 start } elsif( $cmd eq "apps" ) { my $save=0; my $appcmd=""; my $appname=""; my $scansubs=0; # 1. Mal ? $scansubs=1 unless (defined($hash->{helper}{apps})); delete($hash->{helper}{apps}) if(defined($hash->{helper}{apps})); delete($hash->{helper}{appcmd}) if(defined($hash->{helper}{appcmd})); foreach( @args ) { if( $_ =~ /^(icon:)(.*)/ ) { # new entry if($save==1) { $hash->{helper}{apps}{$appname}{cmd}=$appcmd; $hash->{helper}{appcmd}{$appcmd}{name}=$appname; $save=0; } next; } elsif( $_ =~ /^(cmd:)(.*)/ ) { $appcmd=$2; $save=1; next; } elsif( $_ =~ /^(name:)(.*)/ ) { $appname=$2; $appname=~s/\./_/g; $appname=~s/-/_/g; $save=1; next; } elsif( $_ =~ /^(type:)(.*)/ ) { $save=0 if($2 ne 'xmlbrowser'); next; } } if($save==1) { $hash->{helper}{apps}{$appname}{cmd}=$appcmd; $hash->{helper}{appcmd}{$appcmd}{name}=$appname; } SB_SERVER_SetAttrList($hash); if(defined($hash->{helper}{apps})&&($scansubs==1)) { my @enabledApps=split(',',AttrVal($name,'enablePlugins','')); foreach my $app (@enabledApps) { if(defined($hash->{helper}{apps}{$app})) { DevIo_SimpleWrite( $hash, ($hash->{helper}{apps}{$app}{cmd})." items 0 200\n", 0 ); } } } # CD 0032 end # CD 0032 end } else { # unkown } } # CD 0020 start sub SB_SERVER_tcb_Alive($) { my($in ) = shift; my(undef,$name) = split(':',$in); my $hash = $defs{$name}; #Log 0,"SB_SERVER_tcb_Alive"; delete($hash->{helper}{disableReconnect}) if defined($hash->{helper}{disableReconnect}); SB_SERVER_Alive($hash); } # CD 0020 end # ---------------------------------------------------------------------------- # Alivecheck of the server # ---------------------------------------------------------------------------- sub SB_SERVER_Alive( $ ) { my ($hash) = @_; my $name = $hash->{NAME}; my $state=ReadingsVal($name, "state", "unknown"); # CD 0038 # CD 0004 set default to off #my $rccstatus = "on"; #my $pingstatus = "on"; my $rccstatus = "off"; my $pingstatus = "off"; my $nexttime = gettimeofday() + AttrVal( $name, "alivetimer", 120 ); Log3( $hash, 4, "SB_SERVER_Alive($name): called" ); # CD 0006 changed log level from 4 to 2 # CD 0009 level 2->3 # CD 0014 level -> 4 if( AttrVal( $name, "doalivecheck", "false" ) eq "false" ) { Log3( $hash, 5, "SB_SERVER_Alive($name): alivechecking is off" ); $rccstatus = "on"; $pingstatus = "on"; $hash->{helper}{pingCounter}=0; # CD 0004 } else { # check via the RCC element if( $hash->{RCCNAME} ne "none" ) { # an RCC element has been given as argument $rccstatus = ReadingsVal( $hash->{RCCNAME}, "state", "off" ); } # CD 0007 start if (($hash->{PRESENCENAME} ne "none") && defined($defs{$hash->{PRESENCENAME}}) && defined($defs{$hash->{PRESENCENAME}}->{TIMEOUT_NORMAL}) && (($defs{$hash->{PRESENCENAME}}->{TIMEOUT_NORMAL}) < AttrVal( $name, "alivetimer", 30 ))) { Log3( $hash, 4,"SB_SERVER_Alive($name): using $hash->{PRESENCENAME}"); # CD 0009 level 2->4 if( ReadingsVal( $hash->{PRESENCENAME}, "state", "absent" ) eq "present" ) { $pingstatus = "on"; $hash->{helper}{pingCounter}=0; } else { $pingstatus = "off"; $hash->{helper}{pingCounter}=$hash->{helper}{pingCounter}+1; $nexttime = gettimeofday() + 15; } } else { # CD 0007 end # CD 0021 start my $ipp=AttrVal($name, "internalPingProtocol", "tcp" ); if($ipp eq "none") { if ($state eq "disconnected") { # CD 0038 state statt STATE verwenden $pingstatus = "off"; $hash->{helper}{pingCounter}=3; } else { $pingstatus = "on"; $hash->{helper}{pingCounter}=0; } } else { # CD 0021 end Log3( $hash, 4,"SB_SERVER_Alive($name): using internal ping"); # CD 0007 # CD 0009 level 2->4 # check via ping my $p; # CD 0017 eval hinzugefügt, Absturz auf FritzBox, bei Fehler annehmen dass Host verfügbar ist, internalPingProtocol hinzugefügt eval { $p = Net::Ping->new( $ipp ); }; if($@) { Log3( $hash,1,"SB_SERVER_Alive($name): internal ping failed with $@"); $pingstatus = "on"; $hash->{helper}{pingCounter}=0; } else { if( $p->ping( $hash->{IP}, 2 ) ) { $pingstatus = "on"; $hash->{helper}{pingCounter}=0; # CD 0004 } else { $pingstatus = "off"; $hash->{helper}{pingCounter}=$hash->{helper}{pingCounter}+1; # CD 0004 } # close our ping mechanism again $p->close( ); } } # CD 0021 } # CD 0007 Log3( $hash, 5, "SB_SERVER_Alive($name): " . # CD Test 5 "RCC:" . $rccstatus . " Ping:" . $pingstatus ); # CD 0006 changed log level from 5 to 2 # CD 0009 level 2->3 # CD 0014 level -> 5 } # set the status of the server accordingly # CD 0004 added sensitivity to ping # if( ( $rccstatus eq "on" ) || ( $pingstatus eq "on" ) ) { if( ( $rccstatus eq "on" ) || ( $hash->{helper}{pingCounter}<3 ) ) { # the server is reachable if( ReadingsVal( $name, "power", "on" ) eq "off" ) { # the first time we see the server being on Log3( $hash, 3, "SB_SERVER_Alive($name): " . # CD 0004 changed log level from 5 to 2 # CD 0009 level 2->3 "SB-Server is back again." ); # first time we realized server is away if( $state eq "disconnected" ) { # CD 0038 state statt STATE verwenden delete($hash->{NEXT_OPEN}) if($hash->{NEXT_OPEN}); # CD 0007 remove delay for reconnect DevIo_OpenDev( $hash, 1, "SB_SERVER_DoInit" ); } readingsSingleUpdate( $hash, "power", "on", 1 ); $hash->{ALIVECHECK} = "?"; $hash->{CLICONNECTION} = "off"; $hash->{helper}{onAfterAliveCheck}=1; # CD 0038 # quicker update to capture CLI connection faster $nexttime = gettimeofday() + 10; } else { # CD 0005 # check the CLI connection (sub-state) if( $hash->{ALIVECHECK} eq "waiting" ) { # ups, we did not receive any answer in the last minutes # SB Server potentially dead or shut-down Log3( $hash, 3, "SB_SERVER_Alive($name): overrun SB-Server dead." ); # CD 0004 changed log level from 5 to 2 # CD 0009 level 2->3 $hash->{CLICONNECTION} = "off"; $hash->{helper}{onAfterAliveCheck}=0; # CD 0038 # signal that to our clients SB_SERVER_Broadcast( $hash, "SERVER", "OFF" ); # close the device # CD 0007 use DevIo_Disconnected instead of DevIo_CloseDev #DevIo_CloseDev( $hash ); DevIo_Disconnected( $hash ); $hash->{helper}{pingCounter}=9999; # CD 0007 # CD 0000 start - exit infinite loop after socket has been closed $hash->{ALIVECHECK} = "?"; # CD 0038 $hash->{STATE}="disconnected"; # CD 0005 line above does not work (on Linux), fix: # CD 0006 DevIo_setStates requires v7099 of DevIo.pm, replaced with SB_SERVER_setStates SB_SERVER_setStates($hash, "disconnected"); readingsSingleUpdate( $hash, "power", "off", 1 ); # test: clear stack ? $SB_SERVER_CmdStack{$name}{last_n} = 0; $SB_SERVER_CmdStack{$name}{first_n} = 0; $SB_SERVER_CmdStack{$name}{cnt} = 0; # CD end # remove all timers we created SB_SERVER_RemoveInternalTimers( $hash ); } else { if( $hash->{CLICONNECTION} eq "off" ) { # signal that to our clients # to be revisited, should only be sent after CLI established #SB_SERVER_Broadcast( $hash, "SERVER", "ON" ); # CD 0007 disabled, wait for SB_SERVER_LMS_Status SB_SERVER_LMS_Status( $hash ) if ($state eq 'opened'); # CD 0038 nur aufrufen wenn Verbindung aufgebaut ist } $hash->{CLICONNECTION} = "on"; # just send something to the SB-Server. It will echo it # if we receive the echo, the server is still alive $hash->{ALIVECHECK} = "waiting"; DevIo_SimpleWrite( $hash, "fhemalivecheck\n", 0 ) if ($state eq 'opened'); # CD 0038 nur aufrufen wenn Verbindung aufgebaut ist } } } elsif( ( $rccstatus eq "off" ) && ( $pingstatus eq "off" ) ) { if( ReadingsVal( $name, "power", "on" ) eq "on" ) { # the first time we realize the server is off Log3( $hash, 3, "SB_SERVER_Alive($name): " . # CD 0004 changed log level from 5 to 2 # CD 0009 level 2->3 "SB-Server in hibernate / suspend?." ); # first time we realized server is away $hash->{CLICONNECTION} = "off"; readingsSingleUpdate( $hash, "power", "off", 1 ); $hash->{ALIVECHECK} = "?"; # signal that to our clients SB_SERVER_Broadcast( $hash, "SERVER", "OFF" ); # close the device # CD 0007 use DevIo_Disconnected instead of DevIo_CloseDev #DevIo_CloseDev( $hash ); DevIo_Disconnected( $hash ); $hash->{helper}{pingCounter}=9999; # CD 0007 # CD 0004 set STATE, needed for reconnect # CD 0038 $hash->{STATE}="disconnected"; # CD 0005 line above does not work (on Linux), fix: # CD 0006 DevIo_setStates requires v7099 of DevIo.pm, replaced with SB_SERVER_setStates SB_SERVER_setStates($hash, "disconnected"); # remove all timers we created SB_SERVER_RemoveInternalTimers( $hash ); } } else { # we shouldn't end up here Log3( $hash, 5, "SB_SERVER_Alive($name): funny server status " . "received. Ping=" . $pingstatus . " RCC=" . $rccstatus ); } # do an update of the status # CD 0020 SB_SERVER_tcb_Alive verwenden RemoveInternalTimer( "SB_SERVER_Alive:$name"); if( AttrVal( $name, "doalivecheck", "false" ) eq "true" ) { InternalTimer( $nexttime, "SB_SERVER_tcb_Alive", "SB_SERVER_Alive:$name", 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($name): called with $cmd - $msg" ); 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} ) ) && ( $defs{$mydev}{TYPE} eq "SB_PLAYER" )){ # CD 0029 umsortiert if( ( defined( $iodevhash->{NAME} ) ) && # CD 0029 umsortiert ( $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}; Log3( $hash, 4, "SB_SERVER_ParseServerStatus($name): called " ); # 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 ); 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"; my $nameactive=0; my $rescanactive=0; 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+1)."-$year " . # CD 0016 Monat korrigiert "$hour:$min:$sec" ); #readingsBulkUpdate( $hash, "scanlast", strftime("%Y-%m-%d %H:%M:%S", localtime($2))); # CD 0040 next; } elsif( $_ =~ /^(scanning:)([0-9]*)/ ) { readingsBulkUpdate( $hash, "scanning", $2 ); next; } elsif( $_ =~ /^(rescan:)([0-9]*)/ ) { if( $2 eq "1" ) { readingsBulkUpdate( $hash, "scanning", "yes" ); $rescanactive=1; $hash->{helper}{scanstart}=time() unless defined($hash->{helper}{scanstart}); # CD 0040 } else { readingsBulkUpdate( $hash, "scanning", "no" ); # CD 0040 start if(defined $hash->{helper}{scanstart}) { readingsBulkUpdate( $hash, "scanduration", int(time()-$hash->{helper}{scanstart})); delete $hash->{helper}{scanstart}; } # CD 0040 end } 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 ) { # CD 0017 fixed == $players{$id}{ID} = $id; $players{$id}{MAC} = $2; $currentplayerid = $id; } $nameactive=0; # CD 0030 next; } elsif( $_ =~ /^(name:)(.*)/ ) { if( $currentplayerid ne "none" ) { $players{$currentplayerid}{name} = $2; $nameactive=1; # CD 0030 } next; } elsif( $_ =~ /^(displaytype:)(.*)/ ) { if( $currentplayerid ne "none" ) { $players{$currentplayerid}{displaytype} = $2; } $nameactive=0; # CD 0030 next; } elsif( $_ =~ /^(model:)(.*)/ ) { if( $currentplayerid ne "none" ) { $players{$currentplayerid}{model} = $2; } $nameactive=0; # CD 0030 next; } elsif( $_ =~ /^(power:)([0|1])/ ) { if( $currentplayerid ne "none" ) { $players{$currentplayerid}{power} = $2; } $nameactive=0; # CD 0030 next; } elsif( $_ =~ /^(canpoweroff:)([0|1])/ ) { if( $currentplayerid ne "none" ) { $players{$currentplayerid}{canpoweroff} = $2; } $nameactive=0; # CD 0030 next; } elsif( $_ =~ /^(connected:)([0|1])/ ) { if( $currentplayerid ne "none" ) { $players{$currentplayerid}{connected} = $2; } $nameactive=0; # CD 0030 next; } elsif( $_ =~ /^(isplayer:)([0|1])/ ) { if( $currentplayerid ne "none" ) { $players{$currentplayerid}{isplayer} = $2; } $nameactive=0; # CD 0030 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; } $nameactive=0; # CD 0030 next; } elsif( $_ =~ /^(seq_no:)(.*)/ ) { # just to take care of the keyword $nameactive=0; # CD 0030 next; # CD 0017 start } elsif( $_ =~ /^(isplaying:)(.*)/ ) { # just to take care of the keyword $nameactive=0; # CD 0030 next; } elsif( $_ =~ /^(snplayercount:)(.*)/ ) { # just to take care of the keyword $nameactive=0; # CD 0030 next; } elsif( $_ =~ /^(otherplayercount:)(.*)/ ) { # just to take care of the keyword $nameactive=0; # CD 0030 next; } elsif( $_ =~ /^(server:)(.*)/ ) { # just to take care of the keyword $nameactive=0; # CD 0030 next; } elsif( $_ =~ /^(serverurl:)(.*)/ ) { # just to take care of the keyword $nameactive=0; # CD 0030 next; # CD 0017 end # CD 0030 firmware und modelname } elsif( $_ =~ /^(modelname:)(.*)/ ) { if( $currentplayerid ne "none" ) { $players{$currentplayerid}{model} = $2; } $nameactive=0; next; } elsif( $_ =~ /^(firmware:)(.*)/ ) { # just to take care of the keyword $nameactive=0; next; } elsif( $_ =~ /:/ ) { $nameactive=0; # CD 0030 Ende } else { # no keyword found, so let us assume it is part of the player name # CD 0030 aber nur wenn 'name' noch aktiv ist if(( $currentplayerid ne "none" )&&($nameactive==1)) { $players{$currentplayerid}{name} .= $_; } } } if ($rescanactive==0) { # 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" ); } readingsEndUpdate( $hash, 1 ); my @ignoredIPs=split(',',AttrVal($name,'ignoredIPs','')); # CD 0017 my @ignoredMACs=split(',',AttrVal($name,'ignoredMACs','')); # CD 0017 foreach my $player ( keys %players ) { my $playerdata; # CD 0029 if( defined( $players{$player}{isplayer} ) ) { if( $players{$player}{isplayer} eq "0" ) { Log3( $hash, 1, "not a player" ); next; } } # CD 0017 check ignored IPs if( defined( $players{$player}{IP} ) ) { my @ip=split(':',$players{$player}{IP}); if ($ip[0] ~~ @ignoredIPs) { $players{$player}{ignore}=1; next; } } # CD 0017 check ignored MACs if( defined( $players{$player}{MAC} ) ) { if ($players{$player}{MAC} ~~ @ignoredMACs) { $players{$player}{ignore}=1; 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 ); } # CD 0029 start if( defined( $players{$player}{model} ) ) { $playerdata=$players{$player}{model}." "; } else { $playerdata="unknown "; } if( defined( $players{$player}{canpoweroff} ) ) { $playerdata.=$players{$player}{canpoweroff}." "; } else { $playerdata.="unknown "; } if( defined( $players{$player}{displaytype} ) ) { $playerdata.=$players{$player}{displaytype}." "; } else { $playerdata.="unknown "; } if( defined( $players{$player}{connected} ) ) { $playerdata.=$players{$player}{connected}." "; } else { $playerdata.="unknown"; } if( defined( $players{$player}{IP} ) ) { $playerdata.=$players{$player}{IP}; } else { $playerdata.="unknown"; } Dispatch( $hash, "SB_PLAYER:$players{$player}{ID}:" . "playerdata $playerdata", undef ); # CD 0029 end if( defined( $players{$player}{power} ) ) { Dispatch( $hash, "SB_PLAYER:$players{$player}{ID}:" . "power $players{$player}{power}", undef ); } } # the list for the sync masters # make all client create e new sync master list SB_SERVER_Broadcast( $hash, "SYNCMASTER", "FLUSH all", undef ); # now send the list for the sync masters @SB_SERVER_SM=(); foreach my $player ( keys %players ) { next if defined($players{$player}{ignore}); my $uniqueid = join( "", split( ":", $players{$player}{MAC} ) ); Log3( $hash, 1, "SB_SERVER_ParseServerStatus($name): player has no name") unless defined($players{$player}{name}); Log3( $hash, 1, "SB_SERVER_ParseServerStatus($name): player has no MAC") unless defined($players{$player}{MAC}); push @SB_SERVER_SM, "ADD $players{$player}{name} " . "$players{$player}{MAC} $uniqueid" # CD 0029 aufteilen wenn Hardware zu schwach } # CD 0029 start if(scalar(@SB_SERVER_SM)>0) { RemoveInternalTimer( "SB_SERVER_tcb_SendSyncMasters:$name"); InternalTimer( gettimeofday() + 0.01, "SB_SERVER_tcb_SendSyncMasters", "SB_SERVER_tcb_SendSyncMasters:$name", 0 ); } # CD 0029 end SB_SERVER_BuildPlayerList($hash); # CD 0024 return; } # CD 0029 start # für schwache Hardware Übertragung aufteilen sub SB_SERVER_tcb_SendSyncMasters( $ ) { my($in ) = shift; my(undef,$name) = split(':',$in); my $hash = $defs{$name}; RemoveInternalTimer( "SB_SERVER_tcb_SendSyncMasters:$name"); my $a; my $t=time(); do { $a=pop @SB_SERVER_SM; if (defined($a)) { SB_SERVER_Broadcast( $hash, "SYNCMASTER", $a, undef ); } } while ((time()<$t+0.05) && defined($a)); if(scalar(@SB_SERVER_SM)>0) { Log 0,"SB_SERVER_tcb_SendSyncMasters: ".scalar(@SB_SERVER_SM)." entries remaining"; InternalTimer( gettimeofday() + 0.05, "SB_SERVER_tcb_SendSyncMasters", "SB_SERVER_tcb_SendSyncMasters:$name", 0 ); } } # CD 0029 end # ---------------------------------------------------------------------------- # Parse the return values of the favorites items # ---------------------------------------------------------------------------- sub SB_SERVER_FavoritesParse( $$ ) { my ( $hash, $str ) = @_; my $name = $hash->{NAME}; Log3( $hash, 5, "SB_SERVER_FavoritesParse($name): called" ); # 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 = ""; my $isplaylist = false; my $url = "?"; # CD 0009 hinzugefügt my $cnt=0; foreach ( @data ) { #Log 0,$_; 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 )||($isplaylist == true)) { # derive our hash entry $namebuf="noname_".$cnt++ if($namebuf=~/^\s*$/); # CD 0037 my $entryuid = SB_SERVER_FavoritesName2UID( $namebuf ); # CD 0009 decode hinzugefügt # CD 0010 decode wieder entfernt $favorites{$name}{$entryuid} = { ID => $idbuf, Name => $namebuf, URL => $url, }; # CD 0009 hinzugefügt $namebuf = ""; $isaudiobuf = ""; $url = "?"; # CD 0009 hinzugefügt $hasitemsbuf = false; $isplaylist = 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; } # CD 0018 start } elsif( $_ =~ /^(type:)(.*)/ ) { $isplaylist = true if($2 eq "playlist"); if( $namestarted == true ) { $namestarted = false; } # CD 0018 end #} elsif( $_ =~ /^(name:)([0-9a-zA-Z]*)/ ) { # CD 0007 # CD 0009 deaktiviert } elsif( $_ =~ /^(name:)(.*)/ ) { # CD 0009 hinzugefügt $namebuf = $2; $namestarted = true; # CD 0009 start } elsif( $_ =~ /^(url:)(.*)/ ) { $url = $2; $url =~ s/file:\/\/\///; # CD 0009 end } 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 )||($isplaylist == true)) { # CD 0003 replaced ** my $entryuid = join( "", split( " ", $namebuf ) ); ** with: $namebuf="noname_".$cnt++ if($namebuf=~/^\s*$/); # CD 0037 my $entryuid = SB_SERVER_FavoritesName2UID( $namebuf ); # CD 0009 decode hinzugefügt # CD 0010 decode wieder entfernt $favorites{$name}{$entryuid} = { ID => $idbuf, Name => $namebuf, URL => $url, }; # CD 0009 hinzugefügt } 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 all", undef ); # find all the names and broadcast to our clients $favsetstring = "favorites:"; @SB_SERVER_FAVS=(); # CD 0029 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,"; push @SB_SERVER_FAVS, "ADD $name $favorites{$name}{$titi}{ID} " . "$titi $favorites{$name}{$titi}{URL} LMS $favorites{$name}{$titi}{Name}"; # CD 0009 URL an Player schicken # CD 0029 aufteilen wenn Hardware zu schwach } # CD 0029 start if(scalar(@SB_SERVER_FAVS)>0) { RemoveInternalTimer( "SB_SERVER_tcb_SendFavorites:$name"); InternalTimer( gettimeofday() + 0.01, "SB_SERVER_tcb_SendFavorites", "SB_SERVER_tcb_SendFavorites:$name", 0 ); } # CD 0029 end #chop( $favsetstring ); #$favsetstring .= " "; } # CD 0029 start # für schwache Hardware Übertragung aufteilen sub SB_SERVER_tcb_SendFavorites( $ ) { my($in ) = shift; my(undef,$name) = split(':',$in); my $hash = $defs{$name}; RemoveInternalTimer( "SB_SERVER_tcb_SendFavorites:$name"); my $a; my $t=time(); do { $a=pop @SB_SERVER_FAVS; if (defined($a)) { SB_SERVER_Broadcast( $hash, "FAVORITES", $a, undef ); # CD 0009 URL an Player schicken } } while ((time()<$t+0.05) && defined($a)); if(scalar(@SB_SERVER_FAVS)>0) { #Log 0,"SB_SERVER_tcb_SendFavorites: ".scalar(@SB_SERVER_FAVS)." entries remaining"; InternalTimer( gettimeofday() + 0.05, "SB_SERVER_tcb_SendFavorites", "SB_SERVER_tcb_SendFavorites:$name", 0 ); } } # CD 0029 end # ---------------------------------------------------------------------------- # generate a UID for the hash entry from the name # ---------------------------------------------------------------------------- sub SB_SERVER_FavoritesName2UID( $ ) { my $namestr = shift( @_ ); # eliminate spaces $namestr = join( "_", split( " ", $namestr ) ); # CD 0009 Leerzeichen durch _ ersetzen statt löschen # CD 0009 verschiedene Sonderzeichen ersetzen und nicht mehr löschen my %Sonderzeichen = ("ä" => "ae", "Ä" => "Ae", "ü" => "ue", "Ü" => "Ue", "ö" => "oe", "Ö" => "Oe", "ß" => "ss", "é" => "e", "è" => "e", "ë" => "e", "à" => "a", "ç" => "c" ); my $Sonderzeichenkeys = join ("|", keys(%Sonderzeichen)); $namestr =~ s/($Sonderzeichenkeys)/$Sonderzeichen{$1}/g; # $namestr =~ s/($Sonderzeichenkeys)/$Sonderzeichen{$1}||''/g; # CD 0009 # CD 0034 start my $rc=eval { require Text::Unaccent; $namestr=Text::Unaccent::unac_string('UTF8', $namestr); }; # CD 0034 end # this defines the regexp. Please add new stuff with the seperator | # CD 0003 changed öÜ to ö|Ü my $tobereplaced = '[Ä|ä|Ö|ö|Ü|ü|\[|\]|\{|\}|\(|\)|\\\\|,|:|\?|;|' . # CD 0011 ,:? hinzugefügt # CD 0035 ; hinzugefügt '\/|\'|\.|\"|\^|°|\$|\||%|@|*|#|&|\+]'; # CD 0009 + hinzugefügt # CD 0070 * und # hinzugefügt $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}; $n=0 if(!defined($n)); # CD 0007 if( $n > AttrVal( $name, "maxcmdstack", 200 ) ) { Log3( $hash, 5, "SB_SERVER_CMDStackPush($name): limit reached" ); SB_SERVER_CMDStackPop($hash); # CD 0007 added #return; # CD 0007 disabled } $SB_SERVER_CmdStack{$name}{$n}{CMD} = $cmd; $SB_SERVER_CmdStack{$name}{$n}{TS} = time(); # CD 0007 $n = $n + 1; $SB_SERVER_CmdStack{$name}{last_n} = $n; $SB_SERVER_CmdStack{$name}{first_n} = $n if (!defined($SB_SERVER_CmdStack{$name}{first_n})); # CD 0007 # update overall number of entries $SB_SERVER_CmdStack{$name}{cnt} = $SB_SERVER_CmdStack{$name}{last_n} - $SB_SERVER_CmdStack{$name}{first_n} + 1; $hash->{CMDSTACK}=$SB_SERVER_CmdStack{$name}{cnt}; # CD 0007 } # ---------------------------------------------------------------------------- # pop a command from the buffer # ---------------------------------------------------------------------------- sub SB_SERVER_CMDStackPop( $ ) { my ( $hash ) = @_; my $name = $hash->{NAME}; my $n = $SB_SERVER_CmdStack{$name}{first_n}; $n=0 if(!defined($n)); # CD 0007 my $res = ""; # return the first element of the list if( defined( $SB_SERVER_CmdStack{$name}{$n} ) ) { $res = $SB_SERVER_CmdStack{$name}{$n}{CMD}; $res = "empty" if($SB_SERVER_CmdStack{$name}{$n}{TS}{CMDSTACK}=$SB_SERVER_CmdStack{$name}{cnt}; # CD 0007 return( $res ); } # CD 0011 start # ---------------------------------------------------------------------------- # parse the list of known alarm playlists # ---------------------------------------------------------------------------- sub SB_SERVER_ParseServerAlarmPlaylists( $$ ) { my( $hash, $dataptr ) = @_; my $name = $hash->{NAME}; Log3( $hash, 4, "SB_SERVER_ParseServerAlarmPlaylists($name): called" ); # force all clients to delete alarm playlists SB_SERVER_Broadcast( $hash, "ALARMPLAYLISTS", "FLUSH all", undef ); @SB_SERVER_AL_PLS=split("category:",join(" ",@{$dataptr})); # CD 0029 Übertragung an Player aufteilen if(scalar(@SB_SERVER_AL_PLS)>0) { RemoveInternalTimer( "SB_SERVER_tcb_SendAlarmPlaylists:$name"); InternalTimer( gettimeofday() + 0.01, "SB_SERVER_tcb_SendAlarmPlaylists", "SB_SERVER_tcb_SendAlarmPlaylists:$name", 0 ); } } # CD 0011 end # CD 0029 start # für schwache Hardware Übertragung aufteilen sub SB_SERVER_tcb_SendAlarmPlaylists( $ ) { my($in ) = shift; my(undef,$name) = split(':',$in); my $hash = $defs{$name}; RemoveInternalTimer( "SB_SERVER_tcb_SendAlarmPlaylists:$name"); my $a; my $t=time(); do { $a=pop @SB_SERVER_AL_PLS; if (defined($a)) { my $i1=index($a," title:"); my $i2=index($a," url:"); my $i3=index($a," singleton:"); if (($i1!=-1)&&($i2!=-1)&&($i3!=-1)) { my $url=substr($a,$i2+5,$i3-$i2-5); $url=substr($a,$i1+7,$i2-$i1-7) if ($url eq ""); my $pn=SB_SERVER_FavoritesName2UID(decode('utf-8',$url)); SB_SERVER_Broadcast( $hash, "ALARMPLAYLISTS", "ADD $pn category ".substr($a,0,$i1), undef ); SB_SERVER_Broadcast( $hash, "ALARMPLAYLISTS", "ADD $pn title ".substr($a,$i1+7,$i2-$i1-7), undef ); SB_SERVER_Broadcast( $hash, "ALARMPLAYLISTS", "ADD $pn url $url", undef ); } } } while ((time()<$t+0.05) && defined($a)); if(scalar(@SB_SERVER_AL_PLS)>0) { #Log 0,"SB_SERVER_tcb_SendAlarmPlaylists: ".scalar(@SB_SERVER_AL_PLS)." entries remaining"; InternalTimer( gettimeofday() + 0.05, "SB_SERVER_tcb_SendAlarmPlaylists", "SB_SERVER_tcb_SendAlarmPlaylists:$name", 0 ); } } # CD 0029 end # ---------------------------------------------------------------------------- # parse the list of known Playlists # ---------------------------------------------------------------------------- sub SB_SERVER_ParseServerPlaylists( $$ ) { my( $hash, $dataptr ) = @_; my $name = $hash->{NAME}; Log3( $hash, 4, "SB_SERVER_ParseServerPlaylists($name): called" ); my $namebuf = ""; my $uniquename = ""; my $idbuf = -1; # typically the start index being a number if( $dataptr->[ 0 ] =~ /^([0-9])*/ ) { shift( @{$dataptr} ); } else { Log3( $hash, 5, "SB_SERVER_ParseServerPlaylists($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_ParseServerPlaylists($name): entry is " . "not the end number" ); return; } my $datastr = join( " ", @{$dataptr} ); Log3( $hash, 5, "SB_SERVER_ParseServerPlaylists($name): data to parse: " . $datastr ); # make all client create a new favorites list SB_SERVER_Broadcast( $hash, "PLAYLISTS", "FLUSH all", undef ); my @data1 = split( " ", $datastr ); @SB_SERVER_PLS=(); my $cnt=1; # CD 0037 foreach( @data1 ) { if( $_ =~ /^(id:)(.*)/ ) { Log3( $hash, 5, "SB_SERVER_ParseServerPlaylists($name): " . "id:$idbuf name:$namebuf " ); if( $idbuf != -1 ) { $namebuf="noname_".$cnt++ if($namebuf=~/^\s*$/); # CD 0037 $uniquename = SB_SERVER_FavoritesName2UID( $namebuf ); # CD 0009 decode hinzugefügt # CD 0010 decode wieder entfernt push @SB_SERVER_PLS, "ADD $namebuf $idbuf $uniquename LMS"; # CD 0029 } $idbuf = $2; $namebuf = ""; $uniquename = ""; next; } elsif( $_ =~ /^(playlist:)(.*)/ ) { $namebuf = $2; next; } elsif( $_ =~ /^(count:)([0-9]*)/ ) { # the last entry of the return Log3( $hash, 5, "SB_SERVER_ParseServerPlaylists($name): " . "id:$idbuf name:$namebuf " ); if( $idbuf != -1 ) { $namebuf="noname_".$cnt++ if($namebuf=~/^\s*$/); # CD 0037 $uniquename = SB_SERVER_FavoritesName2UID( $namebuf ); # CD 0009 decode hinzugefügt # CD 0010 decode wieder entfernt push @SB_SERVER_PLS, "ADD $namebuf $idbuf $uniquename LMS"; # CD 0029 } } else { $namebuf .= "_" . $_; next; } } # CD 0029 start if(scalar(@SB_SERVER_PLS)>0) { InternalTimer( gettimeofday() + 0.01, "SB_SERVER_tcb_SendPlaylists", "SB_SERVER_tcb_SendPlaylists:$name", 0 ); } # CD 0029 end return; } # CD 0029 start # für schwache Hardware Übertragung aufteilen sub SB_SERVER_tcb_SendPlaylists( $ ) { my($in ) = shift; my(undef,$name) = split(':',$in); my $hash = $defs{$name}; RemoveInternalTimer( "SB_SERVER_tcb_SendPlaylists:$name"); my $a; my $t=time(); do { $a=pop @SB_SERVER_PLS; if (defined($a)) { SB_SERVER_Broadcast( $hash, "PLAYLISTS", $a, undef ); } } while ((time()<$t+0.05) && defined($a)); if(scalar(@SB_SERVER_PLS)>0) { #Log 0,"SB_SERVER_tcb_SendPlaylists: ".scalar(@SB_SERVER_PLS)." entries remaining"; InternalTimer( gettimeofday() + 0.05, "SB_SERVER_tcb_SendPlaylists", "SB_SERVER_tcb_SendPlaylists:$name", 0 ); } } # CD 0029 end # CD 0008 start sub SB_SERVER_CheckConnection($) { my($in ) = shift; my(undef,$name) = split(':',$in); my $hash = $defs{$name}; Log3( $hash, 3, "SB_SERVER_CheckConnection($name): STATE: " . ReadingsVal($name, "state", "unknown") . " power: ". ReadingsVal( $name, "power", "X" )); # CD 0009 level 2->3 # CD 0038 state statt STATE verwenden if(ReadingsVal( $name, "power", "X" ) ne "on") { Log3( $hash, 3, "SB_SERVER_CheckConnection($name): forcing power on"); # CD 0009 level 2->3 $hash->{helper}{pingCounter}=0; SB_SERVER_Broadcast( $hash, "SERVER", "IP " . $hash->{IP} . ":" . AttrVal( $name, "httpport", "9000" ) ); $hash->{helper}{doBroadcast}=1; SB_SERVER_LMS_Status( $hash ); if( AttrVal( $name, "doalivecheck", "false" ) eq "false" ) { readingsSingleUpdate( $hash, "power", "on", 1 ); } elsif( AttrVal( $name, "doalivecheck", "false" ) eq "true" ) { # start the alive checking mechanism # CD 0020 SB_SERVER_tcb_Alive verwenden RemoveInternalTimer( "SB_SERVER_Alive:$name"); InternalTimer( gettimeofday() + AttrVal( $name, "alivetimer", 10 ), "SB_SERVER_tcb_Alive", "SB_SERVER_Alive:$name", 0 ); } } RemoveInternalTimer( "CheckConnection:$name"); } # CD 0008 end # ---------------------------------------------------------------------------- # the Notify function # ---------------------------------------------------------------------------- sub SB_SERVER_Notify( $$ ) { my ( $hash, $dev_hash ) = @_; my $name = $hash->{NAME}; # own name / hash my $devName = $dev_hash->{NAME}; # Device that created the events # CD start if ($dev_hash->{NAME} eq "global" && grep (m/^INITIALIZED$|^REREADCFG$/,@{$dev_hash->{CHANGED}})){ DevIo_OpenDev($hash, 0, "SB_SERVER_DoInit" ) unless defined($hash->{helper}{disableReconnect}); # CD 0038 } # CD end #Log3( $hash, 4, "SB_SERVER_Notify($name): called" . # "Own:" . $name . " Device:" . $devName ); # CD 0024 start if( grep(m/^SAVE$|^SHUTDOWN$/, @{$dev_hash->{CHANGED}}) ) { # CD 0043 auch bei SHUTDOWN speichern SB_SERVER_SaveSyncGroups($hash) if($SB_SERVER_hasDataDumper==1); SB_SERVER_SaveServerStates($hash) if($SB_SERVER_hasDataDumper==1); } # CD 0024 end # CD 0008 start if($devName eq $name ) { if (grep (m/^DISCONNECTED$/,@{$dev_hash->{CHANGED}})) { Log3( $hash, 3, "SB_SERVER_Notify($name): DISCONNECTED - STATE: " . ReadingsVal($name, "state", "unknown") . " power: ". ReadingsVal( $name, "power", "X" )); # CD 0009 level 2->3 RemoveInternalTimer( "CheckConnection:$name"); } if (grep (m/^CONNECTED$/,@{$dev_hash->{CHANGED}})) { Log3( $hash, 3, "SB_SERVER_Notify($name): CONNECTED - STATE: " . ReadingsVal($name, "state", "unknown") . " power: ". ReadingsVal( $name, "power", "X" )); # CD 0009 level 2->3 InternalTimer( gettimeofday() + 2, "SB_SERVER_CheckConnection", "CheckConnection:$name", 0 ); } } # CD 0008 end if( $devName eq $hash->{RCCNAME} ) { if( ReadingsVal( $hash->{RCCNAME}, "state", "off" ) eq "off" ) { SB_SERVER_RemoveInternalTimers( $hash ); # CD 0020 SB_SERVER_tcb_Alive verwenden InternalTimer( gettimeofday() + 10, "SB_SERVER_tcb_Alive", "SB_SERVER_Alive:$name", 0 ); # CD 0007 use DevIo_Disconnected instead of DevIo_CloseDev #DevIo_CloseDev( $hash ); DevIo_Disconnected( $hash ); $hash->{helper}{pingCounter}=9999; # CD 0007 $hash->{CLICONNECTION} = "off"; # CD 0007 # CD 0005 set state after DevIo_CloseDev # CD 0006 DevIo_setStates requires v7099 of DevIo.pm, replaced with SB_SERVER_setStates SB_SERVER_setStates($hash, "disconnected"); } elsif( ReadingsVal( $hash->{RCCNAME}, "state", "off" ) eq "on" ) { SB_SERVER_RemoveInternalTimers( $hash ); # do an update of the status, but SB CLI must come up # CD 0020 SB_SERVER_tcb_Alive verwenden InternalTimer( gettimeofday() + 20, "SB_SERVER_tcb_Alive", "SB_SERVER_Alive:$name", 0 ); } else { return( undef ); } return( "" ); # CD 0007 start } elsif( $devName eq $hash->{PRESENCENAME} ) { if(grep (m/^present$|^absent$/,@{$dev_hash->{CHANGED}})) { Log3( $hash, 3, "SB_SERVER_Notify($name): $devName changed to ". join(" ",@{$dev_hash->{CHANGED}})); # CD 0023 loglevel 2->3 # CD 0023 start if (defined($hash->{helper}{lastPRESENCEstate})) { if($hash->{helper}{lastPRESENCEstate} eq $dev_hash->{CHANGED}[0]) { # nichts geändert return( undef ); } } $hash->{helper}{lastPRESENCEstate}=$dev_hash->{CHANGED}[0]; # CD 0023 end SB_SERVER_RemoveInternalTimers( $hash ); # do an update of the status, but SB CLI must come up # CD 0020 SB_SERVER_tcb_Alive verwenden InternalTimer( gettimeofday() + 10, "SB_SERVER_tcb_Alive", "SB_SERVER_Alive:$name", 0 ); return( "" ); } else { return( undef ); } # CD 0007 end } else { return( undef ); } } # ---------------------------------------------------------------------------- # start up the LMS server status # ---------------------------------------------------------------------------- sub SB_SERVER_LMS_Status( $ ) { my ( $hash ) = @_; my $name = $hash->{NAME}; # own name / hash # CD 0007 login muss als erstes gesendet werden $hash->{helper}{SB_SERVER_LMS_Status}=time(); if( ( $hash->{USERNAME} ne "?" ) && ( $hash->{PASSWORD} ne "?" ) ) { my ($user,$password)=SB_SERVER_readPassword($hash); # CD 0031 if(defined($user)) { DevIo_SimpleWrite( $hash, "login " . $user . " " . $password . "\n", 0 ); } else { Log3( $hash, 3, "SB_SERVER_LMS_Status($name): login " . "required but no username and password specified" ); } } # 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 ) . " want_url:1\n", 0 ); # CD 0009 url mit abfragen DevIo_SimpleWrite( $hash, "playlists 0 200\n", 0 ); DevIo_SimpleWrite( $hash, "alarm playlists 0 300\n", 0 ); # CD 0011 DevIo_SimpleWrite( $hash, "apps 0 200\n", 0 ); # CD 0029 # CD 0032 start if(defined($hash->{helper}{apps})) { my @enabledApps=split(',',AttrVal($name,'enablePlugins','')); foreach my $app (@enabledApps) { if(defined($hash->{helper}{apps}{$app})) { DevIo_SimpleWrite( $hash, ($hash->{helper}{apps}{$app}{cmd})." items 0 200\n", 0 ); } } } # CD 0032 end return( true ); } # CD 0038 start sub SB_SERVER_RemoveInternalTimers($) { my ($hash) = @_; my $name = $hash->{NAME}; RemoveInternalTimer( $hash ); RemoveInternalTimer( "SB_SERVER_Alive:$name"); RemoveInternalTimer( "StartTalk:$name"); RemoveInternalTimer( "RecallAfterTalk:$name"); RemoveInternalTimer( "SB_SERVER_tcb_SendPlaylists:$name"); RemoveInternalTimer( "SB_SERVER_tcb_SendFavorites:$name"); RemoveInternalTimer( "SB_SERVER_tcb_SendSyncMasters:$name"); RemoveInternalTimer( "SB_SERVER_tcb_SendAlarmPlaylists:$name"); RemoveInternalTimer( "CheckConnection:$name"); } # CD 0038 end # CD 0006 start - added # ---------------------------------------------------------------------------- # copied from DevIo.pm 7099 # ---------------------------------------------------------------------------- sub SB_SERVER_setStates($$) { my ($hash, $val) = @_; $hash->{STATE} = $val; setReadingsVal($hash, "state", $val, TimeNow()); } # CD 0006 end # ---------------------------------------------------------------------------- # load/save sync groups # # CD 0024 # ---------------------------------------------------------------------------- sub SB_SERVER_StatefileName($$) { my( $name,$prefix ) = @_; my $statefile = $attr{global}{statefile}; $statefile = substr $statefile,0,rindex($statefile,'/')+1; return $statefile . $prefix . "_$name.dd.save"; } sub SB_SERVER_SaveSyncGroups($) { my( $hash ) = @_; my $name = $hash->{NAME}; return "No saved syncgroups found" unless(defined($hash->{helper}{syncGroups})); return "No statefile specified" if(!$attr{global}{statefile}); my $statefile = SB_SERVER_StatefileName($name,'sbsg'); if(open(FH, ">$statefile")) { my $t = localtime; print FH "#$t\n"; my $dumper = Data::Dumper->new([]); $dumper->Terse(1); $dumper->Values([$hash->{helper}{syncGroups}]); print FH $dumper->Dump; close(FH); } else { my $msg = "SB_SERVER_SaveSyncGroups: Cannot open $statefile: $!"; Log3 $hash, 1, $msg; } return undef; } sub SB_SERVER_LoadSyncGroups($) { my ($hash) = @_; my $name = $hash->{NAME}; return "No statefile specified" if(!$attr{global}{statefile}); my $statefile = SB_SERVER_StatefileName($name,'sbsg'); if(open(FH, "<$statefile")) { my $encoded; while (my $line = ) { chomp $line; next if($line =~ m/^#.*$/); $encoded .= $line; } close(FH); return if( !defined($encoded) ); my $decoded = eval $encoded; $hash->{helper}{syncGroups} = $decoded; } else { my $msg = "SB_SERVER_LoadSyncGroups: no syncgroups file found"; Log3 undef, 4, $msg; } return undef; } sub SB_SERVER_BuildPlayerList($) { my ($hash) = @_; my $name = $hash->{NAME}; delete( $hash->{helper}{players} ) if (defined($hash->{helper}{players})); # build player list foreach my $mydev ( keys %defs ) { my $iodevhash; if( defined( $defs{$mydev}{IODev} ) ) { $iodevhash = $defs{$mydev}{IODev}; if( ( defined( $defs{$mydev}{TYPE} ) ) && ( $defs{$mydev}{TYPE} eq "SB_PLAYER" )){ # CD 0029 umsortiert if( ( defined( $iodevhash->{NAME} ) ) && # CD 0029 umsortiert ( $iodevhash->{NAME} eq $name ) ) { # we found a valid entry my $chash = $defs{$mydev}; my $fn = $chash->{NAME}; my $ln = $chash->{PLAYERNAME}; if($ln ne '?') { if(!defined($hash->{helper}{players}{$fn})) { $hash->{helper}{players}{$fn}{fhemname}=$fn; $hash->{helper}{players}{$fn}{lmsname}=$ln; $hash->{helper}{players}{$fn}{mac}=$chash->{PLAYERMAC}; $hash->{helper}{players}{$fn}{type}='FHEM'; } if(!defined($hash->{helper}{players}{$ln})) { $hash->{helper}{players}{$ln}{fhemname}=$fn; $hash->{helper}{players}{$ln}{lmsname}=$ln; $hash->{helper}{players}{$ln}{mac}=$chash->{PLAYERMAC}; $hash->{helper}{players}{$ln}{type}='LMS'; } } } } } } } # ---------------------------------------------------------------------------- # load/save server state # # CD 0025 # ---------------------------------------------------------------------------- sub SB_SERVER_SaveServerStates($) { my( $hash ) = @_; my $name = $hash->{NAME}; return "No server states found" unless(defined($hash->{helper}{savedServerStates})); return "No statefile specified" if(!$attr{global}{statefile}); my $statefile = SB_SERVER_StatefileName($name,'sbst'); if(open(FH, ">$statefile")) { my $t = localtime; print FH "#$t\n"; my $dumper = Data::Dumper->new([]); $dumper->Terse(1); $dumper->Values([$hash->{helper}{savedServerStates}]); print FH $dumper->Dump; close(FH); } else { my $msg = "SB_SERVER_SaveServerState: Cannot open $statefile: $!"; Log3 $hash, 1, $msg; } return undef; } sub SB_SERVER_LoadServerStates($) { my ($hash) = @_; my $name = $hash->{NAME}; return "No statefile specified" if(!$attr{global}{statefile}); my $statefile = SB_SERVER_StatefileName($name,'sbst'); if(open(FH, "<$statefile")) { my $encoded; while (my $line = ) { chomp $line; next if($line =~ m/^#.*$/); $encoded .= $line; } close(FH); return if( !defined($encoded) ); my $decoded = eval $encoded; $hash->{helper}{savedServerStates} = $decoded; } else { my $msg = "SB_SERVER_LoadServerState: no server state file found"; Log3 undef, 4, $msg; } return undef; } sub SB_SERVER_Save($$) { my ( $hash, $statename ) = @_; my $name = $hash->{NAME}; $statename='default' unless defined($statename); Log3( $hash, 3, "SB_SERVER_Save($name): name: $statename"); delete($hash->{helper}{savedServerStates}{$statename}) if(defined($hash->{helper}{savedServerStates}) && defined($hash->{helper}{savedServerStates}{$statename})); SB_SERVER_BuildPlayerList($hash) if(!defined($hash->{helper}{players})); foreach my $e ( keys %{$hash->{helper}{players}} ) { if($e ne '') { if(defined($hash->{helper}{players}{$e})) { if($hash->{helper}{players}{$e}{type} eq 'FHEM') { if( defined( $defs{$e} ) ) { my $phash = $defs{$e}; $hash->{helper}{savedServerStates}{$statename}{players}{$e}{mac}=$hash->{helper}{players}{$e}{mac}; $hash->{helper}{savedServerStates}{$statename}{players}{$e}{power}=ReadingsVal($e,"power","off"); $hash->{helper}{savedServerStates}{$statename}{players}{$e}{volume}=ReadingsVal($e,"volume","0"); my $sm=InternalVal($e,"SYNCMASTER","none"); $hash->{helper}{savedServerStates}{$statename}{players}{$e}{syncMaster}=$sm; if(($sm eq 'none') || ($sm eq $hash->{helper}{players}{$e}{mac})) { SB_PLAYER_Save($phash,"xxx_sss_".$statename); } } } } } } } sub SB_SERVER_Recall($$) { my ( $hash, $arg ) = @_; # CD 0036 my $name = $hash->{NAME}; my $del=0; my $delonly=0; my $statename; my @args=split " ",$arg; if(defined($args[0])) { $statename=$args[0]; } else { $statename='default'; } Log3( $hash, 3, "SB_SERVER_Recall($name): name: $statename"); # Optionen auswerten for my $opt (@args) { $del=1 if($opt=~ m/^del$/); $delonly=1 if($opt=~ m/^delonly$/); } if(defined($hash->{helper}{savedServerStates}) && defined($hash->{helper}{savedServerStates}{$statename})) { if($delonly==0) { # unsync all & set power foreach my $e ( keys %{$hash->{helper}{savedServerStates}{$statename}{players}} ) { if($e ne '') { if(defined($hash->{helper}{savedServerStates}{$statename}{players}{$e})) { my $mac=$hash->{helper}{savedServerStates}{$statename}{players}{$e}{mac}; SB_SERVER_Write( $hash, $mac." sync -\n", "" ); if($hash->{helper}{savedServerStates}{$statename}{players}{$e}{power} eq 'on') { SB_SERVER_Write( $hash, $mac." power 1\n", "" ); } else { SB_SERVER_Write( $hash, $mac." power 0\n", "" ); } } } } # sync slaves & set volume foreach my $e ( keys %{$hash->{helper}{savedServerStates}{$statename}{players}} ) { if($e ne '') { if(defined($hash->{helper}{savedServerStates}{$statename}{players}{$e})) { my $mac=$hash->{helper}{savedServerStates}{$statename}{players}{$e}{mac}; if (($hash->{helper}{savedServerStates}{$statename}{players}{$e}{syncMaster} ne 'none') && ($hash->{helper}{savedServerStates}{$statename}{players}{$e}{syncMaster} ne $mac)) { SB_SERVER_Write( $hash, $hash->{helper}{savedServerStates}{$statename}{players}{$e}{syncMaster}." sync $mac\n", "" ); SB_SERVER_Write( $hash, "$mac mixer volume ". $hash->{helper}{savedServerStates}{$statename}{players}{$e}{volume}. "\n", "" ); } } } } # reload player states foreach my $e ( keys %{$hash->{helper}{savedServerStates}{$statename}{players}} ) { if($e ne '') { if(defined($hash->{helper}{savedServerStates}{$statename}{players}{$e})) { my $mac=$hash->{helper}{savedServerStates}{$statename}{players}{$e}{mac}; if (($hash->{helper}{savedServerStates}{$statename}{players}{$e}{syncMaster} eq 'none') || ($hash->{helper}{savedServerStates}{$statename}{players}{$e}{syncMaster} eq $mac)) { # CD 0026 ne durch eq ersetzt if( defined( $defs{$e} ) ) { my $phash = $defs{$e}; SB_PLAYER_Recall($phash,"xxx_sss_".$statename); } } } } } } if(($del==1)||($delonly==1)) { # delete foreach my $e ( keys %{$hash->{helper}{savedServerStates}{$statename}{players}} ) { if($e ne '') { if(defined($hash->{helper}{savedServerStates}{$statename}{players}{$e})) { if( defined( $defs{$e} ) ) { my $phash = $defs{$e}; SB_PLAYER_Recall($phash,"xxx_sss_".$statename." delonly"); } } } } delete($hash->{helper}{savedServerStates}{$statename}); } } } # CD 0031 User und Passwort speichern/lesen, aus 72_FRITZBOX sub SB_SERVER_storePassword($$$) { my ($hash, $user, $password) = @_; my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; my $key = getUniqueId().$index; my $enc_pwd = ""; if(eval "use Digest::MD5;1") { $key = Digest::MD5::md5_hex(unpack "H*", $key); $key .= Digest::MD5::md5_hex($key); } for my $char (split //, $password) { my $encode=chop($key); $enc_pwd.=sprintf("%.2x",ord($char)^ord($encode)); $key=$encode.$key; } my $err = setKeyValue($hash->{TYPE}."_".$hash->{NAME}."_user", $user); return "error while saving the user - $err" if(defined($err)); $err = setKeyValue($index, $enc_pwd); return "error while saving the password - $err" if(defined($err)); return "user and password successfully saved"; } sub SB_SERVER_readPassword($) { my ($hash) = @_; my $name = $hash->{NAME}; my $index = $hash->{TYPE}."_".$hash->{NAME}."_passwd"; my $key = getUniqueId().$index; my ($user, $password, $err); ($err, $password) = getKeyValue($index); if ( defined($err) ) { Log3 $hash, 2, "SB_SERVER_readPassword($name): unable to read SB_SERVER password: $err"; return undef; } my $dec_pwd = ''; if ( defined($password) ) { if ( eval "use Digest::MD5;1" ) { $key = Digest::MD5::md5_hex(unpack "H*", $key); $key .= Digest::MD5::md5_hex($key); } for my $char (map { pack('C', hex($_)) } ($password =~ /(..)/g)) { my $decode=chop($key); $dec_pwd.=chr(ord($char)^ord($decode)); $key=$decode.$key; } } else { Log3 $hash, 2, "SB_SERVER_readPassword($name): No password found"; return undef; } ($err, $user) = getKeyValue($hash->{TYPE}."_".$hash->{NAME}."_user"); if ( defined($err) ) { Log3 $hash, 2, "SB_SERVER_readPassword($name): unable to read SB_SERVER user: $err"; return undef; } if ( defined($user) ) { return ($user,$dec_pwd) } else { Log3 $hash, 2, "SB_SERVER_readPassword($name): No user found"; return undef; } } # CD 0031 end # ############################################################################ # No PERL code beyond this line # ############################################################################ 1; =pod =item device =item summary connect to a Logitech Media Server (LMS) =item summary_DE Anbindung an Logitech Media Server (LMS) =begin html

SB_SERVER

    Define
      define <name> SB_SERVER <ip[:cliserverport]> [RCC:<RCC>] [WOL:<WOL>] [PRESENCE:<PRESENCE>] [USER:<username>] [PASSWORD:<password>]

      This module allows you in combination with the module SB_PLAYER to control a Logitech Media Server (LMS) and connected Squeezebox Media Players.

      Attention: The [:cliserverport] parameter is optional. You just need to configure it if you changed it on the LMS. The default TCP port is 9090.

      Optional
      • <[RCC]>: You can define a FHEM RCC Device, if you want to wake it up when you set the SB_SERVER on.
      • <[WOL]>: You can define a FHEM WOL Device, if you want to wake it up when you set the SB_SERVER on.
      • <[PRESENCE]>: You can define a FHEM PRESENCE Device that is used to check if the server is reachable.
      • <username> and <password>: If your LMS is password protected you can define the credentials here.

    Set
      set <name> <command>

      This module supports the following SB_Server related commands:

      • abort - Stops the connection to the server
      • addToFHEMUpdate - Includes the modules in the FHEM update, needs to be executed only once
      • cliraw <cli-command> - Sends a <cli-command> to the LMS CLI
      • on - Tries to switch on the Server by WOL or RCC
      • removeFromFHEMUpdate - Removes the modules from the FHEM update
      • renew - Renews the connection to the server
      • rescan - Starts the scan of the music library of the server
      • statusRequest - Update of readings from server and configured players
      • save [<name>] - Save all players state
      • recall [<name>] [options] - recall all players state
        Options:
        • del - delete saved state after restore
        • delonly - delete saved state without restoring


      The command syncGroup can be used to manage group templates for synchronizing players. Each template contains a list of players and can be activated on demand. Possible subcommands:

      • addp <playerName[,playerName...]> <template> - Add the specified player(s) to the template. if the template doesn't exist, it is created automatically and the first player will be group master.
      • removep <playerName[,playerName...]> <template> - Remove the specified player(s) from the template. If one of the players was group master, the first remaining player will become new group master.
      • masterp <playerName> <template> - Change the group master.
      • load [poweron] <template> - Activate the template. The players of the template are unsynced from their groups and added to a new group. With the optional keyword poweron, FHEM tries to power on all the players.
      • delete <template> - Delete the template.
      • deleteall - Delete all templates.
      • talk <template> <text> - Save the state of all the players, activate the template and output <text> using the configured TTS provider. Restore the state of all the players after the end of the TTS output. With the optional keyword poweron, FHEM tries to power on all the players.
      • resetTTS - Reset TTS output.
      • volume <template> <n> - Set the volume to <n> if the template is active.
      • volume <template> +|-<n> - Increase or decrease the volume by the given value if the template is active.

    Attributes
    • alivetimer <sec>
      Default: 120. Every <sec> seconds it is checked, whether the computer with its LMS is still reachable – either via an internal ping (that leads regulary to problems) or via PRESENCE (preferred, no problems) - and running.
    • doalivecheck <true|false>
      Switches the LMS-monitoring on or off.
    • enablePlugins <plugin1[,pluginX]>
      Adds the playlists and favorites (if available) of the specified LMS-plugins.
    • httpport <port>
      Normally the http-port is set to 9000. If this ist NOT the case, you have to enter here the new port-number. You can check the port-number of the LMS within its setup under Setup – Network – Web Server Port Number.
    • ignoredIPs <IP-Address[,IP-Address]>
      With this attribute you can define IP-addresses of players which will to be ignored by the server, e.g. "192.168.0.11,192.168.0.37"
    • ignoredMACs <MAC-Address[,MAC-Address]>
      With this attribute you can define MAC-addresses of players which will to be ignored by the server, e.g. "00:11:22:33:44:55,ff:ee:dd:cc:bb:aa"
    • internalPingProtocol icmp|tcp|udp|syn|stream|none
      Specifies the protocol for the internal ping, default is tcp.
    • maxcmdstack <quantity>
      By default the stack ist set up to 200. If the connection to the LMS is lost, up to <quantity> commands are buffered. After the link is reconnected, commands, that are not older than five minutes, are send to the LMS.
    • maxfavorites <number>
      Adjust here the maximal number of the favourites.
=end html =begin html_DE

SB_SERVER

    Define
      define <name> SB_SERVER <ip[:cliserverport]> [RCC:<RCC>] [WOL:<WOL>] [PRESENCE:<PRESENCE>] [USER:<username>] [PASSWORD:<password>]

      Diese Modul ermöglicht es - zusammen mit dem Modul SB_PLAYER - einen Logitech Media Server (LMS) und die angeschlossenen Squeezebox Media Player zu steuern.

      Achtung: Die Angabe des Parameters [:cliserverport] ist optional und nur dann erforderlich, wenn die Portnummer im LMS vom Standardwert (TCP Port 9090) abweichend eingetragen wurde.

      Optionen
      • <[RCC]>: Hier kann ein FHEM RCC Device angegeben werden mit dem der Server aufgeweckt und eingeschaltet werden kann.
      • <[WOL]>: Hier kann ein FHEM WOL Device angegeben werden mit dem der Server aufgeweckt und eingeschaltet werden kann.
      • <[PRESENCE]>: Hier kann ein FHEM PRESENCE Device angegeben werden mit dem die Erreichbarkeit des Servers überprüft werden kann.
      • <username> and <password>: Falls der Server durch ein Passwort gesichert wurde, können hier die notwendigen Angaben für den Serverzugang angegeben werden.

    Set
      set <name> <command>

      Dieses Modul unterstützt folgende SB_SERVER relevanten Befehle:

      • abort - Bricht die Verbindung zum Server ab.
      • addToFHEMUpdate - Fügt die Module dem FHEM-Update hinzu, muss nur einmalig ausgeführt werden.
      • cliraw <cli-command> - Sendet einen CLI-Befehl an das LMS CLI
      • on - Versucht den Server per WOL oder RCC einzuschalten.
      • removeFromFHEMUpdate - Schließt die Module vom FHEM-Update aus.
      • renew - Erneuert die Verbindung zum Server.
      • rescan - Startet einen Scan der Musikbibliothek für alle im Server angegebenen Verzeichnisse.
      • statusRequest - Aktualisiert die Readings von Server und konfigurierten Playern.
      • save [<name>] - Speichert den Zustand aller Player unter dem Namen <name> ab.
      • recall [<name>] [options] - Ruft den Zustand aller Player auf.
        Optionen:
        • del - Löscht nach dem Restore den gespeicherten Status
        • delonly - Löscht den gespeicherten Status ohne vorherigem Restore


      Der Befehl syncGroup dient zur Verwaltung von Gruppenvorlagen für die Synchronisierung der Player. Darüber können Gruppen von Playern angelegt werden die sich bei Bedarf aktivieren lassen. Die Vorlagen werden bei SAVE und SHUTDOWN von FHEM abgespeichert und beim Start von FHEM geladen. Folgende Unterbefehle sind definiert:

      • addp <playerName[,playerName...]> <Vorlage> - Fügt die angegebenen Player zur Vorlage hinzu, wenn die Vorlage noch nicht existiert wird sie angelegt und der erste Player wird Master.
      • removep <playerName[,playerName...]> <Vorlage> - Entfernt die angegebenen Player aus der Vorlage, falls einer Master war wird der 1. verbleibende Player der Vorlage zum neuen Master.
      • masterp <playerName> <Vorlage> - Legt den angegebenen Player als Master fest.
      • load [poweron] <Vorlage> - Aktiviert die angegebene Vorlage, die betroffenen Player werden entsynchronisiert und zu einer neuen Gruppe zusammengefügt. Wenn zusätzlich poweron angegeben wird, werden die Player eingeschaltet.
      • delete <Vorlage> - Löscht die angegebene Vorlage.
      • deleteall - Löscht alle Gruppenvorlagen.
      • talk <Vorlage> <Text> - Speichert den Zustand aller Player ab, aktiviert die angegebene Vorlage und spielt den Text über den beim Player konfigurierten TTS-Dienst ab. Nach Ende der Durchsage wird der vorherige Zustand der Player wieder hergestellt. Wenn zusätzlich poweron angegeben wird, wird versucht die Player einzuschalten.
      • resetTTS - TTS zurücksetzen, kann nötig sein wenn die Ausgabe hängt.
      • volume <Vorlage> <n> - Stellt die Lautstärke auf einen Wert <n> ein. Dabei muss <n> eine Zahl zwischen 0 und 100 sein. Der Befehl wird nur ausgeführt wenn die Vorlage aktiv ist.
      • volume <Vorlage> +|-<n> - Erhöht oder vermindert die Lautstärke um den Wert, der durch +|-<n> vorgegeben wird. Dabei muss <n> eine Zahl zwischen 0 und 100 sein. Der Befehl wird nur ausgeführt wenn die Vorlage aktiv ist.

    Attribute
    • alivetimer <sec>
      Default 120. Alle <sec> Sekunden wird überprüft, ob der Rechner mit dem LMS noch erreichbar ist - entweder über internen Ping (führt zu regelmäßigen Hängern von FHEM) oder PRESENCE (bevorzugt, keine Hänger) - und ob der LMS noch läuft.
    • doalivecheck <true|false>
      Überwachung des LMS ein- oder auschalten.
    • enablePlugins <plugin1[,pluginX]>
      Bindet die Wiedergabelisten und Favoriten (soweit vorhanden) von LMS-Plugins (z.B. Spotify) ein.
    • httpport <port>
      Im Normalfall ist der http-Port auf 9000 eingestellt. Sollte dies NICHT der Fall sein muss hier die geänderte Portnummer eingetragen werden. Zur Überprüfung kann im Server unter Einstellungen – Erweitert –Netzwerk - Anschlussnummer des Webservers nachgeschlagen werden.
    • ignoredIPs <IP-Adresse>[,IP-Adresse]
      Mit diesem Attribut kann die automatische Erkennung dedizierter Geräte durch die Angabe derer IP-Adressen unterdrückt werden, z.B. "192.168.0.11,192.168.0.37"
    • ignoredMACs <MAC-Adresse>[,MAC-Adresse]
      Mit diesem Attribut kann die automatische Erkennung dedizierter Geräte durch die Angabe derer MAC-Adressen unterdrückt werden, z.B. "00:11:22:33:44:55,ff:ee:dd:cc:bb:aa"
    • internalPingProtocol icmp|tcp|udp|syn|stream|none
      Legt fest welches Protokoll für den internen Ping verwendet wird. Wenn das Attribut nicht definiert ist, wird tcp verwendet.
    • maxcmdstack <Anzahl>
      Default ist der Stack auf eine Größe von 200 eingestellt. Wenn die Verbindung zum LMS unterbrochen ist, werden bis zu <Anzahl> Befehle zwischengespeichert. Nach dem Verbindungsaufbau werden die Befehle, die nicht älter als 5 Minuten sind, an den LMS geschickt.
    • maxfavorites <Anzahl>
      Die maximale Anzahl der Favoriten wird hier eingestellt.
=end html_DE =cut