#!/usr/bin/perl

#BEGIN {
#    unshift @INC, ( $ENV{'POPHOME'} || '/usr/pop' ) . '/lib'
#        unless $ENV{'KUNDE_NO_PERLPATH'};
#}

use utf8;
use strict;
use warnings;

#use Modbus::Modbus qw(read_holding_register);
use Protocol::Modbus::PowerMod qw(modbus _MB_DEFAULT_PORT_ :FC :FUNC);
use Getopt::Long;

# Exit codes
use constant _OKAY_ => 0;
use constant _WARN_ => 1;
use constant _CRIT_ => 2;
use constant _UNKN_ => 3;

my (
    $message,
    $count,
    $scaleaddr,
    $scaletype
);

$message   = "";
$count     = 1;

my
(
     @scale,
     @last_val,
     @fuse_down,
     @fuse_up,
     @type,       # wird aber nicht ausgewertet
     @warning,    # wird aber nicht ausgewertet
     @critical,   # wird aber nicht ausgewertet
     @status
);

sub print_rcv_error_and_exit($$$)
{
    my ( $rcv_kind, $_count_, $_cnt_rcv_ ) = @_;
    print STDERR "UNKNOWN: Unterschied zwischen der Anzahl der geforderten ($_count_) und empfangenen ($_cnt_rcv_) Daten aus dem $rcv_kind-Register.\n";
    exit _UNKN_;
}

sub print_error($)
{
    my ( $_message_ ) = @_;

    print STDERR "UNKNOWN: Eine oder mehrere Optionen fehlerhaft.\n$_message_";
    exit _UNKN_;
}

sub del_double
{
    my %all=();
    @all{@_}=1;
    return (keys %all);
}

###################
# parsing options #
###################
GetOptions(
    'help|?'    => sub {exec perldoc => -F => $0 or die "Kann perldoc nicht ausfuehren: $!\n";},
    'H=s'       => \my $host,        # host address
    'u=i'       => \my $unit,        # slave id
    'n=i'       => \ $count,         # number of values
    'd=i'       => \my $dataaddr,    # start data address
    'dt=s'      => \my $datatype,    # the kind of register for data  address
    's=s'       => \ $scaleaddr,     # start scale address
    'st=s'      => \ $scaletype,     # kind of the register for scale address
    'vt=s'      => \my $valtype,     # counter or gauge
    'ii=s'      => \my $ii,          # ignore, if last value is lower than $ignore_if
    'pc=s'      => \my $path_conf,   # path to config file
    'pd=s'      => \my $path_data,   # path to data file
    'pf=s'      => \my $path_fuse,   # path to fuse file
    'p=i'       => \my $_port_,      # default port for modbusTCP
);


my $port = $_port_ || _MB_DEFAULT_PORT_;

####################
# checking options #
####################

$scaleaddr = 0
    if not defined $scaleaddr;

$scaletype = "FIX"
    if not defined $scaletype;

$ii = 0
    if not defined $ii;

my @ignore_if = ( $ii ) x ( $count );

$message .= " # ( Option -h  ): Unbekannter Host.\n"
    if not defined $host;

$message .= " # ( Option -u  ): Unbekannte {Slave,Unit}-ID.\n"
    if not defined $unit;

$message .= " # ( Option -d  ): Unbekannte {Daten,Start}-Adresse.\n"
    if not defined $dataaddr;

$message .= " # ( Option -dt ): Unbekannter Adressdatentyp.\n"
    if not defined $datatype;

$message .= " # ( Option -vt ): Unbekannter Wertetyp (counter|gauge).\n"
    if ( not defined $valtype or ($valtype ne "counter" and $valtype ne "gauge") );

$message .= " # ( Option -pd ): Unbekannter Pfad zum Speicherort der Messwertdatei.\n"
    if not defined $path_data;

$message .= " # ( Option -pf ): Unbekannter Pfad zum Speicherort der gefallenen Sicherungen.\n"
    if not defined $path_fuse;




print_error("$message")
    if ( $message ne "" );

my $file_data = "$path_data/$host-$unit";
my $file_fuse = "$path_fuse/$host-$unit";

my $file_conf = "$path_conf/$host-$unit"
    if defined $path_conf;


###################################
# get the source data from device #
###################################
my $connect;
$connect->{'proto'}  = "tcp";
$connect->{'host'}   = $host;
$connect->{'slave'}  = $unit;
$connect->{'port'}   = $port;

my $access;
$access->{'dataaddr'} = $dataaddr;
$access->{'datatype'} = $datatype;
$access->{'count'}    = $count;

