##################################################################################### # $Id$ # # Usage # # define FileLogConvert [DbLog-device] # ##################################################################################### package main; use strict; use warnings; use Blocking; sub FileLogConvert_Initialize($) { my ($hash) = @_; $hash->{AttrFn} = "FileLogConvert_Attr"; $hash->{DefFn} = "FileLogConvert_Define"; $hash->{GetFn} = "FileLogConvert_Get"; $hash->{SetFn} = "FileLogConvert_Set"; $hash->{UndefFn} = "FileLogConvert_Undef"; $hash->{AttrList} = "logdir $readingFnAttributes"; } sub FileLogConvert_Define($$) { my ($hash,$def) = @_; my @args = split " ",$def; return "Usage: define FileLogConvert [DbLog-device]" if (@args < 2 || @args > 3); my ($name,$type,$logdev) = @args; if (!$logdev) { my @logdevs; foreach my $dev (devspec2array("TYPE=DbLog")) { push @logdevs,$dev; } if (@logdevs == 1) { return "$logdevs[0] doesn't exists" if (!IsDevice($logdevs[0])); $hash->{DEF} = $logdevs[0]; $logdev = $logdevs[0]; } elsif (@logdevs > 1) { return "Found too many available DbLog devives! Please specify the DbLog device you want! Available DbLog devices: ".join(",",@logdevs); } else { return "No DbLog device found! Please define a DbLog device first!"; } } else { return "$logdev doesn't exists!" if (!IsDevice($logdev)); return "$logdev is not a valid DbLog device!" if ($defs{$logdev}->{TYPE} ne "DbLog"); } Log3 $name,3,"FileLogConvert ($name) - defined with DbLog $logdev" if ($init_done); readingsBeginUpdate($hash); readingsBulkUpdate($hash,"state","Initialized"); readingsBulkUpdate($hash,"cmd","") if (!defined ReadingsVal($name,"cmd",undef)); readingsBulkUpdate($hash,"destination","") if (!defined ReadingsVal($name,"destination",undef)); readingsBulkUpdate($hash,"events","") if (!defined ReadingsVal($name,"events",undef)); readingsBulkUpdate($hash,"file-analysed","") if (!defined ReadingsVal($name,"file-analysed",undef)); readingsBulkUpdate($hash,"file-source","") if (!defined ReadingsVal($name,"file-source",undef)); readingsBulkUpdate($hash,"lines-analysed","") if (!defined ReadingsVal($name,"lines-analysed",undef)); readingsBulkUpdate($hash,"lines-converted","") if (!defined ReadingsVal($name,"lines-converted",undef)); readingsBulkUpdate($hash,"lines-imported","") if (!defined ReadingsVal($name,"lines-imported",undef)); readingsBulkUpdate($hash,"regex","") if (!defined ReadingsVal($name,"regex",undef)); readingsEndUpdate($hash,0); return; } sub FileLogConvert_Undef($$) { my ($hash,$arg) = @_; my $name = $hash->{NAME}; BlockingKill($hash->{helper}{RUNNING_PID}) if ($hash->{helper}{RUNNING_PID}); Log3 $name,3,"FileLogConvert ($name) - deleted"; return; } sub FileLogConvert_Get($@) { my ($hash,$name,@aa) = @_; my ($cmd,@args) = @aa; my $source = $args[0] ? $args[0] : ""; my $dir = "$attr{global}{modpath}/".AttrVal($name,"logdir","log"); my $files = FileLogConvert_Dir($hash); my @logs = @{$files->{logs}}; my @flogs = @{$files->{log}}; my $params = "fileEvents:".join(",",sort @flogs) if (@flogs); if ($cmd eq "fileEvents") { return "You have to provide a source file" if (!$source); return "Work already in progress... Please wait for the current process to finish." if ($hash->{helper}{RUNNING_PID} && !$hash->{helper}{RUNNING_PID}{terminated}); return "$source is not an availabe log file!" if (!grep(/^$source$/,@logs)); readingsSingleUpdate($hash,"state","analysing $source",1); } else { return "Unknown argument $cmd for $name, choose one of $params" if ($params); return; } my %helper = ( "cmd" => "$cmd", "dir" => "$dir", "source" => "$source" ); $hash->{helper}{filedata} = \%helper; $hash->{helper}{RUNNING_PID} = BlockingCall("FileLogConvert_FileRead","$name","FileLogConvert_FileRead_finished"); return; } sub FileLogConvert_Set($@) { my ($hash,$name,@aa) = @_; my ($cmd,@args) = @aa; my $source = $args[0] ? $args[0] : ""; my $eventregex = $args[1] ? $args[1] : ""; my $dir = "$attr{global}{modpath}/".AttrVal($name,"logdir","log"); my $files = FileLogConvert_Dir($hash); my @logs = @{$files->{logs}}; my @flogs = @{$files->{log}}; my @fcsvs = @{$files->{csv}}; my @fsqls = @{$files->{sql}}; my @fdns = @{$files->{db}}; my @paras; push @paras,"convert2csv" if (@fcsvs); push @paras,"convert2sql" if (@fsqls); push @paras,"import2DbLog" if (@fdns); my $para = join(" ",@paras) if (@paras); if ($cmd =~ /^(fileEvents|convert2(csv|sql)|import2DbLog)$/) { return "You have to provide a source file" if (!$source); return "Work already in progress... Please wait for the current process to finish." if ($hash->{helper}{RUNNING_PID} && !$hash->{helper}{RUNNING_PID}{terminated}); return "$source is not an availabe log file!" if (!grep(/^$source$/,@logs)); my $ofile = $source; $ofile =~ s/log$//; $ofile .= "csv" if ($cmd eq "convert2csv"); $ofile .= "sql" if ($cmd eq "convert2sql"); $ofile .= $hash->{DEF} if ($cmd eq "import2DbLog"); my $f = $ofile; $f = "Dblog $hash->{DEF}"; my $acon = "$source has already been converted into $f"; $acon =~ s/convert/import/ if ($cmd eq "import2DbLog"); return $acon if ($cmd eq "convert2csv" && @fcsvs && !grep(/^$source$/,@fcsvs)); return $acon if ($cmd eq "convert2sql" && @fsqls && !grep(/^$source$/,@fsqls)); return $acon if ($cmd eq "import2DbLog" && @fdns && !grep(/^$source$/,@fdns)); my $state = $cmd =~ /^convert2(csv|sql)$/ ? "converting" : $cmd eq "fileEvents" ? "analysing" : "importing"; my $mstate = "$state $source"; $mstate .= " $eventregex" if ($eventregex); readingsSingleUpdate($hash,"state",$mstate,1); } else { return "$cmd is not a valid command for $name, please choose one of $para" if ($para); return; } my %helper = ( "cmd" => "$cmd", "dir" => "$dir", "source" => "$source", "dblog" => "$hash->{DEF}", "eventregex" => "$eventregex" ); $hash->{helper}{filedata} = \%helper; $hash->{helper}{RUNNING_PID} = BlockingCall("FileLogConvert_FileRead","$name","FileLogConvert_FileRead_finished"); return; } sub FileLogConvert_FileRead($) { my ($string) = @_; return unless (defined $string); my @a = split /\|/,$string; my $name = $a[0]; my $hash = $defs{$name}; my $cmd = $hash->{helper}{filedata}{cmd}; my $dir = $hash->{helper}{filedata}{dir}; my $source = $hash->{helper}{filedata}{source}; my $dblog = $hash->{helper}{filedata}{dblog}; my $eventregex = $hash->{helper}{filedata}{eventregex} ? $hash->{helper}{filedata}{eventregex} : ".*"; my $stateregex = $hash->{helper}{filedata}{stateregex} ? $hash->{helper}{filedata}{stateregex} : ".*"; $stateregex =~ s/:/|/g; delete $hash->{helper}{filedata}; my $fname = "$dir/$source"; my @events; if (!open FH,$fname) { close(FH); my $err = encode_base64("could not read $fname",""); return "$name|''|$err"; } my $arows = 0; my $crows = 0; my $dest = $source; $dest =~ s/log$/csv/ if ($cmd eq "convert2csv"); $dest =~ s/log$/sql/ if ($cmd eq "convert2sql"); $dest =~ s/log$/$dblog/ if ($cmd eq "import2DbLog"); if ($cmd =~ /^(convert2(csv|sql)|import2DbLog)$/) { if (!open WH,">>$dir/$dest") { close WH; my $err = encode_base64("could not write $dir/$dest",""); return "$name|''|$err"; } } while (my $line = ) { $arows++; chomp $line; $line =~ s/\s{2,}/ /g; if ($cmd eq "fileEvents") { next unless ($line =~ /^(\d{4}-\d{2}-\d{2})_(\d{2}:\d{2}:\d{2})\s([A-Za-z0-9\.\-_]+)\s([A-Za-z0-9\.\-_]+):\s(\S+)(\s.*)?$/ || $line =~ /^(\d{4}-\d{2}-\d{2})_(\d{2}:\d{2}:\d{2})\s([A-Za-z0-9\.\-_]+)\s([A-Za-z0-9\.\-_]+)$/); push @events,$4 if (!grep(/^$4$/,@events)); } else { my $i_date; my $i_time; my $i_device; my $i_type; my $i_reading; my $i_event; my $i_value; my $i_unit = ""; if ($line =~ /^(\d{4}-\d{2}-\d{2})_(\d{2}:\d{2}:\d{2})\s([A-Za-z0-9\.\-_]+)\s([A-Za-z0-9\.\-_]+):\s(\S+)(\s.*)?$/) { $i_date = $1; $i_time = $2; $i_device = $3; $i_reading = $4; $i_value = $5; my $rest = $6 if ($6); next if ($i_reading !~ /^($eventregex)$/); $i_unit = $rest ? (split " ",$rest)[0] : ""; $i_unit = "" if ($i_unit =~ /^[\/\[\{\(]/); $i_type = IsDevice($i_device) ? uc $defs{$i_device}->{TYPE} : ""; $i_event = "$i_reading: $i_value"; $i_event .= " $rest" if ($rest); } elsif ($line =~ /^(\d{4}-\d{2}-\d{2})_(\d{2}:\d{2}:\d{2})\s([A-Za-z0-9\.\-_]+)\s([A-Za-z0-9\.\-_]+)$/) { $i_date = $1; $i_time = $2; $i_device = $3; $i_value = $4; next if ($i_value !~ /^($eventregex)$/); $i_type = IsDevice($i_device) ? uc $defs{$i_device}->{TYPE} : ""; $i_reading = "state"; $i_event = $i_value; } else { next; } $crows++; my $ret; $ret = "INSERT INTO history (TIMESTAMP,DEVICE,TYPE,EVENT,READING,VALUE,UNIT) VALUES ('$i_date $i_time','$i_device','$i_type','$i_event','$i_reading','$i_value','$i_unit');" if ($cmd =~ /^(import2DbLog|convert2sql)$/); $ret = '"'.$i_date.' '.$i_time.'","'.$i_device.'","'.$i_type.'","'.$i_event.'","'.$i_reading.'","'.$i_value.'","'.$i_unit.'"' if ($cmd eq "convert2csv"); DbLog_ExecSQL($defs{$dblog},$ret) if ($cmd eq "import2DbLog"); print WH $ret,"\n" if ($cmd =~ /^convert2(csv|sql)$/); } } close WH if ($cmd =~ /^(convert2(csv|sql)|import2DbLog)$/); close FH; my $events = @events ? encode_base64(join(" ",@events),"") : ""; my $regex = $eventregex ? encode_base64($eventregex,"") : ""; return "$name|$cmd,$source,$arows,$dest,$crows,$events,$regex"; } sub FileLogConvert_FileRead_finished($) { my ($string) = @_; my @a = split /\|/,$string; my $name = $a[0]; my $hash = $defs{$name}; delete $hash->{helper}{RUNNING_PID}; my @data = split /,/,$a[1]; my $cmd = $data[0]; my $source = $data[1]; my $arows = $data[2]; my $dest = $data[3]; my $crows = $data[4]; my @events = $data[5] ? sort(split " ",decode_base64($data[5])) : undef; my $regex = $data[6] ? decode_base64($data[6]) : ""; my $err = $a[2] ? decode_base64($a[2]) : ""; readingsBeginUpdate($hash); if ($err) { readingsBulkUpdate($hash,"state",$err); } else { if ($cmd eq "fileEvents") { my $events = @events ? join(" ",@events) : ""; readingsBulkUpdate($hash,"state","analysis done"); readingsBulkUpdate($hash,"file-analysed",$source); readingsBulkUpdate($hash,"lines-analysed",$arows); readingsBulkUpdate($hash,"events",$events); $events =~ s/\s/|/g; readingsBulkUpdate($hash,"cmd","$source $events"); } elsif ($cmd =~ /^convert2(csv|sql)$/) { readingsBulkUpdate($hash,"state","convert done"); readingsBulkUpdate($hash,"file-source",$source); readingsBulkUpdate($hash,"destination",$dest); readingsBulkUpdate($hash,"lines-analysed",$arows); readingsBulkUpdate($hash,"lines-converted",$crows); readingsBulkUpdate($hash,"regex",$regex); } elsif ($cmd eq "import2DbLog") { readingsBulkUpdate($hash,"state","import done"); readingsBulkUpdate($hash,"file-source",$source); readingsBulkUpdate($hash,"destination",$hash->{DEF}); readingsBulkUpdate($hash,"lines-analysed",$arows); readingsBulkUpdate($hash,"lines-imported",$crows); readingsBulkUpdate($hash,"regex",$regex); } } readingsEndUpdate($hash,1); return undef; } sub FileLogConvert_Attr(@) { my ($cmd,$name,$attr_name,$attr_value) = @_; my $hash = $defs{$name}; if ($cmd eq "set") { if ($attr_name eq "logdir" && $init_done) { if (!opendir(DIR,"$attr{global}{modpath}/$attr_value")) { close DIR; return "Folder $attr_value is not available, please choose another one."; } close DIR; Log3 $name,3,"FileLogConvert ($name) - attribute $attr_name added with value $attr_value" if ($init_done); } } return; } sub FileLogConvert_Dir($) { my ($hash) = @_; my $name = $hash->{NAME}; my $dir = "$attr{global}{modpath}/".AttrVal($name,"logdir","log"); my @logs; my @csvs; my @sqls; my @dones; opendir(DIR,$dir); while (my $file = readdir(DIR)) { next unless (-f "$dir/$file"); next unless ($file =~ /\.(log|csv|sql|$hash->{DEF})$/); next if ($file =~ /^fhem-/); push @logs,$file if ($file =~ /log$/); push @csvs,$file if ($file =~ /csv$/); push @sqls,$file if ($file =~ /sql$/); push @dones,$file if ($file =~ /$hash->{DEF}$/); } closedir(DIR); $hash->{files_csv} = scalar @csvs; $hash->{files_sql} = scalar @sqls; $hash->{files_log} = scalar @logs; $hash->{files_db} = scalar @dones; my @flogs; my @fcsvs; my @fsqls; my @fdns; foreach my $f (@logs) { my $fname = $f; $fname =~ s/\.log$//; push @flogs,$f if (!grep(/^$fname.csv$/,@csvs) || !grep(/^$fname.sql$/,@sqls) || !grep(/^$fname.$hash->{DEF}$/,@dones)); push @fcsvs,$f if (!grep(/^$fname.csv$/,@csvs)); push @fsqls,$f if (!grep(/^$fname.sql$/,@sqls)); push @fdns,$f if (!grep(/^$fname.$hash->{DEF}$/,@dones)); } my %files = ( "logs" => \@logs, "log" => \@flogs, "csv" => \@fcsvs, "sql" => \@fsqls, "db" => \@fdns ); return \%files; } 1; =pod =item helper =item summary convert FileLogs to csv or sql file or import directly to DbLog =item summary_DE konvertiert FileLogs zu csv oder sql Datei oder importiert direkt in DbLog =begin html

