########################################################################################
# $Id: 70_SVDRP.pm 26920 2022-12-28 21:14:08Z hapege $
#
# SVDRP 
#
# control VDR via SVDRP
# refer to http://www.vdr-wiki.de/wiki/index.php/VDR_Optionen
#
# version history
#    1.01.01      first released version
#    1.01.02      bugfix for single-digit NextTimer 
#    1.01.03      corrections for german Umlaute
#    1.01.04      fix statusCheckInterval
#    1.01.05      added capability to control plugins (PLUG)
#                 added name to next timer
#                 expicit set for SatIP plugin
#                 handle HELP responses
#    1.01.06      strip <epgsearch> info from timername, fix stateFormat overwrite
#    1.01.07      optimize help text
#
########################################################################################
#
#  This programm is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  The GNU General Public License can be found at
#  http://www.gnu.org/copyleft/gpl.html.
#  A copy is found in the textfile GPL.txt and important notices to the license
#  from the author is found in LICENSE.txt distributed with these scripts.
#
#  This script is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
########################################################################################
  
package main;
use strict;
use warnings;

use Socket;     # For constants like AF_INET and SOCK_STREAM
#use Encode qw(encode);

use Blocking;
use Time::HiRes qw(gettimeofday);
use POSIX;

my $version = "1.01.07";

my %SVDRP_gets = (
  #
);

# Raw is not used by now
my %SVDRP_defaultsetsRaw = (
  "HITK"     => "",
  "LSTT"     => ":get",
  "LSTR"     => ":get",
  "NEXT"     => ":get",
  "STAT"     => ":disk",
  "UPDR"     => ":get",
  "CHAN"     => ":+,-",
  "DELT"     => "",
  "VOLU"     => ":+,-,mute",
  "cleanUp"  => ":noArg",
  "closeDev" => ":noArg",
  "connect"  => ":noArg"
);

my %SVDRP_defaultsets = (
  "HitKey"            => "",
  "ListTimers"        => ":noArg",
  "NextTimer"         => ":noArg",
  "DiskStatus"        => ":noArg",
  "UpdateRecordings"  => ":get",
  "Channel"           => ":+,-",
  "DeleteTimer"       => "",
  "Volume"            => ":+,-,mute",
  "cleanUp"           => ":noArg",
  "closeDev"          => ":noArg",
  "connect"           => ":noArg",
  "PowerOff"          => ":noArg",
  "ListRecording"     => "",
  "GetAll"            => ":noArg",
  "Plugin"            => "",
  "StreamdevServer"   => ":LSTC,DISC",
  "SatIP"             => ":INFO,MODE,LIST,SCAN,STAT,CONT,OPER,ATTA,DETA,TRAC",
  "Help"              => ":noArg"
);

my %SVDRP_defaultsets_unused = (
  "ListRecordings"    => ":get"
);

my %SVDRP_cmdmap = (
  "HitKey"           => "HITK",
  "ListTimers"       => "LSTT",
  "NextTimer"        => "NEXT",
  "DiskStatus"       => "STAT",
  "UpdateRecordings" => "UPDR",
  "Channel"          => "CHAN",
  "DeleteTimer"      => "DELT",
  "Volume"           => "VOLU",
  "ListRecording"    => "LSTR",
  "Plugin"           => "PLUG",
  "StreamdevServer"  => "PLUG streamdev-server",
  "SatIP"            => "PLUG satip",
  "Help"             => "HELP"
);

my @SVDRP_statusCmds = ("LSTT", "NEXT", "CHAN", "VOLU", "STAT");

my %SVDRP_cmdmap_unused = (
  "ListRecordings" => "LSTR"
);

my %SVDRP_data = (
  #
);

my %SVDRP_timers = (
  #
);

my %SVDRP_result;
my %SVDRPaddattrs;

my %SVDRP_sets = %SVDRP_defaultsets;

sub SVDRP_Define {
  my ($hash, $def) = @_;
  my @param = split('[ \t]+', $def);
    
  if(int(@param) < 3) {
    return "too few parameters: define <name> SVDRP <IP_Address> [<port>]";
  }
  $hash->{NAME}       = $param[0];
  $hash->{IP_Address} = $param[2];
  if (!$param[3]){
    $hash->{port}     = "6419";
  }
  else{
    $hash->{port}     = $param[3];
  }
  $hash->{DeviceName} = $param[2].":".$hash->{port};
  
  # prevent "reappeared" messages in loglevel 1
  $hash->{devioLoglevel} = 3;
  # prevent DevIO from setting "STATE" at connect/disconnect
  $hash->{devioNoSTATE} = 1;
  # subscribe only to notify from global and self
  $hash->{NOTIFYDEV} = "global,TYPE=SVDRP";
  # set version
  $hash->{version} = $version;  
  
  my $name = $hash->{NAME}; 
  
  # clean up
  RemoveInternalTimer($hash, "SVDRP_checkConnection");
  main::Log3 $name, 5,"[$name]: Define: status timer removed";
  DevIo_CloseDev($hash);

  # force immediate reconnect
  delete $hash->{NEXT_OPEN} if ( defined( $hash->{NEXT_OPEN} ) );
  # commented to not automatically connect...
  #DevIo_OpenDev($hash, 0, "SVDRP_Init", "SVDRP_Callback");

  return ;
}

sub SVDRP_Undef {
  my ($hash, $arg) = @_;
  my $name = $hash->{NAME}; 
  RemoveInternalTimer($hash);
  main::Log3 $name, 5,"[$name]: Undef: status timer removed";
  BlockingKill( $hash->{helper}{RUNNING_PID} ) if ( defined( $hash->{helper}{RUNNING_PID} ) );
  DevIo_CloseDev($hash);
  return ;
}

sub SVDRP_Shutdown {
  my ($hash) = @_;
  my $name = $hash->{NAME}; 
  RemoveInternalTimer($hash);
  main::Log3 $name, 5,"[$name]: Shutdown: status timer removed";
  DevIo_CloseDev($hash);
  BlockingKill( $hash->{helper}{RUNNING_PID} ) if ( defined( $hash->{helper}{RUNNING_PID} ) );
  delete $hash->{helper}{nextConnectionCheck} if ( defined( $hash->{helper}{nextConnectionCheck} ) );
  delete $hash->{helper}{nextStatusCheck} if ( defined( $hash->{helper}{nextStatusCheck} ) );
  delete $hash->{helper}{RUNNING_PID} if ( defined( $hash->{helper}{RUNNING_PID} ) );
}

sub SVDRP_Initialize {
    my ($hash) = @_;

    $hash->{DefFn}      = \&SVDRP_Define;
    $hash->{UndefFn}    = \&SVDRP_Undef;
    $hash->{SetFn}      = \&SVDRP_Set;
    $hash->{AttrFn}     = \&SVDRP_Attr;
    $hash->{ReadFn}     = \&SVDRP_Read;
    $hash->{ReadyFn}    = \&SVDRP_Ready;
    $hash->{NotifyFn}   = \&SVDRP_Notify;
    #$hash->{StateFn}    = \&SVDRP_State;
    $hash->{ShutdownFn} = \&SVDRP_Shutdown;
    #$hash->{GetFn}      = \&SVDRP_Get;   # not required
    #$hash->{DeleteFn}   = \&SVDRP_Delete;
    #$hash->{RenameFn}   = \&SVDRP_Rename;
    #$hash->{DelayedShutdownFn} = \&SVDRP_DelayedShutdown;
    
    $hash->{AttrList} =
          "delay:1,2,3,4,5 RecordingInfo:short,long connectionCheck:off,1,15,30,60,120,300,600,3600 AdditionalSettings statusCheckCmd statusCheckInterval:off,1,5,10,15,30,60,300,600,3600 statusOfflineMsg disable:0,1 "
        . $readingFnAttributes;
}

