##################################################################################### # $Id$ # # Usage # # define <name> 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 <name> 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 = <FH>) { $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 <a name="FileLogConvert"></a> <h3>FileLogConvert</h3> <ul> With <i>FileLogConvert</i> you can convert existing FileLog(s) to a csv or sql file or import directly to your DbLog.<br> 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.<br> While converting/importing, only lines matching the event log string (TIMESTAMP,DEVICE,EVENT) will be converted/imported. All other lines will be ignored.<br> <i>FileLogConvert</i> is trying to get the TYPE of each device from your current installation and will write this to csv/sql/DbLog.<br> <i>FileLogConvert</i> is also trying to find a unit for each events value and will also write this to csv/sql/DbLog. <br> <br> <a name="FileLogConvert_define"></a> <p><b>Define</b></p> <ul> <code>define <name> FileLogConvert [DbLog-device]</code><br> </ul> <br> Example for defining FileLogConvert: <br><br> <ul> <code>define LogConvert FileLogConvert</code><br> </ul> <br> If only one DbLog device is available, you don't have to provide its name.</br> If more than one DbLog device is available, you must provide its name.</br> <br> <ul> <code>define LogConvert FileLogConvert DbLog</code><br> </ul> <br> <a name="FileLogConvert_get"></a> <p><b>get <required></b></p> <ul> <li> <i>fileEvents <NAME-FileLog-FILE></i><br> analyse all lines of the log file and check for available events<br> available events will be listed space separated in reading events<br> proposals for possible set commands can be found in reading cmd<br> amount of analysed lines can be found in reading lines-analysed<br> last analysed file can be found in reading file-analysed<br> if a file is converted in all formats and imported into DbLog it will be hidden from the dropdown list </li> </ul> <br> <a name="FileLogConvert_set"></a> <p><b>set <required> [optional]</b></p> <ul> <li> <i>convert2csv <NAME-FileLog-FILE> [REGEX-OF-EVENTS-TO-APPLY-TO-CSV]</i><br> convert given FileLog file to csv file<br> if you specify a regex of events, then only these events will be applied to the resulting csv file<br> name of resulting file can be found in reading destination<br> amount of converted lines can be found in reading lines-converted </li> <li> <i>convert2sql <NAME-FileLog-FILE> [REGEX-OF-EVENTS-TO-APPLY-TO-SQL]</i><br> convert given FileLog file to sql file<br> if you specify a regex of events, then only these events will be applied to the resulting sql file<br> name of resulting file can be found in reading destination<br> amount of converted lines can be found in reading lines-converted </li> <li> <i>import2DbLog <NAME-FileLog-FILE> [REGEX-OF-EVENTS-TO-APPLY-TO-DbLog]</i><br> import given FileLog file to DbLog<br> if you specify a regex of events, then only these events will be applied to DbLog<br> name of DbLog device can be found in reading destination<br> amount of converted lines can be found in reading lines-converted </li> </ul> <br> <a name="FileLogConvert_attr"></a> <p><b>Attributes</b></p> <ul> <li> <b><i>logdir</i></b><br> alternative log directory<br> must be a valid (relative) subfolder of "./fhem/"<br> relative paths are also possible to address paths outside "./fhem/", e.g. "../../home/pi/oldlogs"<br> default: log </li> </ul> <br> <a name="FileLogConvert_read"></a> <p><b>Readings</b></p> <ul> <li> <i>cmd</i><br> proposal for possible convert/import command created by fileEvents </li> <li> <i>destination</i><br> last destination of the conversation<br> when importing to DbLog, the name of the DbLog device can be found here<br> when converting a FileLog to csv or sql, the created file name can be found here </li> <li> <i>events</i><br> space separated list of events found by fileEvents </li> <li> <i>file-analysed</i><br> name of the last analysed FileLog file </li> <li> <i>file-source</i><br> name of the last source file </li> <li> <i>lines-analysed</i><br> amount of lines analysed by fileEvents </li> <li> <i>lines-converted</i><br> amount of lines converted by convert2csv and convert2sql </li> <li> <i>lines-imported</i><br> amount of lines imported by import2DbLog </li> <li> <i>regex</i><br> last used regex </li> <li> <i>state</i><br> current state or success or error </li> </ul> </ul> =end html =cut