FileLogConvert

    With FileLogConvert you can convert existing FileLog(s) to a csv or sql file or import directly to your DbLog.
    Before converting/importing you can/should analyse the FileLog file(s) for available events. Then you can specify only your prefered events to convert/import.
    While converting/importing, only lines matching the event log string (TIMESTAMP,DEVICE,EVENT) will be converted/imported. All other lines will be ignored.
    FileLogConvert is trying to get the TYPE of each device from your current installation and will write this to csv/sql/DbLog.
    FileLogConvert is also trying to find a unit for each events value and will also write this to csv/sql/DbLog.

    Define

      define <name> FileLogConvert [DbLog-device]

    Example for defining FileLogConvert:

      define LogConvert FileLogConvert

    If only one DbLog device is available, you don't have to provide its name.
    If more than one DbLog device is available, you must provide its name.

      define LogConvert FileLogConvert DbLog

    get <required>

    • fileEvents <NAME-FileLog-FILE>
      analyse all lines of the log file and check for available events
      available events will be listed space separated in reading events
      proposals for possible set commands can be found in reading cmd
      amount of analysed lines can be found in reading lines-analysed
      last analysed file can be found in reading file-analysed
      if a file is converted in all formats and imported into DbLog it will be hidden from the dropdown list

    set <required> [optional]

    • convert2csv <NAME-FileLog-FILE> [REGEX-OF-EVENTS-TO-APPLY-TO-CSV]
      convert given FileLog file to csv file
      if you specify a regex of events, then only these events will be applied to the resulting csv file
      name of resulting file can be found in reading destination
      amount of converted lines can be found in reading lines-converted
    • convert2sql <NAME-FileLog-FILE> [REGEX-OF-EVENTS-TO-APPLY-TO-SQL]
      convert given FileLog file to sql file
      if you specify a regex of events, then only these events will be applied to the resulting sql file
      name of resulting file can be found in reading destination
      amount of converted lines can be found in reading lines-converted
    • import2DbLog <NAME-FileLog-FILE> [REGEX-OF-EVENTS-TO-APPLY-TO-DbLog]
      import given FileLog file to DbLog
      if you specify a regex of events, then only these events will be applied to DbLog
      name of DbLog device can be found in reading destination
      amount of converted lines can be found in reading lines-converted

    Attributes

    • logdir
      alternative log directory
      must be a valid (relative) subfolder of "./fhem/"
      relative paths are also possible to address paths outside "./fhem/", e.g. "../../home/pi/oldlogs"
      default: log

    Readings

    • cmd
      proposal for possible convert/import command created by fileEvents
    • destination
      last destination of the conversation
      when importing to DbLog, the name of the DbLog device can be found here
      when converting a FileLog to csv or sql, the created file name can be found here
    • events
      space separated list of events found by fileEvents
    • file-analysed
      name of the last analysed FileLog file
    • file-source
      name of the last source file
    • lines-analysed
      amount of lines analysed by fileEvents
    • lines-converted
      amount of lines converted by convert2csv and convert2sql
    • lines-imported
      amount of lines imported by import2DbLog
    • regex
      last used regex
    • state
      current state or success or error
=end html =cut