sub SVDRP_Notify($$) {
  my ($hash, $devHash) = @_;
  my $name = $hash->{NAME}; # own name / hash
  my $devName = $devHash->{NAME}; # Device that created the events
  my $checkInterval;
  my $next;

  if(IsDisabled($name)){
    main::Log3 $name, 5, "[$name]: Notify: $name is disabled by framework!";
    return;
  }

  my $events = deviceEvents($devHash,1);
  #return if( !$events );

  # logging of notifies
  #main::Log3 $name, 5, "[$name]: running notify from $devName for $name, event is @{$events}";
  
  if($devName eq "global" && grep(m/^INITIALIZED|REREADCFG$/, @{$events})){    
    
    if ( defined( $hash->{AdditionalSettings} ))
    {
      SVDRP_Attr("set",$name,"AdditionalSettings",$hash->{AdditionalSettings});
      main::Log3 $name, 5, "adding attrs: $name, ".$hash->{AdditionalSettings};
    }  
  }
  return;
}

sub SVDRP_Attr {
  my ($cmd,$name,$attr_name,$attr_value) = @_;
	my $hash = $defs{$name};
  my $checkInterval;
  my $next;
  main::Log3 $name, 5,"[$name]: Attr: executing $cmd $attr_name to $attr_value";
	if($cmd eq "set") {
    if ($attr_name eq "AdditionalSettings") {
      my @valarray = split / /, $attr_value;
      my $key;
      my $newkey;
      my $newkeyval = "";
      %SVDRPaddattrs = ();
      $hash->{AdditionalSettings} = $attr_value;
      foreach $key (@valarray) {
        #main::Log3 $name, 3,"[$name]: key is $key";
        $newkey = (split /:/, $key, 2)[0];
        # check if AdditionalSetting is only cmd (e.g. "LSTR") without parameter (e.g. ":1,2,3")
        # otherwise take it as ""
        if (defined ((split /:/, $key, 2)[1])){
          $newkeyval = ":".(split /:/, $key, 2)[1];
        }
        main::Log3 $name, 5,"[$name]: Attr: setting $attr_name, key  is $newkey, val is $newkeyval";
        $SVDRPaddattrs{$newkey} = $newkeyval;
        %SVDRP_sets = (%SVDRP_sets, %SVDRPaddattrs);
      }
    }
    elsif ($attr_name eq "connectionCheck"){
      if ($attr_value eq "0") {
        # avoid 0 timer
        return "0 not allowed for $attr_name!";
      } 
      elsif ($attr_value eq "off"){
        RemoveInternalTimer($hash, "SVDRP_checkConnection");
        main::Log3 $name, 5,"[$name]: Attr: status timer removed";
        $hash->{helper}{nextConnectionCheck} = "off";
      }
      else{
        RemoveInternalTimer($hash, "SVDRP_checkConnection");
        main::Log3 $name, 5,"[$name]: Attr: status timer removed";
        $checkInterval = $attr_value;
        $next = gettimeofday() + $checkInterval;
        $hash->{helper}{nextConnectionCheck} = $next;
        InternalTimer( $next, "SVDRP_checkConnection", $hash);
        main::Log3 $name, 5,"[$name]: Attr: set $attr_name interval to $attr_value";        
      }
    }
    elsif ($attr_name eq "statusCheckInterval"){
      # timer to check status of device
      if ($attr_value eq "0") {
        # 0 means off
        return "0 not allowed for $attr_name!";
      } 
      elsif ($attr_value eq "off"){
        RemoveInternalTimer($hash, "SVDRP_checkStatus");
        main::Log3 $name, 5,"[$name]: Attr: status timer removed";
        $hash->{helper}{nextStatusCheck} = "off";
      }
      else{
        RemoveInternalTimer($hash, "SVDRP_checkStatus");
        main::Log3 $name, 5,"[$name]: Attr: status timer removed";
        $checkInterval = $attr_value;
        $next = gettimeofday() + $checkInterval;
        $hash->{helper}{nextStatusCheck} = $next;
        InternalTimer( $next, "SVDRP_checkStatus", $hash);
        main::Log3 $name, 5,"[$name]: Attr: set $attr_name interval to $attr_value";        
      }
    }
    elsif ($attr_name eq "StatusCheckCmd"){
      # decided not to check for allowed commands, user's freedom to define...
    }        
  }
  elsif($cmd eq "del"){
    if($attr_name eq "AdditionalSettings") {
      %SVDRPaddattrs = ();
      %SVDRP_sets = %SVDRP_defaultsets;
      main::Log3 $name, 5,"[$name]: Attr: deleting $attr_name";
    }
    elsif($attr_name eq "connectionCheck") {
      RemoveInternalTimer($hash, "SVDRP_checkConnection");
      main::Log3 $name, 5,"[$name]: Attr: status timer removed";
      delete $hash->{helper}{nextConnectionCheck} if (defined($hash->{helper}{nextConnectionCheck}));
      # next 4 lines to set default value 600, timer running ech 600s
      #my $next = gettimeofday() + "600";
      #$hash->{helper}{nextConnectionCheck} = $next;
      #InternalTimer( $next, "SVDRP_checkConnection", $hash);
      #main::Log3 $name, 5,"[$name]: Attr: $attr_name removed, timer set to +600";
    }
    elsif($attr_name eq "statusCheckInterval") {
      RemoveInternalTimer($hash, "SVDRP_checkStatus");
      main::Log3 $name, 5,"[$name]: Attr: status timer removed";
      delete $hash->{helper}{nextStatusCheck} if (defined($hash->{helper}{nextStatusCheck}));
      # next 4 lines to set default value 600, timer running ech 600s
      #my $next = gettimeofday() + "600";
      #$hash->{helper}{nextStatusCheck} = $next;
      #InternalTimer( $next, "SVDRP_checkStatus", $hash);
      #main::Log3 $name, 5,"[$name]: Attr: $attr_name removed, timer set to +600";
    }
    elsif($attr_name eq "statusCheckInterval") {
      # do nothing
    }    
  }
  return ;
}

sub SVDRP_Ready($){
  my ($hash) = @_;
  #return DevIo_OpenDev($hash, 1, undef );  
}

sub SVDRP_State($$$$){
  # not needed ... ?
  my ($hash, $time, $readingName, $value) = @_;
  my $name = $hash->{NAME};
  Log3 $name, 5, "[$name] SetState called";  
  return undef;
}

sub SVDRP_Get {
	# return immediately, not required currently
    return "none";
}

sub SVDRP_cleanUp {
  my ($hash) = @_;
  my $name = $hash->{NAME};
  main::Log3 $name, 5, "[$name]: cleanup: sending quit, close DevIo";
  DevIo_SimpleWrite($hash, "quit\r\n", "2");    
  #RemoveInternalTimer($hash);
  #main::Log3 $name, 5,"[$name]: cleanUp: status timer removed";
  BlockingKill( $hash->{helper}{RUNNING_PID} ) if ( defined( $hash->{helper}{RUNNING_PID} ) );
  delete $hash->{helper}{RUNNING_PID} if ( defined( $hash->{helper}{RUNNING_PID} ) );
  # give VDR 1 s to react before we close connection
  my $next = gettimeofday() + 3;
  InternalTimer( $next, "SVDRP_closeDev", $hash);
  #DevIo_CloseDev($hash);
  #$hash->{STATE} = "closed";
  #$hash->{PARTIAL}="";
  # reset .sendingcmd to indicate that cmd sending cycle is done
  readingsSingleUpdate($hash, ".sendingcmd", "0", 1);
  return ;
}