my @src_data = modbus
(
    $connect,
    _MB_RHR_,
    $access
) or exit _UNKN_;

my $cnt_rcv = scalar(@src_data);

if ( $cnt_rcv != $count )
{
    print_rcv_error_and_exit("DATA", $count,$cnt_rcv);
}


open(IN, '<', "$file_data") or die "Konnte '$file_data' nicht zum Lesen öffnen.\n";

my $line_cnt=0;

while (<IN>)
{
    my $line = $_;
    chomp $line;

    $last_val[$line_cnt] = $line ;

    $line_cnt++;
}

close (IN);


my $cnt_last = scalar (@last_val);

if ( $cnt_last ne $cnt_rcv )
{
    print STDERR "UNKNOWN: Unterschied zwischen Anzahl der empfangenen ($cnt_rcv) und Anzahl der Werte aus der Überwachungsdatei '$file_data' ($cnt_last)";
    exit _UNKN_;
}


#############################################
# if the scale information stored by device #
#############################################
if ( defined $scaleaddr and defined $scaletype )
{

    if ( $scaletype eq "FIX" )
    {
        ( @scale ) = ( $scaleaddr ) x ( $count );
    }
    else
    {
        $connect->{'proto'}  = "tcp";
       
        $access->{'dataaddr'} = $scaleaddr;
        $access->{'datatype'} = $scaletype;
    
        @scale = modbus
        (
            $connect,
            _MB_RHR_,
            $access

        ) or exit _UNKN_;
 
        $cnt_rcv = scalar(@scale);
 
        if ( $cnt_rcv != $count )
        {
            print_rcv_error_and_exit("SCALE", $count, $cnt_rcv);
        }
    }
}



#############################
# read data and config file #
#############################

my @name;

@name = ("") x ( $count );

