# $Id$ ############################################################################## # # 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 ® # e-mail: fhem.development@betateilchen.de # # 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 . # ############################################################################## # # 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 # ############################################################################## # use DBI; ################################################## # Forward declarations for functions in fhem.pl # sub AnalyzeCommandChain($$;$); sub Debug($); sub Log3($$$); ################################################## # Read configuration file # if(!open(CONFIG, 'configDB.conf')) { Log3('configDB', 1, 'Cannot open database configuration file configDB.conf'); return 0; } my @config=; close(CONFIG); my %dbconfig; eval join("", @config); my $cfgDB_dbconn = $dbconfig{connection}; my $cfgDB_dbuser = $dbconfig{user}; my $cfgDB_dbpass = $dbconfig{password}; my $cfgDB_dbtype; (%dbconfig, @config) = (undef,undef); if($cfgDB_dbconn =~ m/pg:/i) { $cfgDB_dbtype ="POSTGRESQL"; } elsif ($cfgDB_dbconn =~ m/mysql:/i) { $cfgDB_dbtype = "MYSQL"; # } elsif ($cfgDB_dbconn =~ m/oracle:/i) { # $cfgDB_dbtype = "ORACLE"; } elsif ($cfgDB_dbconn =~ m/sqlite:/i) { $cfgDB_dbtype = "SQLITE"; } else { $cfgDB_dbtype = "unknown"; } sub cfgDB_svnId { return "# ".'$Id$' } 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; } sub cfgDB_Uuid{ my $fhem_dbh = cfgDB_Connect; my $uuid; $uuid = $fhem_dbh->selectrow_array('select lower(hex(randomblob(16)))') if($cfgDB_dbtype eq 'SQLITE'); $uuid = $fhem_dbh->selectrow_array('select uuid()') if($cfgDB_dbtype eq 'MYSQL'); $uuid = $fhem_dbh->selectrow_array('select uuid_generate_v4()') if($cfgDB_dbtype eq 'POSTGRESQL'); $fhem_dbh->disconnect(); return $uuid; } sub cfgDB_Init { ################################################## # Create non-existing database tables # Create default config entries if necessary # my $fhem_dbh = cfgDB_Connect; eval { $fhem_dbh->do("CREATE EXTENSION \"uuid-ossp\"") if($cfgDB_dbtype eq 'POSTGRESQL'); }; # create TABLE fhemversions ifnonexistent $fhem_dbh->do("CREATE TABLE IF NOT EXISTS fhemversions(VERSION INT, VERSIONUUID CHAR(50))"); # create TABLE fhemconfig if nonexistent $fhem_dbh->do("CREATE TABLE IF NOT EXISTS fhemconfig(COMMAND CHAR(32), DEVICE CHAR(32), P1 CHAR(50), P2 TEXT, VERSION INT, VERSIONUUID CHAR(50))"); # 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 my $uuid = cfgDB_Uuid; $fhem_dbh->do("INSERT INTO fhemversions values (0, '$uuid')"); cfgDB_InsertLine($fhem_dbh, $uuid, '#created by cfgDB_Init'); cfgDB_InsertLine($fhem_dbh, $uuid, 'attr global logfile ./log/fhem-%Y-%m-%d.log'); cfgDB_InsertLine($fhem_dbh, $uuid, 'attr global modpath .'); cfgDB_InsertLine($fhem_dbh, $uuid, 'attr global userattr devStateIcon devStateStyle icon sortby webCmd'); cfgDB_InsertLine($fhem_dbh, $uuid, 'attr global verbose 3'); cfgDB_InsertLine($fhem_dbh, $uuid, 'define telnetPort telnet 7072 global'); cfgDB_InsertLine($fhem_dbh, $uuid, 'define WEB FHEMWEB 8083 global'); cfgDB_InsertLine($fhem_dbh, $uuid, 'define Logfile FileLog ./log/fhem-%Y-%m-%d.log fakelog'); } # create TABLE fhemstate if nonexistent $fhem_dbh->do("CREATE TABLE IF NOT EXISTS fhemstate(stateString TEXT)"); # close database connection $fhem_dbh->commit(); $fhem_dbh->disconnect(); return; } sub cfgDB_Info { my $l = '--------------------'; $l .= $l; $l .= $l; $l .= "\n"; my $r = $l; $r .= " configDB Database Information\n"; $r .= $l; $r .= " dbconn: $cfgDB_dbconn\n"; $r .= " dbuser: $cfgDB_dbuser\n"; $r .= " dbpass: $cfgDB_dbpass\n"; $r .= " dbtype: $cfgDB_dbtype\n"; $r .= " Unknown dbmodel type in configuration file.\n" if $dbtype eq 'unknown'; $r .= " Only Mysql, Postgresql, SQLite are fully supported.\n" if $dbtype eq 'unknown'; $r .= $l; my $fhem_dbh = cfgDB_Connect; my ($sql, $sth, @line, $row); # read versions table statistics my $count; $count = $fhem_dbh->selectrow_array('SELECT count(*) FROM fhemconfig'); $r .= " fhemconfig: $count entries\n\n"; # 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()) { $row = " Ver $line[6] saved: $line[1] $line[2] $line[3] def: ". $fhem_dbh->selectrow_array("SELECT COUNT(*) from fhemconfig where COMMAND = 'define' and VERSIONUUID = '$line[5]'"); $row .= " attr: ". $fhem_dbh->selectrow_array("SELECT COUNT(*) from fhemconfig where COMMAND = 'attr' and VERSIONUUID = '$line[5]'"); $r .= "$row\n"; } $r .= $l; # read state table statistics $count = $fhem_dbh->selectrow_array('SELECT count(*) FROM fhemstate'); $r .= " fhemstate: $count entries saved: "; # read state table creation time $sth = $fhem_dbh->prepare( "SELECT * FROM fhemstate WHERE STATESTRING like '#%'" ); $sth->execute(); while ($row = $sth->fetchrow_array()) { (undef,$row) = split(/#/,$row); $r .= "$row\n"; } $r .= $l; $fhem_dbh->disconnect(); return $r; } 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 = cfgDB_Uuid; # 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], -1, $touuid); } $fhem_dbh->commit(); $fhem_dbh->disconnect(); # Inform user about restart or rereadcfg needed $ret = "Version 0 deleted.\n"; $ret .= "Version $version copied to version 0\n\n"; $ret .= "Please use rereadcfg or restart to activate configuration."; } 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; } sub cfgDB_Reorg(;$) { my ($lastversion) = @_; $lastversion = ($lastversion > 0) ? $lastversion : 3; Log3('configDB', 4, "DB 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->commit(); $fhem_dbh->disconnect(); return " Result after database reorg:\n".cfgDB_Info; } sub cfgDB_InsertLine($$$) { my ($fhem_dbh, $uuid, $line) = @_; my ($c,$d,$p1,$p2) = split(/ /, $line, 4); my $sth = $fhem_dbh->prepare('INSERT INTO fhemconfig values (?, ?, ?, ?, ?, ?)'); $sth->execute($c, $d, $p1, $p2, -1, $uuid); return; } sub cfgDB_Execute($@) { my ($cl, @dbconfig) = @_; my $ret; foreach (@dbconfig){ my $l = $_; $l =~ s/[\r\n]//g; $ret .= AnalyzeCommandChain($cl, $l); } return $ret if($ret); return undef; } sub cfgDB_SaveCfg { my (%devByNr, @rowList); 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; } if($d ne "global") { my $def = $defs{$d}{DEF}; if(defined($def)) { $def =~ s/;/;;/g; $def =~ s/\n/\\\n/g; } else { $dev = ""; } push @rowList, "define $d $defs{$d}{TYPE} $def"; } foreach my $a (sort keys %{$attr{$d}}) { next if($d eq "global" && ($a eq "configfile" || $a eq "version")); my $val = $attr{$d}{$a}; $val =~ s/;/;;/g; $val =~ s/\n/\\\n/g; push @rowList, "attr $d $a $val"; } } # Insert @rowList into database table my $fhem_dbh = cfgDB_Connect; my $uuid = cfgDB_Rotate($fhem_dbh); $t = localtime; $out = "#created $t"; push @rowList, $out; foreach (@rowList) { cfgDB_InsertLine($fhem_dbh, $uuid, $_); } $fhem_dbh->commit(); $fhem_dbh->disconnect(); return 'configDB saved.'; } sub cfgDB_SaveState { my ($out,$val,$r,$rd,$t,@rowList); $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 =~ s/;/;;/g; $val =~ s/\n/\\\n/g; $out = "setstate $d $val"; push @rowList, $out; } $r = $defs{$d}{READINGS}; if($r) { foreach my $c (sort keys %{$r}) { $rd = $r->{$c}; if(!defined($rd->{TIME})) { Log3(undef, 4, "WriteStatefile $d $c: Missing TIME, using current time"); $rd->{TIME} = TimeNow(); } if(!defined($rd->{VAL})) { Log3(undef, 4, "WriteStatefile $d $c: Missing VAL, setting it to 0"); $rd->{VAL} = 0; } $val = $rd->{VAL}; $val =~ s/;/;;/g; $val =~ s/\n/\\\n/g; $out = "setstate $d $rd->{TIME} $c $val"; push @rowList, $out; } } } my $fhem_dbh = cfgDB_Connect; $fhem_dbh->do("DELETE FROM fhemstate"); my $sth = $fhem_dbh->prepare('INSERT INTO fhemstate values ( ? )'); foreach (@rowList) { $sth->execute( $_ ); } $fhem_dbh->commit(); $fhem_dbh->disconnect(); return; } sub cfgDB_ReadCfg(@) { my (@dbconfig) = @_; my $fhem_dbh = cfgDB_Connect; my ($sth, @line, $row); # using a join would be much nicer, but does not work due to sort of join's result my $uuid = $fhem_dbh->selectrow_array('SELECT versionuuid FROM fhemversions WHERE version = 0'); $sth = $fhem_dbh->prepare( "SELECT * FROM fhemconfig WHERE versionuuid = '$uuid'" ); $sth->execute(); while (@line = $sth->fetchrow_array()) { $row = "$line[0] $line[1] $line[2] $line[3]"; push @dbconfig, $row; } $fhem_dbh->disconnect(); return @dbconfig; } sub cfgDB_ReadState(@) { my (@dbconfig) = @_; my $fhem_dbh = cfgDB_Connect; my ($sth, $row); $sth = $fhem_dbh->prepare( "SELECT * FROM fhemstate" ); $sth->execute(); while ($row = $sth->fetchrow_array()) { push @dbconfig, $row; } $fhem_dbh->disconnect(); return @dbconfig; } sub cfgDB_GlobalAttr { my ($sth, @line, $row, @dbconfig); my $fhem_dbh = cfgDB_Connect; $sth = $fhem_dbh->prepare( "SELECT * FROM fhemconfig WHERE DEVICE = 'global'" ); $sth->execute(); while (@line = $sth->fetchrow_array()) { $row = "$line[0] $line[1] $line[2] $line[3]"; $line[3] =~ s/#.*//; $line[3] =~ s/ .*$//; $attr{global}{$line[2]} = $line[3]; } $fhem_dbh->disconnect(); return; } sub cfgDB_Rotate($) { my ($fhem_dbh) = @_; my $uuid = cfgDB_Uuid; # $fhem_dbh->do("UPDATE fhemconfig SET VERSION = VERSION+1"); $fhem_dbh->do("UPDATE fhemversions SET VERSION = VERSION+1"); $fhem_dbh->do("INSERT INTO fhemversions values (0, '$uuid')"); return $uuid; } sub cfgDB_ReadAll($){ my ($cl) = @_; my $ret; # add Config Rows to commandfile my @dbconfig = cfgDB_ReadCfg(@dbconfig); # add State Rows to commandfile @dbconfig = cfgDB_ReadState(@dbconfig); # AnalyzeCommandChain for all entries $ret .= cfgDB_Execute($cl, @dbconfig); return $ret if($ret); return undef; } sub cfgDB_Migrate { Log3('configDB',4,'Starting migration.'); Log3('configDB',4,'Processing: cfgDB_Init.'); cfgDB_Init; Log3('configDB',4,'Processing: cfgDB_SaveCfg.'); cfgDB_SaveCfg; Log3('configDB',4,'Processing: cfgDB_SaveState.'); cfgDB_SaveState; Log3('configDB',4,'Migration finished.'); return " Result after migration:\n".cfgDB_Info; } sub cfgDB_List(;$$) { my ($search,$searchversion) = @_; $search = $search ? $search : "%"; $searchversion = $searchversion ? $searchversion : 0; my $fhem_dbh = cfgDB_Connect; my ($sql, $sth, @line, $row, @result, $ret); $sql = "SELECT command, device, p1, p2 FROM fhemconfig as c join fhemversions as v ON v.versionuuid=c.versionuuid ". "WHERE v.version = '$searchversion' AND command not like '#create%' AND device like '$search%' ORDER BY lower(device),command DESC"; $sth = $fhem_dbh->prepare( $sql); $sth->execute(); push @result, "search result for device: $search in version: $searchversion"; push @result, "--------------------------------------------------------------------------------"; while (@line = $sth->fetchrow_array()) { $row = "$line[0] $line[1] $line[2] $line[3]"; push @result, "$row"; } $fhem_dbh->disconnect(); $ret = join("\n", @result); return $ret; } sub cfgDB_Diff($$){ my ($search,$searchversion) = @_; eval {use Text::Diff}; return "error: Please install Text::Diff!" if($@); my ($sql, $sth, @line, $row, @result, $ret, $v0, $v1); my $fhem_dbh = cfgDB_Connect; $sql = "SELECT command, device, p1, p2 FROM fhemconfig as c join fhemversions as v ON v.versionuuid=c.versionuuid ". "WHERE v.version = 0 AND device = '$search' ORDER BY command DESC"; $sth = $fhem_dbh->prepare( $sql); $sth->execute(); while (@line = $sth->fetchrow_array()) { $v0 .= "$line[0] $line[1] $line[2] $line[3]\n"; } $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"; $sth = $fhem_dbh->prepare( $sql); $sth->execute(); while (@line = $sth->fetchrow_array()) { $v1 .= "$line[0] $line[1] $line[2] $line[3]\n"; } $fhem_dbh->disconnect(); $ret = "compare device: $search in current version 0 (left) to version: $searchversion (right)\n"; $ret .= diff \$v0, \$v1, { STYLE => "Table" }; #, \%options; return $ret; } 1; =pod =begin html

configDB

=end html =begin html_DE

configDB

=end html_DE =cut