sub SVDRP_closeDev {
  my ($hash) = @_;
  my $name = $hash->{NAME};
  main::Log3 $name, 5,"[$name]: closeDev: closing...";
  delete $hash->{DevIoJustClosed} if (defined($hash->{DevIoJustClosed}));
  DevIo_CloseDev($hash);
  #$hash->{STATE} = "closed";
  $hash->{PARTIAL}="";
}

sub SVDRP_Init($){
  # default: no action - here we just could initializes connection check
  my ($hash) = @_;
  my $name = $hash->{NAME};
  main::Log3 $name, 5,"[$name]: Init: DevIo initializing";
  #SVDRP_checkStatus($hash);
  # my $checkInterval = AttrVal( $name, "connectionCheck", "60" );
  # #set checkInterval to 60 just for first check;
  # if ($checkInterval eq "off"){$checkInterval = 60;}
    
  #RemoveInternalTimer($hash, "SVDRP_checkConnection");
  #main::Log3 $name, 5,"[$name]: Init: status timer removed";  
  
  # my $next = gettimeofday() + $checkInterval;
  # InternalTimer($next , "SVDRP_checkConnection", $hash);
  # #SVDRP_singleWrite("VDRcontrol|STAT|disk");  
  return undef; 
}

sub SVDRP_ReInit($){
  # no action - just log subroutine call
  my ($hash) = @_;
  my $name = $hash->{NAME};
  main::Log3 $name, 5,"[$name]: ReInit: DevIo ReInit done"; 
  return undef; 
}

sub SVDRP_Callback($){
    # will be executed after connection establishment (see DevIo_OpenDev())
    my ($hash, $error) = @_;
    my $name = $hash->{NAME};

    if ($error){
      main::Log3 $name, 3, "[$name]: Callback: DevIo callback error: $error";
      SVDRP_closeDev($hash);
      my $offlineMsg = AttrVal( $name, "statusOfflineMsg", "offline" );
      my $rv = readingsSingleUpdate($hash, "globalError", $offlineMsg, 1);
      main::Log3 $name, 5,"[$name]: Callback: [$name] DevIo callback: globalError set to $offlineMsg";

    }
    else{
      main::Log3 $name, 3, "[$name]: Callback: DevIo callback with no error";
    }
    
    # check for disconnected - not required, since we need to close connection after each request
    # otherwise, no other svdr client can connect!
    # #my $status = $hash->{STATE};
    # #my $status = DevIo_getState($hash);
    # my $status = ReadingsVal($name, "state", "disconnected");
    # my $offlineMsg = AttrVal( $name, "statusOfflineMsg", "offline" );
    # if ($status eq "disconnected"){
    #   # remove timers and pending setValue calls if device is disconnected
    #   main::Log3 $name, 3, "[$name]: Callback: DevIo callback error: STATE is $status";
    #   my $rv = readingsSingleUpdate($hash, "globalError", $offlineMsg, 1);
    #   RemoveInternalTimer($hash);
    #   main::Log3 $name, 5,"[$name]: Callback: status timer removed";
    #   delete $hash->{helper}{nextConnectionCheck}
    #     if ( defined( $hash->{helper}{nextConnectionCheck} ) );
    #   delete $hash->{helper}{nextStatusCheck}
    #     if ( defined( $hash->{helper}{nextStatusCheck} ) );
    #   BlockingKill( $hash->{helper}{RUNNING_PID} ) if ( defined( $hash->{helper}{RUNNING_PID} ) );

    #   # check if we should update statusCheck
    #   my $checkInterval = AttrVal( $name, "statusCheckInterval", "off" );
    #   my $checkcmd = AttrVal( $name, "statusCheckCmd", "DiskStatus" );
    #   #my $offlineMsg = AttrVal( $name, "statusOfflineMsg", "offline" );
  
    #   if ($checkInterval ne "off"){
    #     my $rv = readingsSingleUpdate($hash, $checkcmd, $offlineMsg, 1);
    #     main::Log3 $name, 5,"[$name]: Callback: [$name] DevIo callback: $checkcmd set to $offlineMsg";
    #     #SVDRP_checkStatus($hash);
    #     main::Log3 $name, 5,"[$name]: Callback: offline, but status timer set";
    #     return ;
    #   }      
    # }    
    return undef; 
}

sub SVDRP_Read($){
  # used by devio
  my ($hash) = @_;
  my $name = $hash->{NAME};
  
  # read the available data
  my $data = DevIo_SimpleRead($hash);
  Log3 $name, 5, "[$name]: Read: function called";
  # stop processing if no data is available (device disconnected)
  return if(!defined($data)); # connection lost
  
  #Log3 $name, 5, "[$name] Read received: $data";

  my $buffer = $hash->{PARTIAL};
  #Log3 $name, 3, "[$name] Read: received $data (buffer contains: $buffer)";
  
  # concat received data to $buffer
  my $result = $data;
  $buffer .= $result;
  Log3 $name, 5, "[$name]: Read: received: $result";
  Log3 $name, 5, "[$name]: Read: buffer contains: $buffer";

  # as long as the buffer contains newlines (complete datagramm)
  my $msg = "none";
   while($buffer =~ m/\n/)
   {
     #my $msg;
     # extract the complete message ($msg), everything else is assigned to $buffer
     ($msg, $buffer) = split("\n", $buffer, 2);
     # remove trailing whitespaces
     chomp $msg;
     # now we could parse the extracted message, not implemented, since I get no data...
     SVDRP_parseMessage($hash, $msg);
   }
  # update $hash->{PARTIAL} with the current buffer content
  $hash->{PARTIAL} = $buffer;
  #Log3 $name, 5, "[$name] Read: after LF check, msg is: $msg";
  #Log3 $name, 5, "[$name] Read: after LF check, buffer contains: $buffer"; 
}