if ( defined $file_conf )
{

    open(IN, '<', "$file_conf") or die "Konnte '$file_conf' nicht zum Lesen öffnen.\n";


    my $line_id=0;
    my $line_cnt;

    while (<IN>)
    {
        $line_cnt = $line_id + 1;

        my $line = $_;
        chomp $line;

        if ( $line =~ /^#/ )
        {
            $status[$line_id] = "off";
            $line =~ s/^#[ ]*//g;
        }

        else
        {
            $status[$line_id] = "on";
        }

        my ( @optargs ) = split(/ +/,$line);

        foreach my $optarg (@optargs)
        {
            my ( $opt, $arg ) = split (/=/ , $optarg);

            if (defined $opt and defined $arg)
            {
                $arg =~ s/_/ /g;
                $scale[$line_id]    = $arg if ( $opt eq "s" );
                $name[$line_id]     = $arg if ( $opt eq "des" );
                $ignore_if[$line_id]  = $arg if ( $opt eq "ii" );
            }

        }
        $line_id++;
    }

    if ( $line_cnt ne $count )
    {
        print STDERR "Anzahl der Zeilen in Datei '$file_conf' ($line_cnt) stimmt nicht mir der Anzahl der geforderten Wert (-n $count) überein.\n";
        exit _UNKN_;
    }
}

close (IN);

################################
# search and store eval values #
################################

open(DATASTOR, '>', "$file_data") or die "Konnte '$file_data' nicht zum Schreiben öffnen.\n";

for( my $id = 0 ; $id < $count ; $id++ )
{

    $name[$id] = $id+1 . ". Sicherung"
        if ( $name[$id] eq "");

    my $tmp_val;

    ( $scale[$id] !~ /\./ ) ?
    ( $tmp_val = $src_data[$id]*(10**$scale[$id]) ):
    ( $tmp_val = $src_data[$id] * $scale[$id] );


    if ( $valtype eq "gauge" )
    {
        print DATASTOR "$tmp_val\n";
        push (@fuse_down , $name[$id])
            if ($tmp_val eq 0 and $last_val[$id] gt $ignore_if[$id] and $status[$id] eq "on");

        push (@fuse_up , $name[$id])
            if ( ( $tmp_val gt 0 and $last_val[$id] eq 0 ) or $status[$id] eq "off");
    }

    elsif ( $valtype eq "counter" )
    {
        my  @second_last_val = split (/;/, $last_val[$id]);
        print DATASTOR "$tmp_val;$second_last_val[0]\n";
        push (@fuse_down , $name[$id])
            if ( ( ($tmp_val - $second_last_val[0]) eq 0 and ( $second_last_val[1] - $second_last_val[0] ) gt $ignore_if[$id] ) and ( $status[$id] eq "on" ) );

        push (@fuse_up , $name[$id])
            if ( ( ( $tmp_val - $second_last_val[0]) gt 0 and ( $second_last_val[1] - $second_last_val[0] ) eq 0 ) or $status[$id] eq "off" );
    }

}
close (DATASTOR);

open (IN, "<", $file_fuse) or die "Konnte '$file_fuse' nicht zum Lesen öffnen.\n";


while (<IN>)
{
    my $line = $_;
    chomp $line;

    push (@fuse_down , $line);
}

close (IN);

@fuse_down = del_double(@fuse_down);

foreach my $fuse (@fuse_up)
{
    @fuse_down = grep !/$fuse/, @fuse_down;
}

my $fcnt  = scalar (@fuse_down);

if ( $fcnt > 0 )
{

    print "CRITICAL - Sicherungsfall:  " . join (", ", @fuse_down) . "\n";
    open (FUSE_DOWN, ">", $file_fuse) or die "Konnte '$file_fuse' nicht zum Schreiben öffnen.\n";

    foreach my $fuse (@fuse_down)
    {
        print FUSE_DOWN "$fuse\n";
    }
    
    exit _CRIT_;

}
elsif ( $fcnt eq 0 )
{

    open (FUSE_DOWN, ">", $file_fuse) or die "Konnte '$file_fuse' nicht zum Schreiben öffnen.\n";

    print FUSE_DOWN "";

}

print "OK - Keine gefallenen Sicherungen gefunden.\n";

exit _OKAY_;




__END__

=encoding utf8

=head1 NAME

    check_modbus - Das erste große ModbusTCP-Programm

=head1 BESCHREIBUNG

check_modbus ermittelt Daten via ModbusTCP aus Geräten und überprüft sie auf unterschiedliche Weise.

=head1 SYNOPSE

    check_modbus  -H HOST  -u UNIT_ID  -d DATA_ADDRESS  -dt DATA_TYPE 
                  -w WARNING_{VALUE|ADDRESS}   [-wt TYPE_OF_WARNING_VALUE  ] 
                  -c CRITICAL_{VALUE|ADDRESS}  [-ct TYPE_OF_CRITICAL_VALUE ]
                 [-s SCALE_{VALUE|ADDRESS}     -st TYPE_OF_SCALE_VALUE     ]
                 [-n NUMBER_OF_VALUE -i NAME_OF{INSTANCE|TYPE|DESCRIPTION} -f DATAFILE -disable {w|c} ]
                 [-help|?]

=head1 OPTIONEN

=over 4

=item B<-H HOST>

Hier wird die FQDN des Hosts angegeben.
(Beispiel: -H modbus-gateway.domain.com)

=item B<-u UNIT_ID>

Auch bekannt unter Slave-ID.
Das ist jenes Gerät, welches im BUS-System mit einer eindeutigen Nummer identifiziert wird.
Da die Größe der UNIT_ID sowohl in der Modbus-TCP, als auch im Modbus-RTU-Spezifikation ein Byte groß ist, sind auch nur Werte zwischen 0 und 255 möglich.
(Beispiel: -u 3)

=item B<-d DATA_ADDRESS>

In der Regel sind das die Adressen, welche man in den Bedienungsanleitungen der Hersteller findet.
Da die Größe der Adresse sowohl in der Modbus-TCP, als auch in der Modbus-RTU-Spezifikation 2 Byte groß ist, sind auch nur Werte zwischen 1 und 65536 (1 bis 2^16).
(Beispiel: -d 1000)

=item B<-dt DATA_TYPE>

Hier übergibt man dem Programm den Adress-Typ. Unterstützt werden:
  * int/uint
  * int32/uint32
  * float
  * quad

=item B<-w WARNING_{VALUE|ADDRESS}>

Für den Fall, dass das vom Gerät Schwellwerte ebenso abgerufen werden können, so ist es möglich diese hier anzugeben.
Wichtig! Die Option -wt muss dementsprechend gesetzt werden.
Beispiel: Ist der Schwellwert für WARNING im Register 2000 als Integer gespeichert, lautet die Angabe so:

    -w 2000 -wt int

Möchte man aber manuell einen festen Wert (Beispiel 15) angeben, lautet die Angabe so:

    -w 15 -wt FIX

... wobei -wt FIX in diesem Fall auch weggelassen werden kann.

=item B<-wt TYPE_OF_WARNING_VALUE>

Folgende Werte sind möglich:

  * FIX (Default)
  * int/uint
  * int32/unint32
  * float
  * quad

Im Falle der Eingabe von FIX, wird der angegebene Wert in -w als festen Bezugswert hergenommen.
In den anderen Fällen wird der Wert als Register angenommen und vom Gerät abgerufen.

=item B<-c CRITICAL_{VALUE|ADDRESS}>

Für den Fall, dass das vom Gerät mehrere Schwellwerte ebenso abgerufen werden können, so ist es möglich diese hier anzugeben.
Wichtig! Genau wie -wt muss auch hier die Option -ct dementsprechend gesetzt werden.
Beispiel: Ist der Schwellwert für CRITICAL im Register 3000 als Fließkomma-Zahl gespeichert, lautet die Angabe so:

    -c 3000 -ct float

Möchte man aber manuell einen festen Wert (Beispiel 20) angeben, lautet die Angabe so:

    -c 20 -ct FIX

... wobei -ct FIX in diesem Fall auch weggelassen werden kann.


=item B<-ct TYPE_OF_CRITICAL_VALUE>

Folgende Werte sind möglich:

  * FIX (Default)
  * int/uint
  * int32/unint32
  * float
  * quad

Im Falle der Eingabe von FIX, wird der angegebene Wert in -c als festen Bezugswert hergenommen.
In den anderen Fällen wird der Wert als Register angenommen und vom Gerät abgerufen.

=item -s B<SCALE_{VALUE|ADDRESS}>

Wie bei den Optionen -w und -c gibt es auch hier für die Skalierung die Möglichkeit sie im Gerät abrufen zu können.
In der Regel ist es so, dass die gespeicherte Skalierung im Gerät nicht ausschließlich mit den Daten verrechnet werden,
sondern dass die Skalierung auch für die im Gerät hinterlegten Schwellwerte gelten und verrechnet werden müssen.
Um das alles kümmert sich check_modbus ebenso.
Müssen die Messwerte mit dem Faktor 0,001 multipliziert werden, so lautet der Aufruf:

    -s -3 -st FIX
ODER
    -s 0.001 -st FIX

(-st FIX ist optional und ohne Angabe standardmäßig sowieso FIX)

Ist hingegen im Gerät die Skalierung im Register 5000 gespeichert, lautet der Aufruf folgender Maßen:

    -s 5000 -st int

In der Regel werden Faktoren als vorzeichenbehaftete Ganzzahlen in den jeweiligen Register gespeichert.
Ist das der Fall, wird der Datenwert folgender Maßen skaliert:

    WERT = SOURCE_DATEN * ( 10 ^ SKALIERUNG )

Steht im jeweiligen Register kein ganzzahliger vorzeichenbehafteter Wert, so kann er auch als Fließkomma-Wert
angegeben werden. Beispiel: Im Register 5000 steht eine Fließkomma-Zahl (z. B. 0,02) so ist folgende Angabe notwendig:

    -s 5000 -st float

Damit wird der Wert nicht mit einer Skalierung, sondern mit dem Faktor direkt verrechnet:

    WERT = SOURCE_DATEN * FAKTOR

=item B<-st TYPE_OF_SCALE_VALUE>

Folgende Werte sind möglich:

  * FIX (Default)
  * int/uint
  * int32/unint32
  * float
  * quad


=item B<-n NUMBER_OF_VALUE>

Anzahl der zu ermittelnden Datensätzen.
(Default 1)

=item B<-i NAME_OF{INSTANCE|TYPE|DESCRIPTION}>

Eine reine Angabe um was es für ein Messtyp es sich handelt.
Diese erscheint im Falle einer CRITICAL- oder WARNING-Meldung.
Soll als Maßeinheit dienen (bsp. kWh, A, V, Hz, °C, etc.).


=item B<-f DATAFILE>

Das ist jetzt sehr "Advanced"!
Falls das alles NUR gut und schön ist und man aber trotzdem individuelle CRITICAL- WARNING- und Text-Meldungen haben möchte,
dann ist es einem möglich, diese in einer individuellen Datei zu hinterlegen.

Dabei ist auf 2 Dinge zu achten:

  1) Die Anzahl der Zeilen MUSS mit der Anzahl der mit -n übergbenen Werte überein stimmen
  2) Der Syntax der Datei muss folgender Maßen lauten:

  +---------------------------------------------------+
  | n$NAME w$WARNING c$CRITICAL d$DESCRIPTION s$SCALE |
  +---------------------------------------------------+

