# $Id$ =for comment (License) ############################################################################## # # configDB.pm # # A fhem library to enable configuration from sql database # instead of plain text file, e.g. fhem.cfg # # READ COMMANDREF DOCUMENTATION FOR CORRECT USE! # # Copyright: betateilchen ® # # This file is part of fhem. # # Fhem 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. # # Fhem 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. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # ############################################################################## =cut =for comment (changelog before 2022) # # ChangeLog # # 2014-03-01 - SVN 5080 - initial release of interface inside fhem.pl # - initial release of configDB.pm # # 2014-03-02 - added template files for sqlite in contrib/configDB # - updated commandref (EN) documentation # - added commandref (DE) documentation # # 2014-03-03 - changed performance optimized by using version uuid table # - updated commandref docu for migration # - added cfgDB_svnId for fhem.pl CommandVersion # - added cfgDB_List to show device info from database # - updated commandref docu for cfgDB_List # # 2014-03-06 - added cfgDB_Diff to compare device in two versions # # 2014-03-07 - changed optimized cfgDB_Diff # restructured libraray internally # improved source code documentation # # 2014-03-20 - added export/import # 2014-04-01 - removed export/import due to not working properly # # 2014-04-03 - fixed global attributes not read from version 0 # # 2014-04-18 - added commands fileimport, fileexport # 2014-04-19 - added commands filelist, filedelete # interface cfgDB_Readfile for interaction # with other modules # # 2014-04-21 - added interface functions for FHEMWEB and fhem.pl # to show files in "Edit files" and use them # with CommandReload() mechanism # # modified _cfgDB_Info to show number of files in db # # 2014-04-23 - added command fileshow, filemove # # 2014-04-26 - added migration to generic file handling # fixed problem on migration of multiline DEFs # # 2014-04-27 - added new functions for binfile handling # # 2014-05-11 - removed command binfileimport # changed store all files as binary # added _cfgDB_Move to move all files from text # to binary filesave on first load of configDB # # 2014-05-12 - added sorted write & read for config data # # 2014-05-15 - fixed handling of multiline defs # # 2014-05-20 - removed no longer needed functions for file handling # changed code improvement; use strict; use warnings; # # 2014-08-22 - added automatic fileimport during migration # # 2014-09-30 - added support for device based userattr # # 2015-01-12 - changed use fhem function createUniqueId() # instead of database calls # # 2015-01-15 - changed remove 99_Utils.pm from filelist # # 2015-01-17 - added configdb diff all current # shows diff table between version 0 # and currently running version (in memory) # # 2015-01-23 - changed attribute handling for internal configDB attrs # # 2015-01-23 - added FileRead() caching - experimental # # 2015-10-14 - changed search conditions use ESCAPE, forum #42190 # # 2016-03-19 - changed use modpath, forum #51036 # # 2016-03-26 - added log entry for search (verbose=5) # # 2016-05-22 - added configdb dump (for sqlite) # # 2016-05-28 - added configdb dump (for mysql) # # 2016-05-29 - changed improve support for postgresql (tnx to Matze) # added configdb dump (for postgresql) # # 2016-07-03 - added support for multiple hosts (experimental) # 2016-07-04 - fixed improve config file read # 2016-07-07 - bugfix select configuration # # 2017-03-24 - added use index on fhemconfig (only sqlite) # # 2017-07-17 - changed store files base64 encoded # # 2017-08-31 - changed improve table_info for migration check # # 2018-02-17 - changed remove experimenatal cache functions # 2018-02-18 - changed move dump processing to backend # # 2018-03-24 - changed set privacy as default for username and password # 2018-03-25 - changed move rescue modes from ENV to config file # # 2018-06-17 - changed remove migration on FHEM start by default # check migration only if parameter migrate => 1 # is set in configDB.conf # # 2018-07-04 - bugfix change rescue mode persistence # # 2018-07-07 - change lastReorg added to info output # # 2018-09-08 - change remove base64 migration functions # # 2019-01-17 - added support for device specific uuid (setuuid) # 2019-01-18 - changed use GetDefAndAttr() # # 2019-02-16 - changed default field length for table creation # # 2020-02-25 - added support weekprofile in automatic migration # # 2020-06-37 - added support for special strange readings (length check) # # 2020-06-29 - added support for mysqldump parameter by attribute # # 2020-07-02 - changed code cleanup after last changes (remove debug code) # add "configdb attr ?" to show known attributes # # 2021-04-17 - bugfix problem in File.* commands regarding case sensitivity # # 2021-08-17 - changed adopt to Rudi's funny fakelog changes # # 2021-10-24 - added delete old files for large readings # =cut =for comment (changelog starting 2022) ############################################################################## # # 2022-02-20 - changed use createUniqueId() for uuids # remove _cfgDB_Uuid() # # 2022-02-20 - added statefile versioning - begin # 2022-03-03 statefile versioning - completed # # 2022-03-14 - fixed statefile problems with POSTGRESQL # 2022-08-06 - added attribute shortinfo for use with configdb info # 2022-08-07 - added log a message if more than 20 versions stored # # 2022-12-06 - added add raw json output in configdb info # # 2023-08-07 - fixed missing uuid in migration process # # 2023-08-23 - added show version counter in save message # ############################################################################## =cut use strict; use warnings; use Text::Diff; use DBI; use Sys::Hostname; use MIME::Base64; ################################################## # Forward declarations for functions in fhem.pl # ## no critic sub AnalyzeCommandChain($$;$); sub GetDefAndAttr($;$); sub Log($$); sub createUniqueId(); ## use critic ################################################## # Forward declarations inside this library # sub cfgDB_AttrRead; sub cfgDB_ReadAll; sub cfgDB_Init; sub cfgDB_FileRead; sub cfgDB_FileUpdate; sub cfgDB_Fileversion; sub cfgDB_FileWrite; sub cfgDB_FW_fileList; sub cfgDB_Read99; sub cfgDB_SaveCfg; sub cfgDB_SaveState; sub cfgDB_svnId; sub _cfgDB_binFileimport; sub _cfgDB_Connect; sub _cfgDB_DeleteTemp; sub _cfgDB_Diff; sub __cfgDB_Diff; sub _cfgDB_InsertLine; sub _cfgDB_Execute; sub _cfgDB_Filedelete; sub _cfgDB_Fileexport; sub _cfgDB_Filelist; sub _cfgDB_Info; sub _cfgDB_Migrate; sub _cfgDB_ReadCfg; sub _cfgDB_ReadState; sub _cfgDB_Recover; sub _cfgDB_Reorg; sub _cfgDB_Rotate; sub _cfgDB_Search; sub _cfgDB_table_exists; sub _cfgDB_dump; sub _cfgDB_knownAttr; sub _cfgDB_deleteRF; sub _cfgDB_deleteStatefiles; ################################################## # Read configuration file for DB connection # my ($err,@c) = FileRead({FileName => 'configDB.conf', ForceType => "file"}); return 0 if ($err); my @config; foreach my $line (@c) { $line =~ s/^\s+|\s+$//g; # remove whitespaces etc. $line =~ s/;$/;;/; # duplicate ; at end-of-line push (@config,$line) if($line !~ m/^#/ && length($line) > 0); } use vars qw(%configDB); my %dbconfig; my $configs = join("",@config); my @configs = split(/;;/,$configs); my $count = @configs; my $fhemhost = hostname; ## no critic if ($count > 1) { foreach my $c (@configs) { next unless $c =~ m/^%dbconfig.*/; $dbconfig{fhemhost} = ""; eval $c; last if ($dbconfig{fhemhost} eq $fhemhost); } eval $configs[0] if ($dbconfig{fhemhost} eq ""); } else { eval $configs[0]; } ## use critic my $cfgDB_dbconn = $dbconfig{connection}; my $cfgDB_dbuser = $dbconfig{user}; my $cfgDB_dbpass = $dbconfig{password}; my $cfgDB_dbtype; my $cfgDB_filename; if($cfgDB_dbconn =~ m/pg:/i) { $cfgDB_dbtype ="POSTGRESQL"; } elsif ($cfgDB_dbconn =~ m/mysql:/i) { $cfgDB_dbtype = "MYSQL"; } elsif ($cfgDB_dbconn =~ m/sqlite:/i) { $cfgDB_dbtype = "SQLITE"; (undef,$cfgDB_filename) = split(/=/,$cfgDB_dbconn); $configDB{filename} = $cfgDB_filename; } else { $cfgDB_dbtype = "unknown"; } $configDB{type} = $cfgDB_dbtype; $configDB{exclude} = defined($dbconfig{exclude}) ? $dbconfig{exclude} : ''; $configDB{attr}{nostate} = defined($dbconfig{nostate}) ? $dbconfig{nostate} : 0; $configDB{attr}{rescue} = defined($dbconfig{rescue}) ? $dbconfig{rescue} : 0; $configDB{attr}{loadversion} = defined($dbconfig{loadversion}) ? $dbconfig{loadversion} : 0; _cfgDB_knownAttr(); %dbconfig = (); @config = (); $configs = undef; $count = undef; ################################################## # Basic functions needed for DB configuration # directly called from fhem.pl # # initialize database, create tables if necessary sub cfgDB_Init { ################################################## # Create non-existing database tables # Create default config entries if necessary # my $fhem_dbh = _cfgDB_Connect; # create TABLE fhemversions ifnonexistent $fhem_dbh->do("CREATE TABLE IF NOT EXISTS fhemversions(VERSION INT, VERSIONUUID CHAR(50), VERSIONTAG CHAR(50))"); # create TABLE fhemconfig if nonexistent $fhem_dbh->do("CREATE TABLE IF NOT EXISTS fhemconfig(COMMAND VARCHAR(32), DEVICE VARCHAR(64), P1 VARCHAR(64), P2 TEXT, VERSION INT, VERSIONUUID CHAR(50))"); # create INDEX on fhemconfig if nonexistent (only if SQLITE) $fhem_dbh->do("CREATE INDEX IF NOT EXISTS config_idx on 'fhemconfig' (versionuuid,version)") if($cfgDB_dbtype eq "SQLITE"); # check TABLE fhemconfig already populated my $count = $fhem_dbh->selectrow_array('SELECT count(*) FROM fhemconfig'); if($count < 1) { # insert default entries to get fhem running $fhem_dbh->commit(); my $uuid = createUniqueId(); $fhem_dbh->do("INSERT INTO fhemversions values (0, '$uuid',NULL)"); _cfgDB_InsertLine($fhem_dbh, $uuid, '#created by cfgDB_Init',0); _cfgDB_InsertLine($fhem_dbh, $uuid, 'attr global logdir ./log',1); _cfgDB_InsertLine($fhem_dbh, $uuid, 'attr global logfile %L/fhem-%Y-%m-%d.log',2); _cfgDB_InsertLine($fhem_dbh, $uuid, 'attr global modpath .',3); _cfgDB_InsertLine($fhem_dbh, $uuid, 'attr global userattr devStateIcon devStateStyle icon sortby webCmd',4); _cfgDB_InsertLine($fhem_dbh, $uuid, 'attr global verbose 3',5); _cfgDB_InsertLine($fhem_dbh, $uuid, 'define telnetPort telnet 7072 global',6); _cfgDB_InsertLine($fhem_dbh, $uuid, 'define web FHEMWEB 8083 global',7); _cfgDB_InsertLine($fhem_dbh, $uuid, 'attr web allowfrom .*',8); _cfgDB_InsertLine($fhem_dbh, $uuid, 'define Logfile FileLog %L/fhem-%Y-%m-%d.log Logfile',9); } # create TABLE fhemstate if nonexistent $fhem_dbh->do("CREATE TABLE IF NOT EXISTS fhemstate(stateString TEXT)"); # create TABLE fhemb64filesave if nonexistent if($cfgDB_dbtype eq "MYSQL") { $fhem_dbh->do("CREATE TABLE IF NOT EXISTS fhemb64filesave(filename TEXT, content MEDIUMBLOB)"); } elsif ($cfgDB_dbtype eq "POSTGRESQL") { $fhem_dbh->do("CREATE TABLE IF NOT EXISTS fhemb64filesave(filename TEXT, content bytea)"); } else { $fhem_dbh->do("CREATE TABLE IF NOT EXISTS fhemb64filesave(filename TEXT, content BLOB)"); } # modify table for version tags if needed eval {$fhem_dbh->do("SELECT versiontag from fhemversions where version = 0")}; $fhem_dbh->do("ALTER TABLE fhemversions ADD VERSIONTAG char(50)") if $@; # close database connection $fhem_dbh->commit(); $fhem_dbh->disconnect(); return; } # read attributes sub cfgDB_AttrRead { my ($readSpec) = @_; my ($row, $sql, @line, @rets); my $fhem_dbh = _cfgDB_Connect; my $uuid = $fhem_dbh->selectrow_array('SELECT versionuuid FROM fhemversions WHERE version = 0'); $sql = "SELECT * FROM fhemconfig WHERE COMMAND = 'attr' AND DEVICE = '$readSpec' AND VERSIONUUID = '$uuid'"; $sql = "SELECT * FROM fhemconfig WHERE COMMAND = 'attr' AND (DEVICE = 'global' OR DEVICE = 'configdb') and VERSIONUUID = '$uuid'" if($readSpec eq 'global'); my $sth = $fhem_dbh->prepare( $sql ); $sth->execute(); while (@line = $sth->fetchrow_array()) { if($line[1] eq 'configdb') { $configDB{attr}{$line[2]} = $line[3]; } else { push @rets, "attr $line[1] $line[2] $line[3]"; } } $fhem_dbh->disconnect(); return @rets; } # generic file functions called from fhem.pl sub cfgDB_FileRead { my ($filename,$fhem_dbh) = @_; my $internal_call = 1 if $fhem_dbh; Log 4, "configDB reading file: $filename"; my ($err, @ret, $counter); $fhem_dbh = _cfgDB_Connect unless $fhem_dbh; my $read_cmd = "SELECT content FROM fhemb64filesave WHERE filename = '$filename'"; my $sth = $fhem_dbh->prepare( $read_cmd ); $sth->execute(); my $blobContent = $sth->fetchrow_array(); $sth->finish(); $fhem_dbh->disconnect() unless $internal_call; $blobContent = decode_base64($blobContent) if ($blobContent); $counter = length($blobContent); if($counter) { @ret = split(/\n/,$blobContent); $err = ""; } else { @ret = undef; $err = "Error on reading $filename from database!"; } return ($err, @ret); } sub cfgDB_FileWrite { my ($filename,@content) = @_; Log 4, "configDB writing file: $filename"; my $fhem_dbh = _cfgDB_Connect; $fhem_dbh->do("delete from fhemb64filesave where filename = '$filename'"); my $sth = $fhem_dbh->prepare('INSERT INTO fhemb64filesave values (?, ?)'); $sth->execute($filename,encode_base64(join("\n", @content))); $sth->finish(); $fhem_dbh->commit(); $fhem_dbh->disconnect(); return; } sub cfgDB_FileUpdate { my ($filename) = @_; my $fhem_dbh = _cfgDB_Connect; my $id = $fhem_dbh->selectrow_array("SELECT filename from fhemb64filesave where filename = '$filename'"); $fhem_dbh->disconnect(); if($id) { my $filesize = -s $filename; _cfgDB_binFileimport($filename,$filesize,1) if ($id) ; Log 4, "file $filename updated in configDB"; } return; } # read and execute fhemconfig and statefile sub cfgDB_ReadAll { ## prototype used in fhem.pl my ($cl) = @_; my ($ret, @dbconfig); if ($configDB{attr}{rescue} == 1) { Log 0, 'configDB starting in rescue mode!'; push (@dbconfig, 'attr global modpath .'); push (@dbconfig, 'attr global verbose 3'); push (@dbconfig, 'define telnetPort telnet 7072 global'); push (@dbconfig, 'define web FHEMWEB 8083 global'); push (@dbconfig, 'attr web allowfrom .*'); push (@dbconfig, 'define Logfile FileLog ./log/fhem-%Y-%m-%d.log Logfile'); } else { # add Config Rows to commandfile @dbconfig = _cfgDB_ReadCfg(@dbconfig); # add State Rows to commandfile @dbconfig = _cfgDB_ReadState(@dbconfig) unless $configDB{attr}{nostate} == 1; } # AnalyzeCommandChain for all entries $ret = _cfgDB_Execute($cl, @dbconfig); return $ret if($ret); return; } # save running configuration to version 0 sub cfgDB_SaveCfg { ## prototype used in fhem.pl Log 4, "configDB save config ".$data{saveID} if(defined($data{saveID})); my ($internal) = shift; $internal = defined($internal) ? $internal : 0; my $c = "configdb"; my @dontSave = qw(configdb:rescue configdb:nostate configdb:loadversion configdb:shortinfo global:configfile global:statefile global:version); my (%devByNr, @rowList, %comments, $t, $out); map { $devByNr{$defs{$_}{NR}} = $_ } keys %defs; for(my $i = 0; $i < $devcount; $i++) { my ($h, $d); if($comments{$i}) { $h = $comments{$i}; } else { $d = $devByNr{$i}; next if(!defined($d) || $defs{$d}{TEMPORARY} || # e.g. WEBPGM connections $defs{$d}{VOLATILE}); # e.g at, will be saved to the statefile $h = $defs{$d}; } if(!defined($d)) { push @rowList, $h->{TEXT}; next; } push (@rowList, GetDefAndAttr($d,1)); } foreach my $a (sort keys %{$configDB{attr}}) { my $val = $configDB{attr}{$a}; next unless $val; next if grep {$_ eq "$c:$a";} @dontSave; $val =~ s/;/;;/g; push @rowList, "attr $c $a $val"; } $t = localtime; $out = "#created $t"; push @rowList, $out; Debug "\n".join("\n",@rowList) if defined($data{cfgDB_debug}); return @rowList if defined($data{cfgDB_rawList}); # Insert @rowList into database table my $fhem_dbh = _cfgDB_Connect; my ($num,$uuid) = split(/\:/,_cfgDB_Rotate($fhem_dbh,$internal)); my $counter = 0; foreach (@rowList) { _cfgDB_InsertLine($fhem_dbh, $uuid, $_, $counter); $counter++; } $fhem_dbh->commit(); $fhem_dbh->disconnect(); my $maxVersions = $configDB{attr}{maxversions}; $maxVersions = ($maxVersions) ? $maxVersions : 0; _cfgDB_Reorg($maxVersions,1) if($maxVersions && $internal != -1); return "configDB saved. ($num)"; } # save statefile sub cfgDB_SaveState { my ($out,$val,$r,$rd,$t,@rowList); # don't write statefile in rescue mode return if ($configDB{attr}{rescue} == 1); # _cfgDB_deleteRF; $t = localtime; $out = "#$t"; push @rowList, $out; foreach my $d (sort keys %defs) { next if($defs{$d}{TEMPORARY}); if($defs{$d}{VOLATILE}) { $out = "define $d $defs{$d}{TYPE} $defs{$d}{DEF}"; push @rowList, $out; } $val = $defs{$d}{STATE}; if(defined($val) && $val ne "unknown" && $val ne "Initialized" && $val ne "" && $val ne "???") { $val =~ s/;/;;/g; $val =~ s/\n/\$xyz\$/g; $out = "setstate $d $val"; Log 5, "configDB: $out"; push @rowList, $out; } $r = $defs{$d}{READINGS}; if($r) { foreach my $c (sort keys %{$r}) { $rd = $r->{$c}; if(!defined($rd->{TIME})) { Log 5, "WriteStatefile $d $c: Missing TIME, using current time"; $rd->{TIME} = TimeNow(); } if(!defined($rd->{VAL})) { Log 5, "WriteStatefile $d $c: Missing VAL, setting it to 0"; $rd->{VAL} = 0; } $val = $rd->{VAL}; $val =~ s/;/;;/g; $val =~ s/\n/\$xyz\$/g; $out = "setstate $d $rd->{TIME} $c $val"; Log 5, "configDB: $out"; push @rowList, $out; } } } my $fileName = defined($data{saveID}) ? $data{saveID} : $configDB{loaded}; $fileName =~ s/^\s+|\s+$//g; # trim filename $fileName .= ".fhem.save"; Log 4, "configDB save state $fileName with ".$#rowList." entries"; cfgDB_FileWrite($fileName,@rowList); return; } # import existing files during migration sub cfgDB_MigrationImport { my ($ret, $filename, @files, @def); my $modpath = AttrVal("global","modpath","."); # find eventTypes file @def = ''; @def = _cfgDB_findDef('TYPE=eventTypes'); foreach my $fn (@def) { next unless $fn; push @files, $fn; } # import templateDB.gplot $filename = "$modpath/www/gplot/template.gplot"; push @files, $filename; $filename = "$modpath/www/gplot/templateDB.gplot"; push @files, $filename; # import template.layout $filename = "$modpath/FHEM/template.layout"; push @files, $filename; # find used gplot files @def = ''; @def = _cfgDB_findDef('TYPE=SVG','GPLOTFILE'); foreach my $fn (@def) { next unless $fn; push @files, "$modpath/www/gplot/".$fn.".gplot"; } # find DbLog configs @def = ''; @def = _cfgDB_findDef('TYPE=DbLog','CONFIGURATION'); foreach my $fn (@def) { next unless $fn; push @files, $fn; } # find RSS and Infopanel layouts @def = ''; @def = _cfgDB_findDef('TYPE=(RSS|InfoPanel)','LAYOUTFILE'); foreach my $fn (@def) { next unless $fn; push @files, $fn; } # find weekprofile/LightScene/RHASSPY configurations @def = ''; @def = _cfgDB_findDef('TYPE=(weekprofile|LightScene|RHASSPY)','CONFIGFILE'); foreach my $fn (@def) { next unless $fn; push @files, $fn; } # find holiday files @def = ''; @def = _cfgDB_findDef('TYPE=holiday','NAME'); foreach my $fn (@def) { next unless $fn; if(defined($defs{$fn}{HOLIDAYFILE})) { push @files, $defs{$fn}{HOLIDAYFILE}; } else { push @files, "$modpath/FHEM/holiday/".$fn.".holiday"; } } # import uniqueID file $filename = "$modpath/FHEM/FhemUtils/uniqueID"; push @files,$filename if (-e $filename); # do the import foreach my $fn (@files) { if ( -r $fn ) { my $filesize = -s $fn; _cfgDB_binFileimport($fn,$filesize); $ret .= "importing: $fn\n"; } } return $ret; } # return SVN Id, called by fhem's CommandVersion sub cfgDB_svnId { return "# ".'$Id$' } # return filelist depending on directory and regexp sub cfgDB_FW_fileList { my ($dir,$re,@ret) = @_; my @files = split(/\n/, _cfgDB_Filelist('notitle')); foreach my $f (@files) { next if( $f !~ m/^$dir/ ); $f =~ s,$dir\/,,; next if($f !~ m,^$re$, || $f eq '99_Utils.pm'); push @ret, "$f.configDB"; } return @ret; } # read filelist containing 99_ files in database sub cfgDB_Read99 { my $ret = ""; my $fhem_dbh = _cfgDB_Connect; my $sth = $fhem_dbh->prepare( "SELECT filename FROM fhemb64filesave WHERE filename like '%/99_%.pm' group by filename" ); $sth->execute(); while (my $line = $sth->fetchrow_array()) { $line =~ m,^(.*)/([^/]*)$,; # Split into dir and file $ret .= "$2,"; # } $sth->finish(); $fhem_dbh->disconnect(); $ret =~ s/,$//; return $ret; } # return SVN Id from file stored in database sub cfgDB_Fileversion { my ($file,$ret) = @_; $ret = "No Id found for $file"; my ($err,@in) = cfgDB_FileRead($file); foreach(@in){ $ret = $_ if($_ =~ m/# \$Id:/); } return $ret; } ################################################## # Basic functions needed for DB configuration # but not called from fhem.pl directly # # connect do database sub _cfgDB_Connect { my $fhem_dbh = DBI->connect( "dbi:$cfgDB_dbconn", $cfgDB_dbuser, $cfgDB_dbpass, { AutoCommit => 0, RaiseError => 1 }, ) or die $DBI::errstr; return $fhem_dbh; } # add configuration entry into fhemconfig sub _cfgDB_InsertLine { my ($fhem_dbh, $uuid, $line, $counter) = @_; Log 0, "configDB: $line" if defined($data{cfgDB_debug}); my ($c,$d,$p1,$p2) = split(/ /, $line, 4); my $sth = $fhem_dbh->prepare('INSERT INTO fhemconfig values (?, ?, ?, ?, ?, ?)'); $sth->execute($c, $d, $p1, $p2, $counter, $uuid); return; } # pass command table to AnalyzeCommandChain sub _cfgDB_Execute { my ($cl, @dbconfig) = @_; my (@ret); foreach my $l (@dbconfig) { $l =~ s/[\r\n]/\n/g; $l =~ s/\\\n/\n/g; my $tret = AnalyzeCommandChain($cl, $l); push @ret, $tret if(defined($tret)); } return join("\n", @ret) if(@ret); return; } # read all entries from fhemconfig # and add them to command table for execution sub _cfgDB_ReadCfg { my (@dbconfig) = @_; my $fhem_dbh = _cfgDB_Connect; my ($sth, @line, $row); my $version = $configDB{attr}{loadversion}; delete $configDB{attr}{loadversion}; if ($version > 0) { my $count = $fhem_dbh->selectrow_array('SELECT count(*) FROM fhemversions'); $count--; $version = $version > $count ? $count : $version; Log 0, "configDB loading version $version on user request."; } # maybe this will be done with join later my $uuid = $fhem_dbh->selectrow_array("SELECT versionuuid FROM fhemversions WHERE version = '$version'"); $uuid =~ s/^\s+|\s+$//g; $configDB{loaded} = $uuid; Log 4, "configDB read config ".$configDB{loaded}; my @excluded = split(/,/,$configDB{exclude}); map { s/^\s+|\s+$//g; } @excluded; $sth = $fhem_dbh->prepare( "SELECT * FROM fhemconfig WHERE versionuuid = '$uuid' and device <>'configdb' order by version" ); $sth->execute(); while (@line = $sth->fetchrow_array()) { $row = "$line[0] $line[1] $line[2]"; $row .= " $line[3]" if defined($line[3]); if ( grep( /^$line[1]$/, @excluded ) ) { Log 1, "configDB excluding $line[1] ($row)" if $line[0] eq "define"; } else { push @dbconfig, $row; } } $fhem_dbh->disconnect(); return @dbconfig; } # read all entries from fhemstate # and add them to command table for execution sub _cfgDB_ReadState { my (@dbconfig) = @_; my $stateFileName = $configDB{loaded}.".fhem.save"; my ($err,@state) = cfgDB_FileRead($stateFileName); if ($err eq "") { Log 4, "configDB read state ".$stateFileName; map { my $a = $_; $a =~ s/\$xyz\$/\\n/g; push @dbconfig, $a } @state; my $fhem_dbh = _cfgDB_Connect; $fhem_dbh->do("delete from fhemstate"); $fhem_dbh->commit(); $fhem_dbh->disconnect(); } else { Log 4, "configDB read state from table fhemstate"; my $fhem_dbh = _cfgDB_Connect; my ($sth, $row,$f); $sth = $fhem_dbh->prepare( "SELECT * FROM fhemstate" ); $sth->execute(); while ($row = $sth->fetchrow_array()) { if($row =~ m/(cfgDBkey:)(.{32})/) { my $f = $2; my (undef, $content) = cfgDB_FileRead($f,$fhem_dbh); $row =~ s/cfgDB:................................$/$content/; _cfgDB_Filedelete($f,$fhem_dbh); } push @dbconfig, $row; } $fhem_dbh->disconnect(); } return @dbconfig; } # rotate all versions to versionnum + 1 # return uuid for new version 0 sub _cfgDB_Rotate { my ($fhem_dbh,$newversion) = @_; my $uuid = $data{saveID}; $uuid =~ s/^\s+|\s+$//g; delete $data{saveID}; # no longer needed in memory $configDB{loaded} = $uuid; my $count = $fhem_dbh->do("UPDATE fhemversions SET VERSION = VERSION+1 where VERSION >= 0") if $newversion == 0; $fhem_dbh->do("INSERT INTO fhemversions values ('$newversion', '$uuid', NULL)"); Log3(undef,1,"configDB: more than 20 versions in database! Please consider setting a limit.") if ($count > 20 && !defined($configDB{attr}{maxversions})); return "$count:$uuid"; } sub _cfgDB_filesize_str { my ($size) = @_; if ($size > 1099511627776) # TiB: 1024 GiB { return sprintf("%.2f TB", $size / 1099511627776); } elsif ($size > 1073741824) # GiB: 1024 MiB { return sprintf("%.2f GB", $size / 1073741824); } elsif ($size > 1048576) # MiB: 1024 KiB { return sprintf("%.2f MB", $size / 1048576); } elsif ($size > 1024) # KiB: 1024 B { return sprintf("%.2f KB", $size / 1024); } else # bytes { return "$size byte" . ($size == 1 ? "" : "s"); } } ################################################## # Additional backend functions # not called from fhem.pl directly # # migrate existing fhem config into database sub _cfgDB_Migrate { my $ret; $data{saveID} = createUniqueId(); $ret = "Starting migration...\n"; Log 4, 'configDB: Starting migration'; $ret .= "Processing: database initialization\n"; Log 4, 'configDB: Processing: cfgDB_Init'; cfgDB_Init; $ret .= "Processing: save config\n"; Log 4, 'configDB: Processing: cfgDB_SaveCfg'; cfgDB_SaveCfg; $ret .= "Processing: save state\n"; Log 4, 'configDB: Processing: cfgDB_SaveState'; cfgDB_SaveState; $ret .= "Processing: fileimport\n"; Log 4, 'configDB: Processing: cfgDB_MigrationImport'; $ret .= cfgDB_MigrationImport; $ret .= "Migration completed\n\n"; Log 4, 'configDB: Migration completed.'; $ret .= _cfgDB_Info(undef); return $ret; } # show database statistics sub _cfgDB_Info { my ($info2,$raw) = @_; $info2 //= 'unknown'; $raw //= 0; my ($l, @r, $f); for my $i (1..65){ $l .= '-';} $configDB{attr}{private} //= 1; push @r, $l; push @r, " configDB Database Information"; push @r, $l; my $info1 = cfgDB_svnId; $info1 =~ s/# //; push @r, " d:$info1"; push @r, " c:$info2"; push @r, $l; push @r, " dbconn: $cfgDB_dbconn"; push @r, " dbuser: $cfgDB_dbuser" if !$configDB{attr}{private}; push @r, " dbpass: $cfgDB_dbpass" if !$configDB{attr}{private}; push @r, " dbtype: $cfgDB_dbtype"; push @r, " Unknown dbmodel type in configuration file." if $cfgDB_dbtype eq 'unknown'; push @r, " Only Mysql, Postgresql, SQLite are fully supported." if $cfgDB_dbtype eq 'unknown'; if ($cfgDB_dbtype eq "SQLITE") { my $size = -s $cfgDB_filename; $size = _cfgDB_filesize_str($size); push @r, " dbsize: $size"; } push @r, $l; push @r, " loaded: ".$configDB{loaded}; my $fhem_dbh = _cfgDB_Connect; my ($sql, $sth, @line, $row, $countDef, $countAttr, @raw); # read versions table statistics $configDB{attr}{shortinfo} //= 0; if ($configDB{attr}{shortinfo} == 0) { my $maxVersions = $configDB{attr}{maxversions}; $maxVersions = ($maxVersions) ? $maxVersions : 0; push @r, " max Versions: $maxVersions" if($maxVersions); push @r, " lastReorg: ".$configDB{attr}{'lastReorg'}; my $count; $count = $fhem_dbh->selectrow_array('SELECT count(*) FROM fhemconfig'); push @r, " config: $count entries"; push @r, ""; # read versions creation time $sql = "SELECT * FROM fhemconfig as c join fhemversions as v on v.versionuuid=c.versionuuid ". "WHERE COMMAND like '#created%' ORDER by v.VERSION"; $sth = $fhem_dbh->prepare( $sql ); $sth->execute(); while (@line = $sth->fetchrow_array()) { $line[3] = "" unless defined $line[3]; $countDef = $fhem_dbh->selectrow_array("SELECT COUNT(*) from fhemconfig where COMMAND = 'define' and VERSIONUUID = '$line[5]'"); $countAttr = $fhem_dbh->selectrow_array("SELECT COUNT(*) from fhemconfig where COMMAND = 'attr' and VERSIONUUID = '$line[5]'"); $row = " Ver $line[6] saved: $line[1] $line[2] $line[3] def: $countDef attr: $countAttr"; $row .= " tag: ".$line[8] if $line[8]; push @r, $row; push @raw, {version => $line[6], saved => "$line[1] $line[2] $line[3]", def => $countDef, attr => $countAttr}; } } else { my $count; $count = $fhem_dbh->selectrow_array('SELECT count(*) FROM fhemversions'); push @r, " versions: $count"; $count = $fhem_dbh->selectrow_array('SELECT count(*) FROM fhemconfig'); push @r, " config: $count entries"; } push @r, $l; $row = $fhem_dbh->selectall_arrayref("SELECT filename from fhemb64filesave group by filename"); $count = @$row; $count = ($count)?$count:'No'; $f = ("$count" ne '1') ? "s" : ""; $row = " filesave: $count file$f stored in database"; push @r, $row; push @r, $l; $fhem_dbh->disconnect(); return toJSON \@raw if $raw; return join("\n", @r); } sub _cfgDB_Info_Json { my $cSVN = shift; $cSVN //= 'unknown'; my %info = (); # add ./FHEM/98_configdb.pm svn id $info{cSVN} = $cSVN; # add ./configDB.pm svn id my $dSVN = cfgDB_svnId; $dSVN =~ s/# //; $info{dSVN} = $dSVN; # add configDB database info $info{dbconn} = $cfgDB_dbconn; $info{dbuser} = $configDB{attr}{private} ? 'private' : $cfgDB_dbuser; $info{dbpass} = $configDB{attr}{private} ? 'private' : $cfgDB_dbpass; $info{dbtype} = $cfgDB_dbtype; $info{dbsize} = _cfgDB_filesize_str(-s $cfgDB_filename) if ($cfgDB_dbtype eq "SQLITE"); return toJSON \%info; } # recover former config from database archive sub _cfgDB_Recover { my ($version) = @_; my ($cmd, $count, $ret); if($version > 0) { my $fhem_dbh = _cfgDB_Connect; $cmd = "SELECT count(*) FROM fhemconfig WHERE VERSIONUUID in (select versionuuid from fhemversions where version = $version)"; $count = $fhem_dbh->selectrow_array($cmd); if($count > 0) { my $fromuuid = $fhem_dbh->selectrow_array("select versionuuid from fhemversions where version = $version"); my $touuid = createUniqueId(); # Delete current version 0 $fhem_dbh->do("DELETE FROM fhemconfig WHERE VERSIONUUID in (select versionuuid from fhemversions where version = 0)"); $fhem_dbh->do("update fhemversions set versionuuid = '$touuid' where version = 0"); # Copy selected version to version 0 my ($sth, $sth2, @line); $cmd = "SELECT * FROM fhemconfig WHERE VERSIONUUID = '$fromuuid'"; $sth = $fhem_dbh->prepare($cmd); $sth->execute(); $sth2 = $fhem_dbh->prepare('INSERT INTO fhemconfig values (?, ?, ?, ?, ?, ?)'); while (@line = $sth->fetchrow_array()) { $sth2->execute($line[0], $line[1], $line[2], $line[3], $line[4], $touuid); } $fhem_dbh->commit(); $fhem_dbh->disconnect(); # Copy corresponding statefile my $filename = $fromuuid.".fhem.save"; my ($err,@statefile) = FileRead($filename); $filename = $touuid.".fhem.save"; FileWrite($filename,@statefile); # Inform user about restart required $ret = "Version 0 deleted.\n"; $ret .= "Version $version copied to version 0\n\n"; $ret .= "FHEM will exit in 3 seconds."; InternalTimer(gettimeofday()+3, sub {exit 0}, 0); } else { $fhem_dbh->disconnect(); $ret = "No entries found in version $version.\nNo changes committed to database."; } } else { $ret = 'Please select version 1..n for recovery.'; } return $ret; } # delete old configurations sub _cfgDB_Reorg { my ($lastversion,$quiet) = @_; $lastversion = (defined($lastversion)) ? $lastversion : 3; Log 4, "configDB reorg started, keeping last $lastversion versions."; my $fhem_dbh = _cfgDB_Connect; $fhem_dbh->do("delete FROM fhemconfig where versionuuid in (select versionuuid from fhemversions where version > $lastversion)"); $fhem_dbh->do("delete from fhemversions where version > $lastversion"); $fhem_dbh->do("delete FROM fhemconfig where versionuuid in (select versionuuid from fhemversions where version = -1)"); $fhem_dbh->do("delete from fhemversions where version = -1"); my $ts = localtime(time); $configDB{attr}{'lastReorg'} = $ts; _cfgDB_InsertLine($fhem_dbh,$configDB{loaded},"attr configdb lastReorg $ts",-1); $fhem_dbh->commit(); $fhem_dbh->disconnect(); _cfgDB_deleteStatefiles(); eval { qx(sqlite3 $cfgDB_filename vacuum) } if($cfgDB_dbtype eq "SQLITE"); return if(defined($quiet)); return " Result after database reorg:\n"._cfgDB_Info(undef); } # delete temporary version sub _cfgDB_DeleteTemp { Log 4, "configDB: delete temporary Version -1"; my $fhem_dbh = _cfgDB_Connect; $fhem_dbh->do("delete FROM fhemconfig where versionuuid in (select versionuuid from fhemversions where version = -1)"); $fhem_dbh->do("delete from fhemversions where version = -1"); $fhem_dbh->commit(); $fhem_dbh->disconnect(); return; } # search for device or fulltext in db sub _cfgDB_Search { my ($search,$searchversion,$dsearch) = @_; return 'Syntax error.' if(!(defined($search))); my $fhem_dbh = _cfgDB_Connect; my ($sql, $sth, @line, $row, @result, $ret, $text); $sql = "SELECT command, device, p1, p2 FROM fhemconfig as c join fhemversions as v ON v.versionuuid=c.versionuuid "; $sql .= "WHERE v.version = '$searchversion' AND command not like '#create%' "; # 2015-10-24 - changed, forum #42190 if($cfgDB_dbtype eq 'SQLITE') {; $sql .= "AND device like '$search%' ESCAPE '\\' " if($dsearch); $sql .= "AND (device like '$search%' ESCAPE '\\' OR P1 like '$search%' ESCAPE '\\' OR P2 like '$search%' ESCAPE '\\') " if(!$dsearch); } else { $sql .= "AND device like '$search%' " if($dsearch); $sql .= "AND (device like '$search%' OR P1 like '$search%' OR P2 like '$search%') " if(!$dsearch); } $sql .= "ORDER BY lower(device),command DESC"; $sth = $fhem_dbh->prepare( $sql); Log 4, "configDB: $sql"; $sth->execute(); $text = $dsearch ? " device" : ""; push @result, "search result for$text: $search in version: $searchversion"; push @result, "--------------------------------------------------------------------------------"; while (@line = $sth->fetchrow_array()) { $row = "$line[0] $line[1] $line[2]"; $row .= " $line[3]" if defined($line[3]); Log 4, "configDB: $row"; push @result, "$row" unless ($line[0] eq 'setuuid'); } $fhem_dbh->disconnect(); $ret = join("\n", @result); return $ret; } # called from cfgDB_Diff sub __cfgDB_Diff { my ($fhem_dbh,$search,$searchversion,$svinternal) = @_; my ($sql, $sth, @line, $ret); if($svinternal != -1) { $sql = "SELECT command, device, p1, p2 FROM fhemconfig as c join fhemversions as v ON v.versionuuid=c.versionuuid ". "WHERE v.version = '$searchversion' AND device = '$search' ORDER BY command DESC"; } else { $sql = "SELECT command, device, p1, p2 FROM fhemconfig as c join fhemversions as v ON v.versionuuid=c.versionuuid ". "WHERE v.version = '$searchversion' ORDER BY command DESC"; } $sth = $fhem_dbh->prepare( $sql); $sth->execute(); while (@line = $sth->fetchrow_array()) { $line[3] //= ""; $ret .= "$line[0] $line[1] $line[2] $line[3]\n"; } return $ret; } # compare device configurations from 2 versions sub _cfgDB_Diff { my ($search,$searchversion) = @_; my ($ret, $v0, $v1); if ($search eq 'all' && $searchversion eq 'current') { _cfgDB_DeleteTemp(); cfgDB_SaveCfg(-1); $searchversion = -1; } my $fhem_dbh = _cfgDB_Connect; $v0 = __cfgDB_Diff($fhem_dbh,$search,0,$searchversion); $v1 = __cfgDB_Diff($fhem_dbh,$search,$searchversion,$searchversion); $fhem_dbh->disconnect(); $ret = diff \$v0, \$v1, { STYLE => "Table" }; if($searchversion == -1) { _cfgDB_DeleteTemp(); $searchversion = "UNSAVED"; } $ret = "\nNo differences found!" if !$ret; $ret = "compare device: $search in current version 0 (left) to version: $searchversion (right)\n$ret\n"; return $ret; } # find DEF, input supports devspec definitions sub _cfgDB_findDef { my ($search,$internal) = @_; $internal = 'DEF' unless defined($internal); my @ret; my @etDev = devspec2array($search); foreach my $d (@etDev) { next unless $d; push @ret, $defs{$d}{$internal}; } return @ret; } sub _cfgDB_type { return $cfgDB_dbtype; } sub _cfgDB_dump { my ($param1) = @_; $param1 //= ''; my ($dbconn,$dbuser,$dbpass,$dbtype) = _cfgDB_readConfig(); my ($dbname,$dbhostname,$dbport,$gzip,$mp,$ret,$size,$source,$target,$ts); $ts = strftime('%Y-%m-%d_%H-%M-%S',localtime); $mp = $configDB{attr}{'dumpPath'}; $mp //= AttrVal('global','modpath','.').'/log'; $target = "$mp/configDB_$ts.dump"; if (lc($param1) eq 'unzipped') { $gzip = ''; } else { $gzip = '| gzip -c'; $target .= '.gz'; } if ($dbtype eq 'SQLITE') { (undef,$source) = split (/=/, $dbconn); my $dumpcmd = "echo '.dump fhem%' | sqlite3 $source $gzip > $target"; Log 4, "configDB: $dumpcmd"; $ret = qx($dumpcmd); return $ret if $ret; # return error message if available } elsif ($dbtype eq 'MYSQL') { ($dbname,$dbhostname,$dbport) = split (/;/,$dbconn); $dbport //= '=3306'; (undef,$dbname) = split (/=/,$dbname); (undef,$dbhostname) = split (/=/,$dbhostname); (undef,$dbport) = split (/=/,$dbport); my $xparam = defined($configDB{attr}{mysqldump}) ? $configDB{attr}{mysqldump} : ''; my $dbtables = "fhemversions fhemconfig fhemstate fhemb64filesave"; my $dumpcmd = "mysqldump $xparam --user=$dbuser --password=$dbpass --host=$dbhostname --port=$dbport -Q $dbname $dbtables $gzip > $target"; Log 4, "configDB: $dumpcmd"; $ret = qx($dumpcmd); return $ret if $ret; $source = $dbname; } elsif ($dbtype eq 'POSTGRESQL') { ($dbname,$dbhostname,$dbport) = split (/;/,$dbconn); $dbport //= '=5432'; (undef,$dbname) = split (/=/,$dbname); (undef,$dbhostname) = split (/=/,$dbhostname); (undef,$dbport) = split (/=/,$dbport); my $dbtables = "-t fhemversions -t fhemconfig -t fhemstate -t fhemb64filesave"; my $dumpcmd = "PGPASSWORD=$dbpass pg_dump -U $dbuser -h $dbhostname -p $dbport $dbname $dbtables $gzip > $target"; Log 4, "configDB: $dumpcmd"; $ret = qx($dumpcmd); return $ret if $ret; $source = $dbname; } else { return "configdb dump not supported for $dbtype!"; } $size = -s $target; $size //= 0; $ret = "configDB dumped $size bytes\nfrom: $source\n to: $target"; return $ret; } sub _cfgDB_knownAttr { $configDB{knownAttr}{deleteimported} = "(0|1) delete file from filesystem after import"; $configDB{knownAttr}{dumpPath} = "(valid path) define path for database dump"; $configDB{knownAttr}{maxversions}= "(number) define maximum number of configurations stored in database"; $configDB{knownAttr}{mysqldump}= "(valid parameter string) define additional parameters used for dump in mysql environment"; $configDB{knownAttr}{private}= "(0|1) show or supress userdata in info output"; $configDB{knownAttr}{shortinfo}= "(0|1) show detailed or short result in info output"; # $configDB{knownAttr}{loadversion}= # "for internal use only"; # $configDB{knownAttr}{nostate}= # "for internal use only"; # $configDB{knownAttr}{rescue}= # "for internal use only"; } sub _cfgDB_deleteRF { # Delete old files containing large readings my $filename; my $fhem_dbh2 = _cfgDB_Connect; my $sth = $fhem_dbh2->prepare( "SELECT filename FROM fhemb64filesave" ); $sth->execute(); while ($filename = $sth->fetchrow_array()) { if ($filename =~ m/^[0-9A-F]+$/i) { Log 4, "configDB delete file: $filename"; $fhem_dbh2->do("delete from fhemb64filesave where filename = '$filename'"); } } $fhem_dbh2->commit(); $fhem_dbh2->disconnect(); } sub _cfgDB_deleteStatefiles { my $filename; my $fhem_dbh = _cfgDB_Connect; my $sth = $fhem_dbh->prepare( "SELECT filename FROM fhemb64filesave where filename like '%.fhem.save'" ); $sth->execute(); while ($filename = $sth->fetchrow_array()) { Log 5, "configDB: statefile filename >$filename<"; if (length($filename) > 42) { # malformed filename from postgresql Log 5, "configDB: statefile del1 >$filename<"; $fhem_dbh->do("delete from fhemb64filesave where filename = '$filename'"); next; } my $uuid = ""; $uuid = substr($filename,0,32); Log 5, "configDB: statefile uuid: >$uuid<"; my $found = $fhem_dbh->selectrow_array("SELECT versionuuid FROM fhemversions WHERE versionuuid = '$uuid'"); $found //= 'notfound'; # to prevent perl warning $found = substr($found,0,32); Log 5, "configDB: statefile found: >$found<"; unless ($uuid eq $found) { Log 5, "configDB: statefile del2 >$filename<"; $fhem_dbh->do("delete from fhemb64filesave where filename = '$filename'"); } } $fhem_dbh->commit(); $fhem_dbh->disconnect(); } ################################################## # functions used for file handling # called by 98_configdb.pm # # delete file from database sub _cfgDB_Filedelete { my ($filename,$fhem_dbh) = @_; my $internal_call = 1 if $fhem_dbh; $fhem_dbh = _cfgDB_Connect unless $internal_call; my $ret = $fhem_dbh->do("delete from fhemb64filesave where filename = '$filename'"); $fhem_dbh->commit(); $fhem_dbh->disconnect() unless $internal_call; $ret = ($ret > 0) ? 1 : undef; return $ret; } # export file from database to filesystem sub _cfgDB_Fileexport { my ($filename,$raw) = @_; my $fhem_dbh = _cfgDB_Connect; my $sth = $fhem_dbh->prepare( "SELECT content FROM fhemb64filesave WHERE filename = '$filename'" ); $sth->execute(); my $blobContent = $sth->fetchrow_array(); $blobContent = decode_base64($blobContent) if($blobContent); my $counter = length($blobContent); $sth->finish(); $fhem_dbh->disconnect(); return "No data found for file $filename" unless $counter; return ($blobContent,$counter) if $raw; open( my $f,">","$filename" ); binmode($f); print $f $blobContent; close( $f ); return "$counter bytes written from database into file $filename"; } # import file into database sub _cfgDB_binFileimport { my ($filename,$filesize,$doDelete) = @_; $doDelete = (defined($doDelete)) ? 1 : 0; open (my $inFile,"<","$filename") || die $!; my $blobContent; binmode($inFile); my $readBytes = read($inFile, $blobContent, $filesize); close($inFile); $blobContent = encode_base64($blobContent); my $fhem_dbh = _cfgDB_Connect; $fhem_dbh->do("delete from fhemb64filesave where filename = '$filename'"); my $sth = $fhem_dbh->prepare('INSERT INTO fhemb64filesave values (?, ?)'); # add support for postgresql by Matze $sth->bind_param( 1, $filename ); if ($cfgDB_dbtype eq "POSTGRESQL") { $sth->bind_param( 2, $blobContent, { pg_type => DBD::Pg::PG_BYTEA() } ); } else { $sth->bind_param( 2, $blobContent ); } $sth->execute($filename, $blobContent); $sth->finish(); $fhem_dbh->commit(); $fhem_dbh->disconnect(); unlink($filename) if(($configDB{attr}{deleteimported} || $doDelete) && $readBytes); return "$readBytes bytes written from file $filename to database"; } # list all files stored in database sub _cfgDB_Filelist { my ($notitle) = @_; my $ret = "Files found in database:\n". "------------------------------------------------------------\n"; $ret = "" if $notitle; my $fhem_dbh = _cfgDB_Connect; my $sql = "SELECT filename FROM fhemb64filesave group by filename order by filename"; my $content = $fhem_dbh->selectall_arrayref($sql); foreach my $row (@$content) { $ret .= "@$row[0]\n" if(defined(@$row[0])); } $fhem_dbh->disconnect(); return $ret; } 1; =pod =item helper =item summary configDB backend =item summary_DE configDB backend =begin html

configDB

=end html =begin html_DE

configDB

=end html_DE =cut