sub SVDRP_parseMessage {
  # called from Read with $hash, $msg
  # $msg contains one complete line - but one only!
  my ($hash, $msg) = @_;
  my $name = $hash->{NAME};
  #my ($input) = @_;
  #Log3 "VDR", 5, "[VSR] Parse: input: $input";
  #my ($name, $msg) = split "|", $input;
  Log3 $name, 5, "[$name] Parse: name: $name, msg: $msg";
  #my $hash = $defs{$name};
  #$msg = $hash->{PARTIAL};
  # strip last "|"
  #$msg = substr $msg, 0, -1;
  #my @resultarr = split("\\|", $msg);
  my $reading = "(unknown)";
  my $data;
  my $rv;
  my $count = 0;
  my $output;
  my $timers = "";
  my $parsedmsg = "";
  my $code;
  my $recording = "";
  my $plugin = "";
  my $text="";
   
  #readingsBeginUpdate($hash);
   
  ### now we should analyse which message was received, and put it to the right reading
  #if ($msg =~ /^22[0|1]/){
  if ($msg =~ /^220/){
    # format: 220 VDR SVDRP VideoDiskRecorder 2.0.6; Sun Feb 13 17:33:10 2022; UTF-8
    $reading = "infoOpen";
    (my $code, $msg) = split (/ /, $msg, 2);
    $rv = readingsSingleUpdate($hash, $reading, $msg, 1);
    #Log3 $name, 5, "[$name] Parse: updated $reading with '$msg'";
  }
  elsif ($msg =~ /^221/){
    # format: 220 VDR SVDRP VideoDiskRecorder 2.0.6; Sun Feb 13 17:33:10 2022; UTF-8
    $reading = "infoClose";
    (my $code, $msg) = split (/ /, $msg, 2);
    $rv = readingsSingleUpdate($hash, $reading, $msg, 1);
    #Log3 $name, 5, "[$name] Parse: updated $reading with '$msg'";
  }  
  elsif ($msg =~ /^5\d\d/){
    # format: 5xx some error message
    $reading = "infoError";
    (my $code, $msg) = split (/ /, $msg, 2);
    $rv = readingsSingleUpdate($hash, $reading, $msg, 1);
    #Log3 $name, 5, "[$name] Parse: updated $reading with '$msg'";
  }
  elsif ($msg =~ /^250[ ]\d+MB[ ]\d+MB[ ]\d+%\s$/){
    # disk status format: 250 1760874MB 476308MB 72%  
    $reading = "DiskStatus";
    #$rv = readingsSingleUpdate($hash, $reading, $msg, 1);
    SVDRP_parseDiskStatus($hash, $reading, $msg);
    #Log3 $name, 5, "[$name] Parse: updated $reading with '$msg'";    
  }
  elsif ($msg =~ /^250[ ]\d+[ ][A-Za-z]{3}[ ][A-Za-z]{3}[ ]{1,2}[0-9]{1,2}[ ][0-9]{2}:[0-9]{2}:[0-9]{2}[ ][0-9]{4}\s$/){  
    # next timer format: 250 1 Tue Mar 15 09:50:00 2022
    $reading = "NextTimer";
    $msg = SVDRP_parseNextTimer($hash, $reading, $msg);
    # seems that only with newline the sprintf formatting is kept - but I don't like it here ;-)
    #$msg = $timers."\n".$msg;
    readingsSingleUpdate($hash, $reading, $msg, 1); 
  }  
  elsif ($msg =~ /^250[ ]\d+[ ][A-Za-z0-9\h\.\-_?!#]+\s$/){
    # Channel format: 250 4 RTL Television
    $reading = "Channel";
    (my $code, $msg) = split (/ /, $msg, 2);
    #$msg = substr $msg, 0, -1;
    $rv = readingsSingleUpdate($hash, $reading, $msg, 1);
    #Log3 $name, 5, "[$name] Parse: updated $reading with $msg"
  }
  elsif ($msg =~ /^250[ ]Audio[ ]volume[ ]is[ ][0-9]+|mute\s$/){
    # Vol format: 250 Audio volume is 245
    $reading = "Volume";
    (my $code, $msg) = split (/ /, $msg, 2);
    $rv = readingsSingleUpdate($hash, $reading, $msg, 1);
    #Log3 $name, 5, "[$name] Parse: updated $reading with $msg"
  }
  elsif ($msg =~ /^250[ ]Key[ ][A-Za-z0-9"]+[ ]accepted\s$/){
    # HitKey format: 250 Key "up" accepted
    $reading = "HitKeyInfo";
    (my $code, $msg) = split (/ /, $msg, 2);
    $rv = readingsSingleUpdate($hash, $reading, $msg, 1);
    #Log3 $name, 5, "[$name] Parse: updated $reading with $msg"
  }
  elsif ($msg =~ /^250[-|\h]\d+[ ]\d+:\d+:[A-Za-z\-]{7}/ ||
        $msg =~ /^250[-|\h]\d+[ ]\d+:\d+:\d{4}-\d{2}-\d{2}:\d{4}:\d{4}:\d{2}:\d{2}:/){
    # ListTimer formats:
    # 250 1 1:1:MTWTF--@2022-03-15:0950:1115:50:99:Verrückt nach Meer (neu):
    # 250 2 1:4:2022-02-13:1858:1915:50:99:RTL Aktuell - Das Wetter:
    $reading = "ListTimers";
    # check if we got "250-n"
    if (substr($msg, 3, 1) eq "-"){
      ($code, $msg) = split (/-/, $msg, 2);
      #Log3 $name, 5, "[$name] Parse: substring contains '-'";
    }
    else{
      ($code, $msg) = split (/ /, $msg, 2);
    }
    $timers = ReadingsVal($name, $reading, "");
    $msg = SVDRP_parseTimer($name, $msg);
    #Log3 $name, 5, "[$name] Parse: parseTimer returned $msg";
    $msg = $timers."\n".$msg;
    $rv = readingsSingleUpdate($hash, $reading, $msg, 1);
    #Log3 $name, 5, "[$name] Parse: updated $reading with $parsedmsg"
  }
  elsif ($msg =~ /^250-\d+\h[0-9]{2}\.[0-9]{2}\.[0-9]{2}\h[0-9]{2}:[0-9]{2}\h/){
    # Recording List format:
    # 250-84 26.02.20 16:05v 1:25* Verrückt nach Meer~Staffel 09
    $reading = "Recordings";
    # check if we got "250-n"
    if (substr($msg, 3, 1) eq "-"){
      ($code, $msg) = split (/-/, $msg, 2);
      #Log3 $name, 5, "[$name] Parse: substring contains '-'";
    }
    else{
      ($code, $msg) = split (/ /, $msg, 2);
    }
    $recording = ReadingsVal($name, $reading, "");
    $msg = $recording."\n".$msg;    
    $rv = readingsSingleUpdate($hash, $reading, $msg, 1);
    #Log3 $name, 5, "[$name] Parse: updated $reading with $msg"        
  }
  elsif ($msg =~ /^215/){
    # Recording format: 215-xxxx
    $reading = "Recordings";
    # check if we got "215-n"
    if (substr($msg, 3, 1) eq "-"){
      ($code, $msg) = split (/-/, $msg, 2);
      #Log3 $name, 5, "[$name] Parse: substring contains '-'";
    }
    else{
      ($code, $msg) = split (/ /, $msg, 2);
    }
    $recording = ReadingsVal($name, $reading, "");
    $msg = SVDRP_parseRecording($name, $msg);
    if ($msg ne "none"){
      $msg = $recording."\n".$msg;
      $rv = readingsSingleUpdate($hash, $reading, $msg, 1);
      Log3 $name, 5, "[$name] Parse: updated $reading with '$msg'";    
    }
    #$rv = readingsSingleUpdate($hash, $reading, $msg, 1);
    #Log3 $name, 5, "[$name] Parse: updated $reading with $msg"
  }

  elsif ($msg =~ /^214/){
    # Helpinfo format: 214-xxxx
    $reading = "HelpInfo";
    # empty HelpInfo reading, if we got a new one
    my $sendingcmd = ReadingsVal($name,".sendingcmd","0");
    if ($sendingcmd eq "1") {
      readingsBeginUpdate($hash);
      readingsBulkUpdate($hash, ".sendingcmd", "0", 1);
      readingsBulkUpdate($hash, $reading, "", 1);
      readingsEndUpdate($hash, 1);
    }
    # check if we got "214-n"
    if (substr($msg, 3, 1) eq "-"){
      ($code, $msg) = split (/-/, $msg, 2);
      #Log3 $name, 5, "[$name] Parse: HelpInfo: substring contains '-'";
    }
    else{
      ($code, $msg) = split (/ /, $msg, 2);
    }
    $text = ReadingsVal($name, $reading, "");
    $msg = SVDRP_parseHelpinfo($name, $msg);
    if ($msg ne "none"){
      $msg = $text."\n".$msg;
      $rv = readingsSingleUpdate($hash, $reading, $msg, 1);
      Log3 $name, 5, "[$name] Parse: HelpInfo: updated $reading with '$msg'";    
    }
    #$rv = readingsSingleUpdate($hash, $reading, $msg, 1);
    #Log3 $name, 5, "[$name] Parse: HelpInfo: updated $reading with $msg"
  }

  elsif ($msg =~ /^9[0-9][0-9]/){
    # PLUG resonse is like
    # 900-SAT>IP device: 0
    # 900-CardIndex: 0
    # 900-Stream: rtsp://10.1.1.9/?src=1&freq=11185&pol=v&ro=0.35&msys=dvbs2&mtype=8psk&sr=22000&fec=23 (Unicast) [stream=1]
    # 900-Signal: lock=1 strength=67 quality=100 frontend=1
    # 900-Stream bitrate: 75 kB/s
    # 900-Buffer bitrate: 0 kB/s
    # 900-Buffer usage: 0/2048 kB (0,0%)
    # 900-Channel: Das Erste HD;ARD:11493:HC23M5O35P0S1:S19.2E:22000:5101=27:5102=deu@3,5103=mis@3,5107=qks@3;5106=deu@106:5104;5105=deu:0:10301:1:1019:0
    # 900-Active pids:
    # 900-Active section filters:
    # 900-Filter 0:    7 (   9 kB/s) Pid=0x12 (EIT)
    # 900-Filter 1:    0 (   0 kB/s) Pid=0x14 (TDT)
    # 900-Filter 2:    2 (   0 kB/s) Pid=0x00 (PAT)
    # 900-Filter 3:    0 (   0 kB/s) Pid=0x11 (SDT)
    # 900-Filter 4:    0 (   0 kB/s) Pid=0x10 (NIT)
    # 900 Filter 5:    0 (   0 kB/s) Pid=0x60 (---)
    $reading = "PluginInfo";

    # check if we got "900-n"
    if (substr($msg, 3, 1) eq "-"){
      ($code, $msg) = split (/-/, $msg, 2);
      #Log3 $name, 5, "[$name] Parse: substring contains '-'";
    }
    else{
      ($code, $msg) = split (/ /, $msg, 2);
    }
  
    $plugin = ReadingsVal($name, $reading, "");
    $msg = SVDRP_parsePlugin($name, $msg);

    if ($msg ne "none"){
      $msg = $plugin."\n".$msg;
      $rv = readingsSingleUpdate($hash, $reading, $msg, 1);
      Log3 $name, 5, "[$name] Parse: updated $reading with '$msg'";    
    }

    #Log3 $name, 5, "[$name] Parse: parsePlugin returned $msg";
    #$msg = $plugin."\n".$msg;
    #$rv = readingsSingleUpdate($hash, $reading, $msg, 1);
    #Log3 $name, 5, "[$name] Parse: updated $reading with $parsedmsg"
  }

  #Log3 $name, 5, "[$name] Parse: updated $reading with '$msg'";
}

sub SVDRP_parseDiskStatus{
  my ($hash,$reading,$resultarr) = @_;
  my $name = $hash->{NAME};
  my ($code, $disksize, $diskfree, $diskspace) = (split (" ", $resultarr,4));
  my $sizeunit = "GB";
  my $freeunit = "GB";
  my $rv;
  # strip unit "MB", keep only numbers
  $disksize =~ tr/0-9//cd;
  $diskfree =~ tr/0-9//cd;
  Log3 $name, 5, "[$name] Parse: Disksize: $disksize, Diskfree: $diskfree";
  $disksize = $disksize / 1024;
  if ($disksize > 1000){
    $disksize = sprintf ("%.1f", $disksize / 1024);
    $sizeunit = "TB";
  }
  else{
    $disksize = sprintf ("%.1f", $disksize);
  }
  $diskfree = $diskfree / 1024;
  if ($diskfree > 1000){
    $diskfree = $diskfree / 1024;
    $freeunit = "TB";
  }
  else{
    $diskfree = sprintf ("%.1f", $diskfree);
  }       
  my $returnval = "Size: ".$disksize.$sizeunit." | Free: ".$diskfree.$freeunit." | Used: ".$diskspace; 
  readingsBeginUpdate($hash);
  $rv = readingsBulkUpdate($hash, "DiskUsed", $diskspace, 1);
  $rv = readingsBulkUpdate($hash, $reading, $returnval, 1);
  readingsEndUpdate($hash, 1);
  #$rv = readingsBulkUpdate($hash, $reading, $resultarr[0], 1);
}

sub SVDRP_parseTimer{
  my ($name, $msg) = @_;
  my $hash = $defs{$name};
  #$count = 0;
  #$output = "";
  my $parsedmsg = "none";
  my $timerid = "0";
  my $timerstr = "none";
  my $i1 = "0";
  my $i2 = "0",
  my $day = "none";
  my $start = "0";
  my $end = "0";
  my $i3 = "0";
  my $i4 = "0";
  my $timername = "none";
  my $timernameraw = "none";
  if (!defined($msg)){
    $parsedmsg = "error";
  }
  else{
    # format variants:
    # 1 1:1:MTWTF--@2022-03-15:0950:1115:50:99:Verrückt nach Meer (neu):
    # 2 1:4:2022-02-13:1858:1915:50:99:RTL Aktuell - Das Wetter:
    #Log3 $name, 5, "[$name] ParseTimer: reading: $reading, result: $resultarr[$count]";
    ($timerid, $timerstr) = split (" ", $msg,2);
    ($i1, $i2, $day, $start, $end, $i3, $i4, $timernameraw) = split (":", $timerstr, 8);
    substr ($start, 2, 0) = ":";
    substr ($end, 2, 0) = ":";
    
    # strip <epgsearch> info from timername
    $timernameraw =~ s/[:\r]//g;
    $timername = (split /<epgsearch>/, $timernameraw, 2)[0];
    # store timer ID and Name in hidden setting, to re-use with NextTimer command
    #$timername =~ s/[:\r\n]//g;
    #$timername =~ s/[:\r]//g;
    $SVDRP_timers{$timerid} = $timername;
    readingsSingleUpdate( $hash, ".Timers", encode_json( \%SVDRP_timers ), 1 );
   
    $parsedmsg = "ID: ".sprintf("%2s",$timerid)." | Day: ".sprintf("%-10s",$day)." | Start: ".$start." | Stop: ".$end." | Name: ".$timername;
    
  }
  #Log3 $name, 5, "[$name] parseTimer: parsed output is $parsedmsg";
  return $parsedmsg;
}

sub SVDRP_parseNextTimer {
  my ($hash, $reading, $msg) = @_;
  #my $hash = $defs{$name};
  my $name = $hash->{NAME};
  my $timername = "";
  (my $code, $msg) = split (/ /, $msg, 2);
  # replace double blank by single blank
  $msg =~ s/  / /g;
  Log3 $name, 5, "[$name] Parse: NextTimer: $msg";
  (my $tid, my $tday, my $tmonth, my $tdate, my $tstart, my $tyear) = split (/ /,$msg);
  Log3 $name, 5, "[$name] Parse: NextTimer: $tid - $tday - $tmonth - $tdate - $tstart - $tyear";
  # get timer name from hidden reading (requires ListTimer to be run)
  my %myVDRtimers = SVDRP_restoreJson($hash, ".Timers");
  if (exists($myVDRtimers{$tid})){
    $timername = $myVDRtimers{$tid};
    Log3 $name, 5, "[$name] Parse: NextTimer: ID: $tid, name: $timername"; 

  }
  my $parsedmsg = "ID: ".sprintf("%2s",$tid)." | Day: ".sprintf("%3s",$tday).sprintf("%3s",$tdate).".".sprintf("%3s",$tmonth)." ".sprintf("%4i",$tyear)." | Start: ".$tstart." | Name: ".$timername;
  Log3 $name, 5, "[$name] Parse: NextTimer: $parsedmsg"; 

  #readingsSingleUpdate($hash, $reading, $parsedmsg, 1);
  #Log3 $name, 5, "[$name] Parse: updated $reading with $msg";
  return $parsedmsg;
}

sub SVDRP_getTimerNames {
  my ($name, $msg) = @_;
  
}
sub SVDRP_parseRecording {
   my ($name, $msg) = @_;
   my $type = "none";
   my $recinfo = AttrVal($name,"RecordingInfo","short");
   if ($recinfo eq "short") {
     #
     #T Löwengrube (Title)
     #S Tigerbande (Subtitle)
     #D August 1950 (Description)
     if (substr($msg, 0, 1) eq "T"){
       #$type = "Title:       ";       
       $type = "- ";       
     }
     elsif (substr($msg, 0, 1) eq "S"){
       #$type = "Subtitle:    ";       
       $type = "- ";       
     }
     elsif (substr($msg, 0, 1) eq "D"){
       #$type = "Description: ";       
       $type = "";
       # add newlines after next space after $lf characters
       #$msg = join ("\n",  ( $msg =~ /.{1,80}/gs ));
       #$msg =~ s/(.{39}[^\s]*)\s+/$1\n/;
       my $length = length($msg);
       my $lf = "70";
       my $i = "1";
       my $count;
       while ($length > 0){
         $count  = $i * $lf;
         $msg =~ s/(.{\Q$count\E}[^\h]*)\s+/$1\n/g;
         $length = $length - $lf;
         $i++;
       }              
     }
     else{
       return "none";
     }
     $msg = $type.(split / /, $msg, 2)[1];
   }
   return $msg;
}

sub SVDRP_parseHelpinfo {
  my ($name, $msg) = @_;
  Log3 $name, 5, "[$name] parseHelpinfo: parsed output is $msg";
  return $msg;
}


sub SVDRP_parsePlugin {
  my ($name, $msg) = @_;
  Log3 $name, 5, "[$name] parsePlugin: parsed output is $msg";
  return $msg;
}

sub SVDRP_Set {
  my ($hash, @param) = @_;
	
  return '"set SVDRP" needs at least one argument' if (int(@param) < 2);
	
  my $name = shift @param;
  my $opt = shift @param;
  my $value = join(" ", @param);
  #my $value = shift @param;
  my $msg;
  my $msg2;
  my $list = "";
  my $optorg = $opt;
  my $next;
  my $writecmd;
  
  $hash = $defs{$name};
  $hash->{version} = $version;

  # construct set list
  my @cList = (keys %SVDRP_sets);
  foreach my $key (@cList){
    $list = $list.$key.$SVDRP_sets{$key}." ";
  }
  if (!exists($SVDRP_sets{$opt})){
    return "Unknown argument $opt, please choose one of $list";
  }
  
  # return if device is disabled
  if(IsDisabled($name)){
    main::Log3 $name, 5, "[$name]: Set: $name is disabled by framework!";
    return;
  }
  # empty reading error
  readingsSingleUpdate($hash, "globalError", "", 1);
  readingsSingleUpdate($hash, "infoError", "", 1);
  # set .sendingcmd to indicate that cmd sending cycle is started
  # needed e.g. to empty HelpInfo as soon as new info is received
  readingsSingleUpdate($hash, ".sendingcmd", "1", 1);

  if ($opt eq "cleanUp"){
    main::Log3 $name, 5, "[$name]: Set: $name cleanUp";
    SVDRP_cleanUp($hash);
    return;
  }

  if ($opt eq "closeDev"){
    main::Log3 $name, 5, "[$name]: Set: $name closeDev";
    SVDRP_closeDev($hash);
    return;
  }

  if ($opt eq "connect"){
    main::Log3 $name, 5, "[$name]: Set: $name connect";
    DevIo_OpenDev($hash, 0, "SVDRP_Init", "SVDRP_Callback");
    return;
  }  
  
  # $opt is the nice name - read real command from SVDRP_cmdmap
  if (exists($SVDRP_cmdmap{$opt})){
    $opt = $SVDRP_cmdmap{$opt};
    main::Log3 $name, 5, "[$name]: Set: converted command to $opt";    
  }
  # STAT has only one option "disk"
  $value = "disk" if ($opt eq "STAT");
  
  if ($opt eq "PowerOff"){
    $opt = "HITK";
    $value = "Power";
  }

  if ($opt eq "LSTT"){
    # delete ListTimers, will be re-filled completely
    readingsSingleUpdate($hash, "ListTimers", "", 1);
    #$SVDRP_timers{$timerid} = $timername;
    %SVDRP_timers = ();
    main::Log3 $name, 5, "[$name]: Set: deleted ListTimers, value is now ".ReadingsVal($name,"ListTimers","none");
  }

  if ($opt eq "LSTR"){
    # delete Recordings, will be re-filled completely
    my $recid;
    if (!$value){
      $recid = "Recording ID: all";
    }
    else{
      $recid = "Recording ID: ".$value;
    }
    #main::Log3 $name, 5, "[$name]: Set: LastCmd is ".AttrVal($name,"LastCmd","unknown");
    #my $recid = "Recording ID: ".((split / /, AttrVal($name,"LastCmd","unknown"), 2)[1] || "all");
    readingsSingleUpdate($hash, "Recordings", $recid, 1);
    main::Log3 $name, 5, "[$name]: Set: deleted Recordings, value is now ".ReadingsVal($name,"Recordings","none");
  }

  if ($opt =~ /^PLUG/){
    readingsSingleUpdate($hash, "PluginInfo", "", 1);
    main::Log3 $name, 5, "[$name]: Set: $name PluginInfo";
  }

  # get or no value will sent send $msg to the given command $opt
  if ($value eq "get" || !$value){
    $msg = "$opt\r\n";
    $msg2 = $msg;
  }
  # construct command with value 
  else {
    $msg = "$opt $value\r\n";
    $msg2 = $opt."|".$value."\r\n";  
  }

  #delete $hash->{helper}{LastCmd};
  $hash->{STATE} = "query...";
  DevIo_OpenDev($hash, 1, "SVDRP_Init", "SVDRP_Callback");
  # Open connection returns welcome string like
  # "220 VDR SVDRP VideoDiskRecorder 2.0.6; Sun Feb  6 21:16:36 2022; UTF-8"
  # Read stores received data in $hash->{PARTIAL}

  my $delay = AttrVal( $name, "delay", "1" );
  # give VDR "delay" s to react before we send command
  #$writecmd = $name."|".$msg."|".$optorg;
  $next = gettimeofday() + $delay;
  if ($msg =~ /GetAll/){
    my $cmds = join (" ", @SVDRP_statusCmds);
    $writecmd = $name."|".$cmds."|".$optorg;
    InternalTimer( $next, "SVDRP_multiWrite", $writecmd);
  }
  elsif($msg =~ /NEXT/) {
    my $cmds = "LSTT NEXT";
    $writecmd = $name."|".$cmds."|".$optorg;
    InternalTimer( $next, "SVDRP_multiWrite", $writecmd);    
  }
  else{
    $writecmd = $name."|".$msg."|".$optorg;
    InternalTimer( $next, "SVDRP_singleWrite", $writecmd);
  }
  
  $msg =~ s/[\r\n]//g;
  readingsSingleUpdate($hash, "LastCmd", $msg, 1);
  
  # give VDR 1 s to react before we close connection
  $next = gettimeofday() + (2 * $delay);
  InternalTimer( $next, "SVDRP_cleanUp", $hash);    
  return;    
}

sub SVDRP_singleWrite {
  # write single command via DevIo
  my ($writecmd) = @_;
  my ( $name, $msg, $optorg  ) = split( "\\|", $writecmd );
  my $hash = $defs{$name};
  #$hash->{helper}{LastCmd} = $optorg;
  DevIo_SimpleWrite($hash, $msg, "2");
  main::Log3 $name, 5, "[$name]: singleWrite: sending $msg";
}

sub SVDRP_multiWrite {
  # write multiple commands via DevIo
  my ($writecmd) = @_;
  my ( $name, $msg, $optorg  ) = split( "\\|", $writecmd );
  my $hash = $defs{$name};
  my $send;
  main::Log3 $name, 5, "[$name]: multiWrite: will send: $msg";
  my @msgarr = split / /, $msg;
  #$hash->{helper}{LastCmd} = $optorg;
  foreach (@msgarr) {
    if ($_ eq "LSTT"){
      # delete ListTimers, will be re-filled completely
      readingsSingleUpdate($hash, "ListTimers", "", 1);
      %SVDRP_timers = ();
    }
    if ($_ eq "STAT"){
      $send = $_." disk\r\n"
    }
    else{
      $send = "$_\r\n";
    } 
    DevIo_SimpleWrite($hash, $send, "2");
    #main::Log3 $name, 5, "[$name]: multiWrite: sending $send";
  }  
}

sub SVDRP_checkConnection ($) {
  my ($hash) = @_;
  my $name = $hash->{NAME};
  
  RemoveInternalTimer($hash, "SVDRP_checkConnection");
  main::Log3 $name, 5,"[$name]: checkConnection: status timer removed";
  my $checkInterval = AttrVal( $name, "connectionCheck", "off" );
  
  if ($checkInterval eq "off"){
    return ;
  }
  
  # my $status = DevIo_IsOpen($hash); # would just tell if FD exists
  # let's try to reopen the connection. If successful, FD is kept or created.
  # if not successful, NEXT_OPEN is created.
  # $status is always undef, since callback fn is given
  my $status = DevIo_OpenDev($hash, 1, "SVDRP_ReInit", "SVDRP_Callback");
  
  #delete $hash->{NEXT_OPEN} if ( defined( $hash->{NEXT_OPEN} ) );
  #delete $hash->{helper}{nextConnectionCheck} if ( defined( $hash->{helper}{nextConnectionCheck} ) );

  if (!($hash->{FD}) && $hash->{NEXT_OPEN}) {
    # device was connected, but TCP timeout reached
    # DevIo tries to re-open after NEXT_OPEN
    # no internal timer needed
    delete $hash->{helper}{nextConnectionCheck}
      if ( defined( $hash->{helper}{nextConnectionCheck} ) );    
    main::Log3 $name, 3, "[$name]: DevIo_Open has no FD, NEXT_OPEN is $hash->{NEXT_OPEN}, no timer set";  
  }
  elsif (!($hash->{FD}) && !$hash->{NEXT_OPEN}){
    # not connected, DevIo not active, so device won't open again automatically
    # should never happen, since we called DevIo_Open above!
    # no internal timer needed, but should we ask DevIo again for opening the connection?
    #DevIo_OpenDev($hash, 1, "SVDRP_Init", "SVDRP_Callback");
    main::Log3 $name, 3, "[$name]: DevIo_Open has no FD, no NEXT_OPEN, should not happen!";
  }
  elsif ($hash->{FD} && $hash->{NEXT_OPEN}){
    # not connected - device was connected, but is not reachable currently
    # DevIo tries to connect again at NEXT_OPEN
    # should we try to clean up by closing and reopening?
    # no internal timer needed
    #DevIo_CloseDev($hash);
    #DevIo_OpenDev($hash, 1, "SVDRP_Init", "SVDRP_Callback");
    delete $hash->{helper}{nextConnectionCheck}
      if ( defined( $hash->{helper}{nextConnectionCheck} ) );
    main::Log3 $name, 3, "[$name]: DevIo_Open has FD and NEXT_OPEN, try to reconnect periodically";
  }
  elsif ($hash->{FD} && !$hash->{NEXT_OPEN}){
    # device is connected, or seems to be (since broken connection is not detected by DevIo!)
    # normal state when device is on and reachable
    # or when it was on, turned off, but DevIo did not recognize (TCP timeout not reached)
    # internal timer makes sense to check, if device is really reachable
    my $next = gettimeofday() + $checkInterval; # if checkInterval is off, we won't reach this line
    $hash->{helper}{nextConnectionCheck} = $next;
    InternalTimer( $next, "SVDRP_checkConnection", $hash);
    main::Log3 $name, 3, "[$name]: DevIo_Open has FD but no NEXT_OPEN, next timer set";
  }  
}

sub SVDRP_checkStatus ($){
  my ($hash) = @_;
  my $name = $hash->{NAME};
  
  my $checkInterval = AttrVal( $name, "statusCheckInterval", "off" );
  my $checkcmd = AttrVal( $name, "statusCheckCmd", "DiskStatus" );
  my $next;

  # add LF for better overview
  main::Log3 $name, 5, "\n";
  main::Log3 $name, 5,"[$name]: checkStatus: called with $checkcmd with interval $checkInterval";
  
  if ($checkInterval eq "off"){
    RemoveInternalTimer($hash, "SVDRP_checkStatus");
    main::Log3 $name, 5,"[$name]: checkStatus: status timer removed";
    return ;
  }
  else{
    my $value = "get";
    SVDRP_Set($hash, $name, $checkcmd, $value);
    $next = gettimeofday() + $checkInterval;
    $hash->{helper}{nextStatusCheck} = $next;
    InternalTimer( $next, "SVDRP_checkStatus", $hash);
    main::Log3 $name, 5,"[$name]: checkStatus: next status timer set";
  }  
}

sub SVDRP_restoreJson {
  my ($hash, $reading) = @_;
  my $name = $hash->{NAME};
  my $jsets = ReadingsVal($name, $reading, "{none:none}");
  my $decode = decode_json($jsets);
  # just for logging
  #my %decode = %$decode;
  #main::Log3 $name, 5, "[$name]: restore: ". keys(%decode);
  return %$decode;
}
###################################################
#                    end                          #
###################################################


1;

=pod
=item summary    control VDR by SVDRP via (W)Lan
=item summary_DE Steuerung von VDR mittels SVDRP über (W)Lan
=begin html

<a id="SVDRP"></a>
<h3>SVDRP</h3>

<ul>
  <i>SVDRP</i> implements SVDRP to control VDR via (W)Lan.
  <br><br>
  <a id="SVDRP-define"></a>
  <b>Define</b>
  <ul>
    <code>define &lt;name&gt; SVDRP &lt;IP_Address&gt [&lt;port&gt;]</code>
    <br>
    <br>70_SVDRP.pm provides basic control of your VDR.
    <br>Only a reasonable subset of SVDRP commands in implemented, since it e.g. does not make sense to set timers via fhem - vdradmin is a much more convenient GUI for that.
    <br><br>
    <ul>
      <li><b>IP_Address</b> - the IP Address of your VDR
      </li>
      <li><b>port</b> - ... guess? Yes, the port. If not given, VDR standard port 6419 is used.
      </li>
      <li>Example: <code>define VDRcontrol SVDRP 10.10.0.1 6419</code>
      </li>
    </ul>      
  </ul>
  <br>

  <a id="SVDRP-set"></a>
  <b>Set</b>
  <br>
  <ul>
    <br>Available <b>set</b> commands are taken from http://www.vdr-wiki.de/wiki/index.php/SVDRP.
    <br>For the predefined "raw" commands, "nice" names will be shown for the readings, e.g. <b>DiskStatus</b> instead of <b>STAT disk</b>.
    <br>Default set commands are
    <br><br>
    <a id="SVDRP-set-Channel"></a>
    <li>Channel
      <br>set value can be <i>"+"</i> or <i>"-"</i> or any channel number you want to switch to.
      <br><i>set &lt;name&gt; Channel</i> will get you the channel VDR is currently tuned to.
    </li>
    <br>
    <a id="SVDRP-set-DeleteTimer"></a>
    <li>DeleteTimer
      <br><i>set &lt;name&gt; DeleteTimer &lt;number&gt;</i> will delete ... hm, guess?
      <br>(you can get the timer numbers via <i>ListTimers</i>) 
    </li>
    <br>
    <a id="SVDRP-set-DiskStatus"></a>
    <li>DiskStatus
      <br>no value or <i>get</i> will display the current disk usage in <i>DiskStatus</i>
      <br>Additionally, the reading <i>DiskUsed</i> will be set to the disk fill level.
    </li>
    <br>
    <a id="SVDRP-set-GetAll"></a>
    <li>GetAll
      <br>no value or <i>get</i> will query several SVDRP settings:
      <br>"LSTT", "NEXT", "CHAN", "VOLU", "STAT"
      <br>(i.e. ListTimers, NextTimer, Channel, Volume, DiskStatus)
    </li>
    <br>
    <a id="SVDRP-set-Help"></a>
    <li>Help
      <br>gets the avaialble SVDRP commands from VDR and stores them in reading "HelpInfo" 
    </li>
    <a id="SVDRP-set-HitKey"></a>
    <li>HitKey
      <br>Enables you to send any Key defined by http://www.vdr-wiki.de/wiki/index.php/SVDRP
      <br>E.g.<i>set &lt;name&gt; HitKey Power</i> will cleanly power off VDR.
    </li>
    <br>
    <a id="SVDRP-set-ListRecording"></a>
    <li>ListRecording
      <br>set value should be an existing recording ID. Depending on the attribute <i>RecordingInfo</i> either all available info will be shown, or a reasonable subset.
      <br>If no value is given, all available recordings will be read and shown.
      <br>Attention: Depending on the number of number of recordings, this might take a while! fhem might show "timeout", and a screen refresh might be necessary. Use with care... 
    </li>
    <br>
    <a id="SVDRP-set-ListTimers"></a>
    <li>ListTimers
      <br>no value or <i>get</i> will query all timers from VDR.
      <br>raw answer from VDR will be parsed into a little bit nicer format. 
    </li>
    <br>
    <a id="SVDRP-set-NextTimer"></a>
    <li>NextTimer
      <br>no value or <i>get</i> will exactly get what it says.
      <br>(to get the timer name, ListTimers will be called before) 
    </li>
    <br>
    <a id="SVDRP-set-Plugin"></a>
    <li>Plugin
      <br>calls SVDRP with PLUG - you can enter any Plugin's SVDRP commands here to control the plugin
      <br>calling without parameter gets the list of avaialble plugins and stroes them in reading "HelpInfo"
      <br>I cannot test this with any plugin, but the Plugin's answer should go to the reading "PluginInfo" or "InfoError" (if Plugin gives an error message)
    </li>
    <br>
    <a id="SVDRP-set-PowerOff"></a>
    <li>PowerOff
      <br>A shortcut to cleanly power off VDR, same as <i>set &lt;name&gt; HitKey Power</i>
    </li>
    <br>
    <a id="SVDRP-set-SatIP"></a>
    <li>SatIP
      <br>send control commands to your SatIP plugin
    </li>
    <br>
    <a id="SVDRP-set-StreamdevServer"></a>
    <li>StreamdevServer
      <br>sends the corresponding SVDRP command to the streamdev-Plugin (LSTC,DISC)
    </li>
    <br>
    <a id="SVDRP-set-UpdateRecordings"></a>
    <li>UpdateRecordings
      <br>no value or <i>get</i> will trigger VDR to re-read the recordings.
      <br>(No output to fhem - no sense to show all recordings here)
    </li>
    <br>
    <a id="SVDRP-set-Volume"></a>
    <li>Volume
      <br>set value can be <i>"+"</i> or <i>"-"</i> or <i>mute</i> or any Volume (0-255) you want to set.
      <br><i>set &lt;name&gt; Volume</i> will get you VDR's current Volume setting.
    </li>
    <br>
    <a id="SVDRP-set-connect"></a>
    <li>connect
      <br>just connects to VDR, no further action.
      <br>Reading "info" will be updated.
      <br>Attention: As long as connection to VDR is open, no other SVDRP client can connect!
      <br>You might want to use "cleanup" to be able to reconnect other clients.
    </li>
    <br>
    <a id="SVDRP-set-cleanUp"></a>
    <li>cleanUp
      <br>closes connection to VDR, no further action.
      <br>Reading "info" will be updated.
    </li>
    <br>
    <a id="SVDRP-set-closeDev"></a>
    <li>closeDev
      <br>subset of cleanup. Just closes DevIo connection.
      <br>If you don't know what that means, you don't need it ;-)
    </li>
  </ul>
  <br>

  <a id="SVDRP-attr"></a>
  <b>Attributes</b>
  <br>
  <ul>
    <a id="SVDRP-attr-AdditionalSettings"></a>
    <li>AdditionalSettings
      <br><i>cmd1:val_1,...,val_n cmd2:val_1,...,val_n</i>
      <br>You can specify own set commands here, they will be added to the <b>set</b> list.
      <br>Multiple own sets can be specified, separated by a blank.
      <br>command and values are separated by <b>":"</b>, values are separated by <b>","</b>.
      <br>Example: <i>HITK:up,down,Power MESG</i>
    </li>
    <br>
    <a id="SVDRP-attr-RecordingInfo"></a>
    <li>RecordingInfo
      <br><i>short|long</i>
      <br>defines the amount of information shown on <i>ListRecording </i>
      <br><i>short</i> will display recording iD, title, subtitle, Description
      <br><i>long</i> will show all available information of the requested Recording
      <br>Default value is "short"
    </li>
    <br>
    <a id="SVDRP-attr-connectionCheck"></a>
    <li>connectionCheck
      <br><i>off|(value in seconds)</i>
      <br><i>value</i> defines the intervall in seconds to perform an connection check.
      <br>Normally you won't need that. Use at your own risk...
      <br>Default value is "off".
    </li>
    <br>            
    <a id="SVDRP-attr-statusCheckInterval"></a>
    <li>statusCheckInterval
      <br><i>off|(value in seconds)</i>
      <br><i>value</i> defines the interval in seconds to perform an status check.
      <br>Each <i>interval</i> the VDR is queried with the command defined by <i>statusCheckCmd</i> (default: DiskStatus).
      <br>Default value is off.
    </li>
    <br>
    <a id="SVDRP-attr-statusCheckCmd"></a>
    <li>statusCheckCmd
      <br><i>(any command(s) you set)</i>
      <br>Defines the command(s) used by statusCheckInterval.
    </li>
    <br>            
    <a id="SVDRP-attr-statusOfflineMsg"></a>
    <li>statusOfflineMsg
      <br><i>(any message text you set)</i>
      <br>Defines the message to set in the Reading related to <i>statusCheckCmd</i> when the device goes offline.
      <br>Status of device will be checked after each <i>statusCheckInterval</i> (default: off), querying the <i>statusCheckCmd</i> command (default: DiskStatus), and if STATE is <i>disconnected</i> the Reading of <i>statusCheckCmd</i> will be set to this message. Default: closed.
    </li>
    <br>
    <a id="SVDRP-attr-delay"></a>
    <li>delay
      <br><i>delay time in seconds</i>
      <br>Depending on the answering speed of your VDR, it might be necessary to grant a certain delay beween opening the connection (and getting the initial answer shown in reading "info"), sending a command, receiving the result and closing the connection.
      <br>Default: 1.
    </li>
    <br>
  </ul>
</ul>
=end html
=cut