Beispiel:

           +---------------------------+
 Zeile1:   | nF10   w13  c14  dA   s-2 |
 Zeile2:   | nF11   w28  c30  dA   s-4 |
 Zeile3:   | nF12   w13  c    dA   s   |
 Zeile4:   | nT1    w27  c35  d°C  s-1 |
           | [...]                     |
           +---------------------------+

Erläuterung:
Der Name der ersten Messstelle lautet "F10", dieser wird mit dem vorstehenden Option "n" eingeleitet.
Der nächste Wert ist der Schwellwert für WARNING, der auf "13" steht und mit der Option "w" eingeleitet wird.
Der nächste Wert ist der Schwellwert für CRITICAL, der auf "14" steht und mir der Option "c" eingeleitet wird.
Der nächste Wert ist der Messtyp, der auf "A" steht und mit einem "d" eingeleitet wird.
Zu guter letzt kommt die Saklierung, welche auf "-2" steht und mit der Option "s" eingeleitet wird.

Zwischen den Optionen (n, w, c, d oder s) und ihren Werten darf KEIN Leerzeichen stehen!
Zwischen den einzelnen Angaben können so viele Leerzeichen stehen wie es einem lieb ist.
In JEDER Zeile müssen IMMER alle 5 Optionen stehen (die Reihenfolge wiederum ist egal).
Ist kein Wert erwünscht, kann er einfach weggelassen werden (siehe Zeile 3 bei Optioen "c" und "s").
Ist kein Argument, bzw. Wert nach einer Option angegeben, wo gelten die in der Kommandozeile angegebenen Optionen.

Beispiel:

Nehmen wir an, folgende Optionen wurden in der Kommandozeile übergeben:

    check_modbus [...] -w 10 -wt FIX -c 20 -ct FIX -s 1000 -st int -f /home/stoni/werte_datei

... und in der Werte-Datei stehen die oben genannten Werte,
dann werden alle in der Kommandozeile übergebenen Werte mit den Werten in der Datei /home/stoni/werte_datei überschrieben.
Außer die Werte, welche in der Datei nicht angegeben wurde, das wär hier nur für die 3. Zeile folgende Werte:

    *  CRITICAL-Wert (-c 20) 
    *  Skalier-Wert  (Register 1002) 

Für die 1. Zeile:
 WARNING     ab Wert "10" - wird aber durch die Datei mit dem Wert "13" ersetzt.
 CRITICAL    ab Wert "20" - wird aber durch die Datei mit dem Wert "14" ersetzt.
 DESCRIPTION wurde nicht auf der Kommandozeile angegeben, wird aber durch die Datei mit dem Wert "A" ersetzt.
 SCALIERUNG  Müsste aus dem Integer-Register "1000" abgerufen werden, wird aber durch den Wert "-2" ersetzt.

Für die 2. Zeile:
 WARNING     ab Wert "10" - wird aber durch die Datei mit dem Wert "28" ersetzt.
 CRITICAL    ab Wert "20" - wird aber durch die Datei mit dem Wert "30" ersetzt.
 DESCRIPTION wurde nicht auf der Kommandozeile angegeben, wird aber durch die Datei mit dem Wert "A" ersetzt.
 SCALIERUNG  Müsste aus dem Integer-Register "1001" abgerufen werden, wird aber durch den Wert "-4" ersetzt.

Für die 3. Zeile:
 WARNING     ab Wert "10" - wird aber durch die Datei mit dem Wert "13" ersetzt.
 CRITICAL    ab Wert "20" - wird NICHT durch die Datei ersetzt, da kein Argument eingetragen ist.
 DESCRIPTION wurde nicht auf der Kommandozeile angegeben, wird aber durch die Datei mit dem Wert "A" ersetzt.
 SCALIERUNG  Wird aus dem Integer-Register "1002" abgerufen, da die Datei kein Argument für die Skalierung hat.

Für die 4. Zeile:
 WARNING     ab Wert "10" - wird aber durch die Datei mit dem Wert "27" ersetzt.
 CRITICAL    ab Wert "20" - wird aber durch die Datei mit dem Wert "35" ersetzt.
 DESCRIPTION wurde nicht auf der Kommandozeile angegeben, wird aber durch die Datei mit dem Wert "°C" ersetzt.
 SCALIERUNG  Müsste aus dem Integer-Register "1003" abgerufen werden, wird aber durch den Wert -1 ersetzt.

=item B<-disable {w|c}>

Hier kann man Festlegen, ob man die CRITICAL-Meldungen deaktivieren möchte (dann fallen alle CRITICAL-Werte unter den WARNINGS),
oder ob man die WARNING-Meldungen deaktivieren möchte (dann werden NUR CRITICAL-Werte ausgegeben, die WARNINGS werden dann als OK eingestuft).

=item B<-help|?>

um (nur) diese Dokumentation anzeigen zu lassen

=back

Stefan Steiner (Stoni)
für die noris network AG
