configDB.pm 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338
  1. # $Id: configDB.pm 16235 2018-02-20 21:53:26Z betateilchen $
  2. =for comment
  3. ##############################################################################
  4. #
  5. # configDB.pm
  6. #
  7. # A fhem library to enable configuration from sql database
  8. # instead of plain text file, e.g. fhem.cfg
  9. #
  10. # READ COMMANDREF DOCUMENTATION FOR CORRECT USE!
  11. #
  12. # Copyright: betateilchen ®
  13. #
  14. # This file is part of fhem.
  15. #
  16. # Fhem is free software: you can redistribute it and/or modify
  17. # it under the terms of the GNU General Public License as published by
  18. # the Free Software Foundation, either version 2 of the License, or
  19. # (at your option) any later version.
  20. #
  21. # Fhem is distributed in the hope that it will be useful,
  22. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  23. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  24. # GNU General Public License for more details.
  25. #
  26. # You should have received a copy of the GNU General Public License
  27. # along with fhem. If not, see <http://www.gnu.org/licenses/>.
  28. #
  29. ##############################################################################
  30. #
  31. # ChangeLog
  32. #
  33. # 2014-03-01 - SVN 5080 - initial release of interface inside fhem.pl
  34. # - initial release of configDB.pm
  35. #
  36. # 2014-03-02 - added template files for sqlite in contrib/configDB
  37. # - updated commandref (EN) documentation
  38. # - added commandref (DE) documentation
  39. #
  40. # 2014-03-03 - changed performance optimized by using version uuid table
  41. # - updated commandref docu for migration
  42. # - added cfgDB_svnId for fhem.pl CommandVersion
  43. # - added cfgDB_List to show device info from database
  44. # - updated commandref docu for cfgDB_List
  45. #
  46. # 2014-03-06 - added cfgDB_Diff to compare device in two versions
  47. #
  48. # 2014-03-07 - changed optimized cfgDB_Diff
  49. # restructured libraray internally
  50. # improved source code documentation
  51. #
  52. # 2014-03-20 - added export/import
  53. # 2014-04-01 - removed export/import due to not working properly
  54. #
  55. # 2014-04-03 - fixed global attributes not read from version 0
  56. #
  57. # 2014-04-18 - added commands fileimport, fileexport
  58. # 2014-04-19 - added commands filelist, filedelete
  59. # interface cfgDB_Readfile for interaction
  60. # with other modules
  61. #
  62. # 2014-04-21 - added interface functions for FHEMWEB and fhem.pl
  63. # to show files in "Edit files" and use them
  64. # with CommandReload() mechanism
  65. #
  66. # modified _cfgDB_Info to show number of files in db
  67. #
  68. # 2014-04-23 - added command fileshow, filemove
  69. #
  70. # 2014-04-26 - added migration to generic file handling
  71. # fixed problem on migration of multiline DEFs
  72. #
  73. # 2014-04-27 - added new functions for binfile handling
  74. #
  75. # 2014-05-11 - removed command binfileimport
  76. # changed store all files as binary
  77. # added _cfgDB_Move to move all files from text
  78. # to binary filesave on first load of configDB
  79. #
  80. # 2014-05-12 - added sorted write & read for config data
  81. #
  82. # 2014-05-15 - fixed handling of multiline defs
  83. #
  84. # 2014-05-20 - removed no longer needed functions for file handling
  85. # changed code improvement; use strict; use warnings;
  86. #
  87. # 2014-08-22 - added automatic fileimport during migration
  88. #
  89. # 2014-09-30 - added support for device based userattr
  90. #
  91. # 2015-01-12 - changed use fhem function createUniqueId()
  92. # instead of database calls
  93. #
  94. # 2015-01-15 - changed remove 99_Utils.pm from filelist
  95. #
  96. # 2015-01-17 - added configdb diff all current
  97. # shows diff table between version 0
  98. # and currently running version (in memory)
  99. #
  100. # 2015-01-23 - changed attribute handling for internal configDB attrs
  101. #
  102. # 2015-01-23 - added FileRead() caching - experimental
  103. #
  104. # 2015-10-14 - changed search conditions use ESCAPE, forum #42190
  105. #
  106. # 2016-03-19 - changed use modpath, forum #51036
  107. #
  108. # 2016-03-26 - added log entry for search (verbose=5)
  109. #
  110. # 2016-05-22 - added configdb dump (for sqlite)
  111. #
  112. # 2016-05-28 - added configdb dump (for mysql)
  113. #
  114. # 2016-05-29 - changed improve support for postgresql (tnx to Matze)
  115. # added configdb dump (for postgresql)
  116. #
  117. # 2016-07-03 - added support for multiple hosts (experimental)
  118. # 2016-07-04 - fixed improve config file read
  119. # 2016-07-07 - bugfix select configuration
  120. #
  121. # 2017-03-24 - added use index on fhemconfig (only sqlite)
  122. #
  123. # 2017-07-17 - changed store files base64 encoded
  124. #
  125. # 2017-08-31 - changed improve table_info for migration check
  126. #
  127. # 2018-02-17 - changed remove experimenatal cache functions
  128. # 2018-02-18 - changed move dump processing to backend
  129. #
  130. ##############################################################################
  131. =cut
  132. use strict;
  133. use warnings;
  134. use Text::Diff;
  135. use DBI;
  136. use Sys::Hostname;
  137. use MIME::Base64;
  138. ##################################################
  139. # Forward declarations for functions in fhem.pl
  140. #
  141. sub AnalyzeCommandChain($$;$);
  142. sub Log($$);
  143. sub Log3($$$);
  144. sub createUniqueId();
  145. ##################################################
  146. # Forward declarations inside this library
  147. #
  148. sub cfgDB_AttrRead($);
  149. sub cfgDB_Init();
  150. sub cfgDB_FileRead($);
  151. sub cfgDB_FileUpdate($);
  152. sub cfgDB_Fileversion($$);
  153. sub cfgDB_FileWrite($@);
  154. sub cfgDB_FW_fileList($$@);
  155. sub cfgDB_Read99();
  156. sub cfgDB_ReadAll($);
  157. sub cfgDB_SaveCfg(;$);
  158. sub cfgDB_SaveState();
  159. sub cfgDB_svnId();
  160. sub _cfgDB_binFileimport($$;$);
  161. sub _cfgDB_Connect();
  162. sub _cfgDB_DeleteTemp();
  163. sub _cfgDB_Diff($$);
  164. sub __cfgDB_Diff($$$$);
  165. sub _cfgDB_InsertLine($$$$);
  166. sub _cfgDB_Execute($@);
  167. sub _cfgDB_Filedelete($);
  168. sub _cfgDB_Fileexport($;$);
  169. sub _cfgDB_Filelist(;$);
  170. sub _cfgDB_Info($);
  171. sub _cfgDB_Migrate();
  172. sub _cfgDB_ReadCfg(@);
  173. sub _cfgDB_ReadState(@);
  174. sub _cfgDB_Recover($);
  175. sub _cfgDB_Reorg(;$$);
  176. sub _cfgDB_Rotate($$);
  177. sub _cfgDB_Search($$;$);
  178. sub _cfgDB_Uuid();
  179. sub _cfgDB_table_exists($$);
  180. sub _cfgDB_dump($);
  181. ##################################################
  182. # Read configuration file for DB connection
  183. #
  184. if(!open(CONFIG, 'configDB.conf')) {
  185. Log3('configDB', 1, 'Cannot open database configuration file configDB.conf');
  186. return 0;
  187. }
  188. my @config;
  189. while (<CONFIG>){
  190. my $line = $_;
  191. $line =~ s/^\s+|\s+$//g; # remove whitespaces etc.
  192. $line =~ s/;$/;;/; # duplicate ; at end-of-line
  193. push (@config,$line) if($line !~ m/^#/ && length($line) > 0);
  194. }
  195. close CONFIG;
  196. use vars qw(%configDB);
  197. my %dbconfig;
  198. my $configs = join("",@config);
  199. my @configs = split(/;;/,$configs);
  200. my $count = @configs;
  201. my $fhemhost = hostname;
  202. if ($count > 1) {
  203. foreach my $c (@configs) {
  204. next unless $c =~ m/^%dbconfig.*/;
  205. $dbconfig{fhemhost} = "";
  206. eval $c;
  207. last if ($dbconfig{fhemhost} eq $fhemhost);
  208. }
  209. eval $configs[0] if ($dbconfig{fhemhost} eq "");
  210. } else {
  211. eval $configs[0];
  212. }
  213. my $cfgDB_dbconn = $dbconfig{connection};
  214. my $cfgDB_dbuser = $dbconfig{user};
  215. my $cfgDB_dbpass = $dbconfig{password};
  216. my $cfgDB_dbtype;
  217. my $cfgDB_filename;
  218. %dbconfig = ();
  219. @config = ();
  220. $configs = undef;
  221. $count = undef;
  222. if($cfgDB_dbconn =~ m/pg:/i) {
  223. $cfgDB_dbtype ="POSTGRESQL";
  224. } elsif ($cfgDB_dbconn =~ m/mysql:/i) {
  225. $cfgDB_dbtype = "MYSQL";
  226. } elsif ($cfgDB_dbconn =~ m/sqlite:/i) {
  227. $cfgDB_dbtype = "SQLITE";
  228. (undef,$cfgDB_filename) = split(/=/,$cfgDB_dbconn);
  229. } else {
  230. $cfgDB_dbtype = "unknown";
  231. }
  232. $configDB{attr}{nostate} = 1 if($ENV{'cfgDB_nostate'});
  233. $configDB{attr}{rescue} = 1 if($ENV{'cfgDB_rescue'});
  234. $configDB{attr}{loadversion} = $ENV{'cfgDB_version'} ? $ENV{'cfgDB_version'} : 0;
  235. ##################################################
  236. # Basic functions needed for DB configuration
  237. # directly called from fhem.pl
  238. #
  239. # initialize database, create tables if necessary
  240. sub cfgDB_Init() {
  241. ##################################################
  242. # Create non-existing database tables
  243. # Create default config entries if necessary
  244. #
  245. my $fhem_dbh = _cfgDB_Connect;
  246. # create TABLE fhemversions ifnonexistent
  247. $fhem_dbh->do("CREATE TABLE IF NOT EXISTS fhemversions(VERSION INT, VERSIONUUID CHAR(50))");
  248. # create TABLE fhemconfig if nonexistent
  249. $fhem_dbh->do("CREATE TABLE IF NOT EXISTS fhemconfig(COMMAND VARCHAR(32), DEVICE VARCHAR(64), P1 VARCHAR(50), P2 TEXT, VERSION INT, VERSIONUUID CHAR(50))");
  250. # create INDEX on fhemconfig if nonexistent (only if SQLITE)
  251. $fhem_dbh->do("CREATE INDEX IF NOT EXISTS config_idx on 'fhemconfig' (versionuuid,version)")
  252. if($cfgDB_dbtype eq "SQLITE");
  253. # check TABLE fhemconfig already populated
  254. my $count = $fhem_dbh->selectrow_array('SELECT count(*) FROM fhemconfig');
  255. if($count < 1) {
  256. # insert default entries to get fhem running
  257. $fhem_dbh->commit();
  258. my $uuid = _cfgDB_Uuid;
  259. $fhem_dbh->do("INSERT INTO fhemversions values (0, '$uuid')");
  260. _cfgDB_InsertLine($fhem_dbh, $uuid, '#created by cfgDB_Init',0);
  261. _cfgDB_InsertLine($fhem_dbh, $uuid, 'attr global logdir ./log',1);
  262. _cfgDB_InsertLine($fhem_dbh, $uuid, 'attr global logfile %L/fhem-%Y-%m-%d.log',1);
  263. _cfgDB_InsertLine($fhem_dbh, $uuid, 'attr global modpath .',2);
  264. _cfgDB_InsertLine($fhem_dbh, $uuid, 'attr global userattr devStateIcon devStateStyle icon sortby webCmd',3);
  265. _cfgDB_InsertLine($fhem_dbh, $uuid, 'attr global verbose 3',4);
  266. _cfgDB_InsertLine($fhem_dbh, $uuid, 'define telnetPort telnet 7072 global',5);
  267. _cfgDB_InsertLine($fhem_dbh, $uuid, 'define web FHEMWEB 8083 global',6);
  268. _cfgDB_InsertLine($fhem_dbh, $uuid, 'define Logfile FileLog %L/fhem-%Y-%m-%d.log fakelog',7);
  269. }
  270. # create TABLE fhemstate if nonexistent
  271. $fhem_dbh->do("CREATE TABLE IF NOT EXISTS fhemstate(stateString TEXT)");
  272. # create TABLE fhemb64filesave if nonexistent
  273. if($cfgDB_dbtype eq "MYSQL") {
  274. $fhem_dbh->do("CREATE TABLE IF NOT EXISTS fhemb64filesave(filename TEXT, content MEDIUMBLOB)");
  275. } elsif ($cfgDB_dbtype eq "POSTGRESQL") {
  276. $fhem_dbh->do("CREATE TABLE IF NOT EXISTS fhemb64filesave(filename TEXT, content bytea)");
  277. } else {
  278. $fhem_dbh->do("CREATE TABLE IF NOT EXISTS fhemb64filesave(filename TEXT, content BLOB)");
  279. }
  280. ### migrate fhembinfilesave to fhemb64filesave
  281. # # check: fhembinfilesave exists?
  282. # my $sth_test = $fhem_dbh->table_info(undef, 'public', "fhembinfilesave", 'TABLE');
  283. # $sth_test->execute;
  284. # my @info = $sth_test->fetchrow_array;
  285. # my $exists = scalar @info;
  286. if (_cfgDB_table_exists($fhem_dbh,'fhembinfilesave')) {
  287. # $sth_test->finish();
  288. # check: any files for migratione?
  289. $count = undef;
  290. $count = $fhem_dbh->selectrow_array('SELECT count(*) FROM fhembinfilesave');
  291. if ($count > 0) {
  292. printf "need to migrate $count files to base64\n";
  293. my @toMigrate;
  294. my $sth = $fhem_dbh->prepare( "SELECT filename FROM fhembinfilesave" );
  295. $sth->execute();
  296. while (my $file = $sth->fetchrow_array()) {
  297. printf "migrating $file : ";
  298. # 1. read from fhembinfilesave
  299. my $sth2 = $fhem_dbh->prepare( "SELECT content from fhembinfilesave where filename = '$file'" );
  300. $sth2->execute();
  301. my $content = $sth2->fetchrow_array();
  302. $sth2->finish();
  303. # 2. encode and write to fhemb64filesave
  304. $fhem_dbh->do("delete from fhemb64filesave where filename = '$file'");
  305. $sth2 = $fhem_dbh->prepare('INSERT INTO fhemb64filesave values (?, ?)');
  306. $sth2->execute($file,encode_base64($content));
  307. $sth2->finish();
  308. # 3. delete from fhembinfilesave
  309. $fhem_dbh->do("delete from fhembinfilesave where filename = '$file'");
  310. printf "done.\n";
  311. }
  312. }
  313. # 4. drop table fhembinfilesave
  314. $fhem_dbh->do("drop table fhembinfilesave");
  315. }
  316. ### end migration base64
  317. # close database connection
  318. $fhem_dbh->commit();
  319. $fhem_dbh->disconnect();
  320. return;
  321. }
  322. sub _cfgDB_table_exists($$) {
  323. my ($dbh,$table) = @_;
  324. printf "looking for table: $table \n";
  325. my @tables = $dbh->tables('','','','TABLE');
  326. if (@tables) {
  327. printf "testing: #1\n";
  328. for (@tables) {
  329. next unless $_;
  330. printf "found: $_"."\n";
  331. return 1 if $_ =~ $table;
  332. }
  333. } else {
  334. printf "testing: #2\n";
  335. eval {
  336. local $dbh->{PrintError} = 0;
  337. local $dbh->{RaiseError} = 1;
  338. $dbh->do(qq{SELECT * FROM $table WHERE 1 = 0 });
  339. };
  340. return 1 unless $@;
  341. }
  342. printf "table not found\n";
  343. return 0;
  344. }
  345. # read attributes
  346. sub cfgDB_AttrRead($) {
  347. my ($readSpec) = @_;
  348. my ($row, $sql, @line, @rets);
  349. my $fhem_dbh = _cfgDB_Connect;
  350. my $uuid = $fhem_dbh->selectrow_array('SELECT versionuuid FROM fhemversions WHERE version = 0');
  351. $sql = "SELECT * FROM fhemconfig WHERE COMMAND = 'attr' AND DEVICE = '$readSpec' AND VERSIONUUID = '$uuid'";
  352. $sql = "SELECT * FROM fhemconfig WHERE COMMAND = 'attr' AND (DEVICE = 'global' OR DEVICE = 'configdb') and VERSIONUUID = '$uuid'"
  353. if($readSpec eq 'global');
  354. my $sth = $fhem_dbh->prepare( $sql );
  355. $sth->execute();
  356. while (@line = $sth->fetchrow_array()) {
  357. if($line[1] eq 'configdb') {
  358. $configDB{attr}{$line[2]} = $line[3];
  359. # $attr{configdb}{$line[2]} = $line[3];
  360. } else {
  361. push @rets, "attr $line[1] $line[2] $line[3]";
  362. }
  363. }
  364. $fhem_dbh->disconnect();
  365. return @rets;
  366. }
  367. # generic file functions called from fhem.pl
  368. sub cfgDB_FileRead($) {
  369. my ($filename) = @_;
  370. # if ($configDB{cache}{$filename} && $configDB{attr}{useCache}) {
  371. # Log3(undef, 4, "configDB serving from cache: $filename");
  372. # return (undef,split(/\n/,$configDB{cache}{$filename}));
  373. # }
  374. Log3(undef, 4, "configDB reading file: $filename");
  375. my ($err, @ret, $counter);
  376. my $fhem_dbh = _cfgDB_Connect;
  377. my $sth = $fhem_dbh->prepare( "SELECT content FROM fhemb64filesave WHERE filename LIKE '$filename'" );
  378. $sth->execute();
  379. my $blobContent = $sth->fetchrow_array();
  380. $sth->finish();
  381. $fhem_dbh->disconnect();
  382. $blobContent = decode_base64($blobContent) if ($blobContent);
  383. $counter = length($blobContent);
  384. if($counter) {
  385. # if ($configDB{attr}{useCache}) {
  386. # Log3(undef,4,"configDB caching: $filename");
  387. # $configDB{cache}{$filename} = $blobContent;
  388. # }
  389. @ret = split(/\n/,$blobContent);
  390. $err = "";
  391. } else {
  392. @ret = undef;
  393. $err = "Error on reading $filename from database!";
  394. }
  395. return ($err, @ret);
  396. }
  397. sub cfgDB_FileWrite($@) {
  398. my ($filename,@content) = @_;
  399. # if ($configDB{attr}{useCache}) {
  400. # Log3(undef,4,"configDB delete from cache: $filename");
  401. # $configDB{cache}{$filename} = undef;
  402. # }
  403. Log3(undef, 4, "configDB writing file: $filename");
  404. my $fhem_dbh = _cfgDB_Connect;
  405. $fhem_dbh->do("delete from fhemb64filesave where filename = '$filename'");
  406. my $sth = $fhem_dbh->prepare('INSERT INTO fhemb64filesave values (?, ?)');
  407. $sth->execute($filename,encode_base64(join("\n", @content)));
  408. $sth->finish();
  409. $fhem_dbh->commit();
  410. $fhem_dbh->disconnect();
  411. return;
  412. }
  413. sub cfgDB_FileUpdate($) {
  414. my ($filename) = @_;
  415. my $fhem_dbh = _cfgDB_Connect;
  416. my $id = $fhem_dbh->selectrow_array("SELECT filename from fhemb64filesave where filename = '$filename'");
  417. $fhem_dbh->disconnect();
  418. if($id) {
  419. my $filesize = -s $filename;
  420. _cfgDB_binFileimport($filename,$filesize,1) if ($id) ;
  421. Log(5, "file $filename updated in configDB");
  422. }
  423. return;
  424. }
  425. # read and execute fhemconfig and fhemstate
  426. sub cfgDB_ReadAll($) {
  427. my ($cl) = @_;
  428. my ($ret, @dbconfig);
  429. if (defined($configDB{attr}{rescue}) && ($configDB{attr}{rescue} == 1)) {
  430. Log (0, 'configDB starting in rescue mode!');
  431. push (@dbconfig, 'attr global modpath .');
  432. push (@dbconfig, 'attr global verbose 3');
  433. push (@dbconfig, 'define telnetPort telnet 7072 global');
  434. push (@dbconfig, 'define WEB FHEMWEB 8083 global');
  435. push (@dbconfig, 'define Logfile FileLog ./log/fhem-%Y-%m-%d.log fakelog');
  436. } else {
  437. # add Config Rows to commandfile
  438. @dbconfig = _cfgDB_ReadCfg(@dbconfig);
  439. # add State Rows to commandfile
  440. @dbconfig = _cfgDB_ReadState(@dbconfig) unless $configDB{attr}{nostate};
  441. }
  442. # AnalyzeCommandChain for all entries
  443. $ret = _cfgDB_Execute($cl, @dbconfig);
  444. return $ret if($ret);
  445. return undef;
  446. }
  447. # save running configuration to version 0
  448. sub cfgDB_SaveCfg(;$) {
  449. my ($internal) = shift;
  450. $internal = defined($internal) ? $internal : 0;
  451. my (%devByNr, @rowList, %comments, $t, $out);
  452. map { $devByNr{$defs{$_}{NR}} = $_ } keys %defs;
  453. for(my $i = 0; $i < $devcount; $i++) {
  454. my ($h, $d);
  455. if($comments{$i}) {
  456. $h = $comments{$i};
  457. } else {
  458. $d = $devByNr{$i};
  459. next if(!defined($d) ||
  460. $defs{$d}{TEMPORARY} || # e.g. WEBPGM connections
  461. $defs{$d}{VOLATILE}); # e.g at, will be saved to the statefile
  462. $h = $defs{$d};
  463. }
  464. if(!defined($d)) {
  465. push @rowList, $h->{TEXT};
  466. next;
  467. }
  468. if($d ne "global") {
  469. my $def = $defs{$d}{DEF};
  470. if(defined($def)) {
  471. $def =~ s/;/;;/g;
  472. $def =~ s/\n/\\\n/g;
  473. } else {
  474. $def = "";
  475. }
  476. push @rowList, "define $d $defs{$d}{TYPE} $def";
  477. }
  478. foreach my $a (sort {
  479. return -1 if($a eq "userattr"); # userattr must be first
  480. return 1 if($b eq "userattr");
  481. return $a cmp $b;
  482. } keys %{$attr{$d}}) {
  483. next if($d eq "global" &&
  484. ($a eq "configfile" || $a eq "version"));
  485. my $val = $attr{$d}{$a};
  486. $val =~ s/;/;;/g;
  487. push @rowList, "attr $d $a $val";
  488. }
  489. }
  490. foreach my $a (sort keys %{$configDB{attr}}) {
  491. # foreach my $a (sort keys %{$attr{configdb}}) {
  492. my $val = $configDB{attr}{$a};
  493. next unless $val;
  494. $val =~ s/;/;;/g;
  495. push @rowList, "attr configdb $a $val";
  496. }
  497. # Insert @rowList into database table
  498. my $fhem_dbh = _cfgDB_Connect;
  499. my $uuid = _cfgDB_Rotate($fhem_dbh,$internal);
  500. $t = localtime;
  501. $out = "#created $t";
  502. push @rowList, $out;
  503. my $counter = 0;
  504. foreach (@rowList) {
  505. _cfgDB_InsertLine($fhem_dbh, $uuid, $_, $counter);
  506. $counter++;
  507. }
  508. $fhem_dbh->commit();
  509. $fhem_dbh->disconnect();
  510. my $maxVersions = $configDB{attr}{maxversions};
  511. $maxVersions = ($maxVersions) ? $maxVersions : 0;
  512. _cfgDB_Reorg($maxVersions,1) if($maxVersions && $internal != -1);
  513. return 'configDB saved.';
  514. }
  515. # save statefile
  516. sub cfgDB_SaveState() {
  517. my ($out,$val,$r,$rd,$t,@rowList);
  518. $t = localtime;
  519. $out = "#$t";
  520. push @rowList, $out;
  521. foreach my $d (sort keys %defs) {
  522. next if($defs{$d}{TEMPORARY});
  523. if($defs{$d}{VOLATILE}) {
  524. $out = "define $d $defs{$d}{TYPE} $defs{$d}{DEF}";
  525. push @rowList, $out;
  526. }
  527. $val = $defs{$d}{STATE};
  528. if(defined($val) &&
  529. $val ne "unknown" &&
  530. $val ne "Initialized" &&
  531. $val ne "" &&
  532. $val ne "???") {
  533. $val =~ s/;/;;/g;
  534. $val =~ s/\n/\\\n/g;
  535. $out = "setstate $d $val";
  536. push @rowList, $out;
  537. }
  538. $r = $defs{$d}{READINGS};
  539. if($r) {
  540. foreach my $c (sort keys %{$r}) {
  541. $rd = $r->{$c};
  542. if(!defined($rd->{TIME})) {
  543. Log3(undef, 4, "WriteStatefile $d $c: Missing TIME, using current time");
  544. $rd->{TIME} = TimeNow();
  545. }
  546. if(!defined($rd->{VAL})) {
  547. Log3(undef, 4, "WriteStatefile $d $c: Missing VAL, setting it to 0");
  548. $rd->{VAL} = 0;
  549. }
  550. $val = $rd->{VAL};
  551. $val =~ s/;/;;/g;
  552. $val =~ s/\n/\\\n/g;
  553. $out = "setstate $d $rd->{TIME} $c $val";
  554. push @rowList, $out;
  555. }
  556. }
  557. }
  558. my $fhem_dbh = _cfgDB_Connect;
  559. $fhem_dbh->do("DELETE FROM fhemstate");
  560. my $sth = $fhem_dbh->prepare('INSERT INTO fhemstate values ( ? )');
  561. foreach (@rowList) { $sth->execute( $_ ); }
  562. $fhem_dbh->commit();
  563. $fhem_dbh->disconnect();
  564. return;
  565. }
  566. # import existing files during migration
  567. sub cfgDB_MigrationImport() {
  568. my ($ret, $filename, @files, @def);
  569. my $modpath = AttrVal("global","modpath",".");
  570. # find eventTypes file
  571. $filename = '';
  572. @def = '';
  573. @def = _cfgDB_findDef('TYPE=eventTypes');
  574. foreach $filename (@def) {
  575. next unless $filename;
  576. push @files, $filename;
  577. }
  578. # import templateDB.gplot
  579. $filename = "$modpath/www/gplot/template.gplot";
  580. push @files, $filename;
  581. $filename = "$modpath/www/gplot/templateDB.gplot";
  582. push @files, $filename;
  583. # import template.layout
  584. $filename = "$modpath/FHEM/template.layout";
  585. push @files, $filename;
  586. # find used gplot files
  587. $filename ='';
  588. @def = '';
  589. @def = _cfgDB_findDef('TYPE=SVG','GPLOTFILE');
  590. foreach $filename (@def) {
  591. next unless $filename;
  592. push @files, "$modpath/www/gplot/".$filename.".gplot";
  593. }
  594. # find DbLog configs
  595. $filename ='';
  596. @def = '';
  597. @def = _cfgDB_findDef('TYPE=DbLog','CONFIGURATION');
  598. foreach $filename (@def) {
  599. next unless $filename;
  600. push @files, $filename;
  601. }
  602. # find RSS layouts
  603. $filename ='';
  604. @def = '';
  605. @def = _cfgDB_findDef('TYPE=RSS','LAYOUTFILE');
  606. foreach $filename (@def) {
  607. next unless $filename;
  608. push @files, $filename;
  609. }
  610. # find InfoPanel layouts
  611. $filename ='';
  612. @def = '';
  613. @def = _cfgDB_findDef('TYPE=InfoPanel','LAYOUTFILE');
  614. foreach $filename (@def) {
  615. next unless $filename;
  616. push @files, $filename;
  617. }
  618. # find holiday files
  619. $filename ='';
  620. @def = '';
  621. @def = _cfgDB_findDef('TYPE=holiday','NAME');
  622. foreach $filename (@def) {
  623. next unless $filename;
  624. if(defined($defs{$filename}{HOLIDAYFILE})) {
  625. push @files, $defs{$filename}{HOLIDAYFILE};
  626. } else {
  627. push @files, "$modpath/FHEM/".$filename.".holiday";
  628. }
  629. }
  630. # import uniqueID file
  631. $filename = "$modpath/FHEM/FhemUtils/uniqueID";
  632. push @files,$filename if (-e $filename);
  633. # do the import
  634. $filename = '';
  635. foreach $filename (@files) {
  636. if ( -r $filename ) {
  637. my $filesize = -s $filename;
  638. _cfgDB_binFileimport($filename,$filesize);
  639. $ret .= "importing: $filename\n";
  640. }
  641. }
  642. return $ret;
  643. }
  644. # return SVN Id, called by fhem's CommandVersion
  645. sub cfgDB_svnId() {
  646. return "# ".'$Id: configDB.pm 16235 2018-02-20 21:53:26Z betateilchen $'
  647. }
  648. # return filelist depending on directory and regexp
  649. sub cfgDB_FW_fileList($$@) {
  650. my ($dir,$re,@ret) = @_;
  651. my @files = split(/\n/, _cfgDB_Filelist('notitle'));
  652. foreach my $f (@files) {
  653. next if( $f !~ m/^$dir/ );
  654. $f =~ s,$dir\/,,;
  655. next if($f !~ m,^$re$, || $f eq '99_Utils.pm');
  656. push @ret, "$f.configDB";
  657. }
  658. return @ret;
  659. }
  660. # read filelist containing 99_ files in database
  661. sub cfgDB_Read99() {
  662. my $ret = "";
  663. my $fhem_dbh = _cfgDB_Connect;
  664. my $sth = $fhem_dbh->prepare( "SELECT filename FROM fhemb64filesave WHERE filename like '%/99_%.pm' group by filename" );
  665. $sth->execute();
  666. while (my $line = $sth->fetchrow_array()) {
  667. $line =~ m,^(.*)/([^/]*)$,; # Split into dir and file
  668. $ret .= "$2,"; #
  669. }
  670. $sth->finish();
  671. $fhem_dbh->disconnect();
  672. $ret =~ s/,$//;
  673. return $ret;
  674. }
  675. # return SVN Id from file stored in database
  676. sub cfgDB_Fileversion($$) {
  677. my ($file,$ret) = @_;
  678. $ret = "No Id found for $file";
  679. my ($err,@in) = cfgDB_FileRead($file);
  680. foreach(@in){ $ret = $_ if($_ =~ m/# \$Id:/); }
  681. return $ret;
  682. }
  683. ##################################################
  684. # Basic functions needed for DB configuration
  685. # but not called from fhem.pl directly
  686. #
  687. # connect do database
  688. sub _cfgDB_Connect() {
  689. my $fhem_dbh = DBI->connect(
  690. "dbi:$cfgDB_dbconn",
  691. $cfgDB_dbuser,
  692. $cfgDB_dbpass,
  693. { AutoCommit => 0, RaiseError => 1 },
  694. ) or die $DBI::errstr;
  695. return $fhem_dbh;
  696. }
  697. # add configuration entry into fhemconfig
  698. sub _cfgDB_InsertLine($$$$) {
  699. my ($fhem_dbh, $uuid, $line, $counter) = @_;
  700. my ($c,$d,$p1,$p2) = split(/ /, $line, 4);
  701. my $sth = $fhem_dbh->prepare('INSERT INTO fhemconfig values (?, ?, ?, ?, ?, ?)');
  702. $sth->execute($c, $d, $p1, $p2, $counter, $uuid);
  703. return;
  704. }
  705. # pass command table to AnalyzeCommandChain
  706. sub _cfgDB_Execute($@) {
  707. my ($cl, @dbconfig) = @_;
  708. my (@ret);
  709. foreach my $l (@dbconfig) {
  710. $l =~ s/[\r\n]/\n/g;
  711. $l =~ s/\\\n/\n/g;
  712. my $tret = AnalyzeCommandChain($cl, $l);
  713. push @ret, $tret if(defined($tret));
  714. }
  715. return join("\n", @ret) if(@ret);
  716. return undef;
  717. }
  718. # read all entries from fhemconfig
  719. # and add them to command table for execution
  720. sub _cfgDB_ReadCfg(@) {
  721. my (@dbconfig) = @_;
  722. my $fhem_dbh = _cfgDB_Connect;
  723. my ($sth, @line, $row);
  724. my $version = $configDB{attr}{loadversion};
  725. delete $configDB{attr}{loadversion};
  726. if ($version > 0) {
  727. my $count = $fhem_dbh->selectrow_array('SELECT count(*) FROM fhemversions');
  728. $count--;
  729. $version = $version > $count ? $count : $version;
  730. Log 0, "configDB loading version $version on user request. ".
  731. "Don't forget to unset variable cfgDB_version in environment!";
  732. }
  733. # maybe this will be done with join later
  734. my $uuid = $fhem_dbh->selectrow_array("SELECT versionuuid FROM fhemversions WHERE version = '$version'");
  735. $sth = $fhem_dbh->prepare( "SELECT * FROM fhemconfig WHERE versionuuid = '$uuid' and device <>'configdb' order by version" );
  736. $sth->execute();
  737. while (@line = $sth->fetchrow_array()) {
  738. $row = "$line[0] $line[1] $line[2]";
  739. $row .= " $line[3]" if defined($line[3]);
  740. push @dbconfig, $row;
  741. }
  742. $fhem_dbh->disconnect();
  743. return @dbconfig;
  744. }
  745. # read all entries from fhemstate
  746. # and add them to command table for execution
  747. sub _cfgDB_ReadState(@) {
  748. my (@dbconfig) = @_;
  749. my $fhem_dbh = _cfgDB_Connect;
  750. my ($sth, $row);
  751. $sth = $fhem_dbh->prepare( "SELECT * FROM fhemstate" );
  752. $sth->execute();
  753. while ($row = $sth->fetchrow_array()) {
  754. push @dbconfig, $row;
  755. }
  756. $fhem_dbh->disconnect();
  757. return @dbconfig;
  758. }
  759. # rotate all versions to versionnum + 1
  760. # return uuid for new version 0
  761. sub _cfgDB_Rotate($$) {
  762. my ($fhem_dbh,$newversion) = @_;
  763. my $uuid = _cfgDB_Uuid;
  764. $fhem_dbh->do("UPDATE fhemversions SET VERSION = VERSION+1 where VERSION >= 0") if $newversion == 0;
  765. $fhem_dbh->do("INSERT INTO fhemversions values ('$newversion', '$uuid')");
  766. return $uuid;
  767. }
  768. # 2015-01-12 use the fhem default function
  769. sub _cfgDB_Uuid() {
  770. return createUniqueId();
  771. }
  772. sub _cfgDB_filesize_str($) {
  773. my ($size) = @_;
  774. if ($size > 1099511627776) # TiB: 1024 GiB
  775. {
  776. return sprintf("%.2f TB", $size / 1099511627776);
  777. }
  778. elsif ($size > 1073741824) # GiB: 1024 MiB
  779. {
  780. return sprintf("%.2f GB", $size / 1073741824);
  781. }
  782. elsif ($size > 1048576) # MiB: 1024 KiB
  783. {
  784. return sprintf("%.2f MB", $size / 1048576);
  785. }
  786. elsif ($size > 1024) # KiB: 1024 B
  787. {
  788. return sprintf("%.2f KB", $size / 1024);
  789. }
  790. else # bytes
  791. {
  792. return "$size byte" . ($size == 1 ? "" : "s");
  793. }
  794. }
  795. ##################################################
  796. # Additional backend functions
  797. # not called from fhem.pl directly
  798. #
  799. # migrate existing fhem config into database
  800. sub _cfgDB_Migrate() {
  801. my $ret;
  802. $ret = "Starting migration...\n";
  803. Log3('configDB',4,'Starting migration');
  804. $ret .= "Processing: database initialization\n";
  805. Log3('configDB',4,'Processing: cfgDB_Init');
  806. cfgDB_Init;
  807. $ret .= "Processing: save config\n";
  808. Log3('configDB',4,'Processing: cfgDB_SaveCfg');
  809. cfgDB_SaveCfg;
  810. $ret .= "Processing: save state\n";
  811. Log3('configDB',4,'Processing: cfgDB_SaveState');
  812. cfgDB_SaveState;
  813. $ret .= "Processing: fileimport\n";
  814. Log3('configDB',4,'Processing: cfgDB_MigrationImport');
  815. $ret .= cfgDB_MigrationImport;
  816. $ret .= "Migration completed\n\n";
  817. Log3('configDB',4,'Migration completed.');
  818. $ret .= _cfgDB_Info(undef);
  819. return $ret;
  820. }
  821. # show database statistics
  822. sub _cfgDB_Info($) {
  823. my ($info2) = @_;
  824. $info2 //= 'unknown';
  825. Debug ">$info2<";
  826. my ($l, @r, $f);
  827. for my $i (1..65){ $l .= '-';}
  828. push @r, $l;
  829. push @r, " configDB Database Information";
  830. push @r, $l;
  831. my $info1 = cfgDB_svnId;
  832. $info1 =~ s/# //;
  833. push @r, " d:$info1";
  834. push @r, " c:$info2";
  835. push @r, $l;
  836. push @r, " dbconn: $cfgDB_dbconn";
  837. push @r, " dbuser: $cfgDB_dbuser" if !$configDB{attr}{private};
  838. push @r, " dbpass: $cfgDB_dbpass" if !$configDB{attr}{private};
  839. push @r, " dbtype: $cfgDB_dbtype";
  840. push @r, " Unknown dbmodel type in configuration file." if $cfgDB_dbtype eq 'unknown';
  841. push @r, " Only Mysql, Postgresql, SQLite are fully supported." if $cfgDB_dbtype eq 'unknown';
  842. if ($cfgDB_dbtype eq "SQLITE") {
  843. my $size = -s $cfgDB_filename;
  844. $size = _cfgDB_filesize_str($size);
  845. push @r, " dbsize: $size";
  846. }
  847. push @r, $l;
  848. my $fhem_dbh = _cfgDB_Connect;
  849. my ($sql, $sth, @line, $row);
  850. # read versions table statistics
  851. my $maxVersions = $configDB{attr}{maxversions};
  852. $maxVersions = ($maxVersions) ? $maxVersions : 0;
  853. push @r, " max Versions: $maxVersions" if($maxVersions);
  854. my $count;
  855. $count = $fhem_dbh->selectrow_array('SELECT count(*) FROM fhemconfig');
  856. push @r, " config: $count entries";
  857. push @r, "";
  858. # read versions creation time
  859. $sql = "SELECT * FROM fhemconfig as c join fhemversions as v on v.versionuuid=c.versionuuid ".
  860. "WHERE COMMAND like '#created%' ORDER by v.VERSION";
  861. $sth = $fhem_dbh->prepare( $sql );
  862. $sth->execute();
  863. while (@line = $sth->fetchrow_array()) {
  864. $line[3] = "" unless defined $line[3];
  865. $row = " Ver $line[6] saved: $line[1] $line[2] $line[3] def: ".
  866. $fhem_dbh->selectrow_array("SELECT COUNT(*) from fhemconfig where COMMAND = 'define' and VERSIONUUID = '$line[5]'");
  867. $row .= " attr: ".
  868. $fhem_dbh->selectrow_array("SELECT COUNT(*) from fhemconfig where COMMAND = 'attr' and VERSIONUUID = '$line[5]'");
  869. push @r, $row;
  870. }
  871. push @r, $l;
  872. # read state table statistics
  873. $count = $fhem_dbh->selectrow_array('SELECT count(*) FROM fhemstate');
  874. $f = ($count>1) ? "s" : "";
  875. # read state table creation time
  876. $sth = $fhem_dbh->prepare( "SELECT * FROM fhemstate WHERE STATESTRING like '#%'" );
  877. $sth->execute();
  878. while ($row = $sth->fetchrow_array()) {
  879. (undef,$row) = split(/#/,$row);
  880. $row = " state: $count entrie$f saved: $row";
  881. push @r, $row;
  882. }
  883. push @r, $l;
  884. $row = $fhem_dbh->selectall_arrayref("SELECT filename from fhemb64filesave group by filename");
  885. $count = @$row;
  886. $count = ($count)?$count:'No';
  887. $f = ("$count" ne '1') ? "s" : "";
  888. $row = " filesave: $count file$f stored in database";
  889. push @r, $row;
  890. push @r, $l;
  891. $fhem_dbh->disconnect();
  892. return join("\n", @r);
  893. }
  894. # recover former config from database archive
  895. sub _cfgDB_Recover($) {
  896. my ($version) = @_;
  897. my ($cmd, $count, $ret);
  898. if($version > 0) {
  899. my $fhem_dbh = _cfgDB_Connect;
  900. $cmd = "SELECT count(*) FROM fhemconfig WHERE VERSIONUUID in (select versionuuid from fhemversions where version = $version)";
  901. $count = $fhem_dbh->selectrow_array($cmd);
  902. if($count > 0) {
  903. my $fromuuid = $fhem_dbh->selectrow_array("select versionuuid from fhemversions where version = $version");
  904. my $touuid = _cfgDB_Uuid;
  905. # Delete current version 0
  906. $fhem_dbh->do("DELETE FROM fhemconfig WHERE VERSIONUUID in (select versionuuid from fhemversions where version = 0)");
  907. $fhem_dbh->do("update fhemversions set versionuuid = '$touuid' where version = 0");
  908. # Copy selected version to version 0
  909. my ($sth, $sth2, @line);
  910. $cmd = "SELECT * FROM fhemconfig WHERE VERSIONUUID = '$fromuuid'";
  911. $sth = $fhem_dbh->prepare($cmd);
  912. $sth->execute();
  913. $sth2 = $fhem_dbh->prepare('INSERT INTO fhemconfig values (?, ?, ?, ?, ?, ?)');
  914. while (@line = $sth->fetchrow_array()) {
  915. $sth2->execute($line[0], $line[1], $line[2], $line[3], $line[4], $touuid);
  916. }
  917. $fhem_dbh->commit();
  918. $fhem_dbh->disconnect();
  919. # Inform user about restart or rereadcfg needed
  920. $ret = "Version 0 deleted.\n";
  921. $ret .= "Version $version copied to version 0\n\n";
  922. $ret .= "Please use rereadcfg or restart to activate configuration.";
  923. } else {
  924. $fhem_dbh->disconnect();
  925. $ret = "No entries found in version $version.\nNo changes committed to database.";
  926. }
  927. } else {
  928. $ret = 'Please select version 1..n for recovery.';
  929. }
  930. return $ret;
  931. }
  932. # delete old configurations
  933. sub _cfgDB_Reorg(;$$) {
  934. my ($lastversion,$quiet) = @_;
  935. $lastversion = (defined($lastversion)) ? $lastversion : 3;
  936. Log3('configDB', 4, "DB Reorg started, keeping last $lastversion versions.");
  937. my $fhem_dbh = _cfgDB_Connect;
  938. $fhem_dbh->do("delete FROM fhemconfig where versionuuid in (select versionuuid from fhemversions where version > $lastversion)");
  939. $fhem_dbh->do("delete from fhemversions where version > $lastversion");
  940. $fhem_dbh->do("delete FROM fhemconfig where versionuuid in (select versionuuid from fhemversions where version = -1)");
  941. $fhem_dbh->do("delete from fhemversions where version = -1");
  942. $fhem_dbh->commit();
  943. $fhem_dbh->disconnect();
  944. eval qx(sqlite3 $cfgDB_filename vacuum) if($cfgDB_dbtype eq "SQLITE");
  945. return if(defined($quiet));
  946. return " Result after database reorg:\n"._cfgDB_Info(undef);
  947. }
  948. # delete temporary version
  949. sub _cfgDB_DeleteTemp() {
  950. Log3('configDB', 4, "configDB: delete temporary Version -1");
  951. my $fhem_dbh = _cfgDB_Connect;
  952. $fhem_dbh->do("delete FROM fhemconfig where versionuuid in (select versionuuid from fhemversions where version = -1)");
  953. $fhem_dbh->do("delete from fhemversions where version = -1");
  954. $fhem_dbh->commit();
  955. $fhem_dbh->disconnect();
  956. return;
  957. }
  958. # search for device or fulltext in db
  959. sub _cfgDB_Search($$;$) {
  960. my ($search,$searchversion,$dsearch) = @_;
  961. return 'Syntax error.' if(!(defined($search)));
  962. my $fhem_dbh = _cfgDB_Connect;
  963. my ($sql, $sth, @line, $row, @result, $ret, $text);
  964. $sql = "SELECT command, device, p1, p2 FROM fhemconfig as c join fhemversions as v ON v.versionuuid=c.versionuuid ";
  965. $sql .= "WHERE v.version = '$searchversion' AND command not like '#create%' ";
  966. # 2015-10-24 - changed, forum #42190
  967. if($cfgDB_dbtype eq 'SQLITE') {;
  968. $sql .= "AND device like '$search%' ESCAPE '\\' " if($dsearch);
  969. $sql .= "AND (device like '$search%' ESCAPE '\\' OR P1 like '$search%' ESCAPE '\\' OR P2 like '$search%' ESCAPE '\\') " if(!$dsearch);
  970. } else {
  971. $sql .= "AND device like '$search%' " if($dsearch);
  972. $sql .= "AND (device like '$search%' OR P1 like '$search%' OR P2 like '$search%') " if(!$dsearch);
  973. }
  974. $sql .= "ORDER BY lower(device),command DESC";
  975. $sth = $fhem_dbh->prepare( $sql);
  976. Log 5,"configDB: $sql";
  977. $sth->execute();
  978. $text = " device" if($dsearch);
  979. push @result, "search result for$text: $search in version: $searchversion";
  980. push @result, "--------------------------------------------------------------------------------";
  981. while (@line = $sth->fetchrow_array()) {
  982. $row = "$line[0] $line[1] $line[2] $line[3]";
  983. push @result, "$row";
  984. }
  985. $fhem_dbh->disconnect();
  986. $ret = join("\n", @result);
  987. return $ret;
  988. }
  989. # called from cfgDB_Diff
  990. sub __cfgDB_Diff($$$$) {
  991. my ($fhem_dbh,$search,$searchversion,$svinternal) = @_;
  992. my ($sql, $sth, @line, $ret);
  993. if($svinternal != -1) {
  994. $sql = "SELECT command, device, p1, p2 FROM fhemconfig as c join fhemversions as v ON v.versionuuid=c.versionuuid ".
  995. "WHERE v.version = '$searchversion' AND device = '$search' ORDER BY command DESC";
  996. } else {
  997. $sql = "SELECT command, device, p1, p2 FROM fhemconfig as c join fhemversions as v ON v.versionuuid=c.versionuuid ".
  998. "WHERE v.version = '$searchversion' ORDER BY command DESC";
  999. }
  1000. $sth = $fhem_dbh->prepare( $sql);
  1001. $sth->execute();
  1002. while (@line = $sth->fetchrow_array()) {
  1003. $ret .= "$line[0] $line[1] $line[2] $line[3]\n";
  1004. }
  1005. return $ret;
  1006. }
  1007. # compare device configurations from 2 versions
  1008. sub _cfgDB_Diff($$) {
  1009. my ($search,$searchversion) = @_;
  1010. my ($ret, $v0, $v1);
  1011. if ($search eq 'all' && $searchversion eq 'current') {
  1012. _cfgDB_DeleteTemp();
  1013. cfgDB_SaveCfg(-1);
  1014. $searchversion = -1;
  1015. }
  1016. my $fhem_dbh = _cfgDB_Connect;
  1017. $v0 = __cfgDB_Diff($fhem_dbh,$search,0,$searchversion);
  1018. $v1 = __cfgDB_Diff($fhem_dbh,$search,$searchversion,$searchversion);
  1019. $fhem_dbh->disconnect();
  1020. $ret = diff \$v0, \$v1, { STYLE => "Table" };
  1021. if($searchversion == -1) {
  1022. _cfgDB_DeleteTemp();
  1023. $searchversion = "UNSAVED";
  1024. }
  1025. $ret = "\nNo differences found!" if !$ret;
  1026. $ret = "compare device: $search in current version 0 (left) to version: $searchversion (right)\n$ret\n";
  1027. return $ret;
  1028. }
  1029. # find DEF, input supports devspec definitions
  1030. sub _cfgDB_findDef($;$) {
  1031. my ($search,$internal) = @_;
  1032. $internal = 'DEF' unless defined($internal);
  1033. my @ret;
  1034. my @etDev = devspec2array($search);
  1035. foreach my $d (@etDev) {
  1036. next unless $d;
  1037. push @ret, $defs{$d}{$internal};
  1038. }
  1039. return @ret;
  1040. }
  1041. sub _cfgDB_type() {
  1042. return "$cfgDB_dbtype (b64)";
  1043. }
  1044. sub _cfgDB_dump($) {
  1045. my ($param1) = @_;
  1046. $param1 //= '';
  1047. my ($dbconn,$dbuser,$dbpass,$dbtype) = _cfgDB_readConfig();
  1048. my ($dbname,$dbhostname,$dbport,$gzip,$mp,$ret,$size,$source,$target,$ts);
  1049. $ts = strftime('%Y-%m-%d_%H-%M-%S',localtime);
  1050. $mp = $configDB{attr}{'dumpPath'};
  1051. $mp //= AttrVal('global','modpath','.').'/log';
  1052. $target = "$mp/configDB_$ts.dump";
  1053. if (lc($param1) eq 'unzipped') {
  1054. $gzip = '';
  1055. } else {
  1056. $gzip = '| gzip -c';
  1057. $target .= '.gz';
  1058. }
  1059. if ($dbtype eq 'SQLITE') {
  1060. (undef,$source) = split (/=/, $dbconn);
  1061. my $dumpcmd = "echo '.dump fhem%' | sqlite3 $source $gzip > $target";
  1062. Log 4,"configDB: $dumpcmd";
  1063. $ret = qx($dumpcmd);
  1064. return $ret if $ret; # return error message if available
  1065. } elsif ($dbtype eq 'MYSQL') {
  1066. ($dbname,$dbhostname,$dbport) = split (/;/,$dbconn);
  1067. $dbport //= '=3306';
  1068. (undef,$dbname) = split (/=/,$dbname);
  1069. (undef,$dbhostname) = split (/=/,$dbhostname);
  1070. (undef,$dbport) = split (/=/,$dbport);
  1071. my $dbtables = "fhemversions fhemconfig fhemstate fhemb64filesave";
  1072. my $dumpcmd = "mysqldump --user=$dbuser --password=$dbpass --host=$dbhostname --port=$dbport -Q $dbname $dbtables $gzip > $target";
  1073. Log 4,"configDB: $dumpcmd";
  1074. $ret = qx($dumpcmd);
  1075. return $ret if $ret;
  1076. $source = $dbname;
  1077. } elsif ($dbtype eq 'POSTGRESQL') {
  1078. ($dbname,$dbhostname,$dbport) = split (/;/,$dbconn);
  1079. $dbport //= '=5432';
  1080. (undef,$dbname) = split (/=/,$dbname);
  1081. (undef,$dbhostname) = split (/=/,$dbhostname);
  1082. (undef,$dbport) = split (/=/,$dbport);
  1083. my $dbtables = "-t fhemversions -t fhemconfig -t fhemstate -t fhemb64filesave";
  1084. my $dumpcmd = "PGPASSWORD=$dbpass pg_dump -U $dbuser -h $dbhostname -p $dbport $dbname $dbtables $gzip > $target";
  1085. Log 4,"configDB: $dumpcmd";
  1086. $ret = qx($dumpcmd);
  1087. return $ret if $ret;
  1088. $source = $dbname;
  1089. } else {
  1090. return "configdb dump not supported for $dbtype!";
  1091. }
  1092. $size = -s $target;
  1093. $size //= 0;
  1094. $ret = "configDB dumped $size bytes\nfrom: $source\n to: $target";
  1095. return $ret;
  1096. }
  1097. ##################################################
  1098. # functions used for file handling
  1099. # called by 98_configdb.pm
  1100. #
  1101. # delete file from database
  1102. sub _cfgDB_Filedelete($) {
  1103. my ($filename) = @_;
  1104. my $fhem_dbh = _cfgDB_Connect;
  1105. my $ret = $fhem_dbh->do("delete from fhemb64filesave where filename = '$filename'");
  1106. $fhem_dbh->commit();
  1107. $fhem_dbh->disconnect();
  1108. $ret = ($ret > 0) ? 1 : undef;
  1109. # if($ret > 0) {
  1110. # $ret = "File $filename deleted from database.";
  1111. # } else {
  1112. # $ret = "File $filename not found in database.";
  1113. # }
  1114. return $ret;
  1115. }
  1116. # export file from database to filesystem
  1117. sub _cfgDB_Fileexport($;$) {
  1118. my ($filename,$raw) = @_;
  1119. my $fhem_dbh = _cfgDB_Connect;
  1120. my $sth = $fhem_dbh->prepare( "SELECT content FROM fhemb64filesave WHERE filename = '$filename'" );
  1121. $sth->execute();
  1122. my $blobContent = $sth->fetchrow_array();
  1123. $blobContent = decode_base64($blobContent);
  1124. my $counter = length($blobContent);
  1125. $sth->finish();
  1126. $fhem_dbh->disconnect();
  1127. return "No data found for file $filename" unless $counter;
  1128. return ($blobContent,$counter) if $raw;
  1129. open( FILE,">$filename" );
  1130. binmode(FILE);
  1131. print FILE $blobContent;
  1132. close( FILE );
  1133. return "$counter bytes written from database into file $filename";
  1134. }
  1135. # import file into database
  1136. sub _cfgDB_binFileimport($$;$) {
  1137. my ($filename,$filesize,$doDelete) = @_;
  1138. $doDelete = (defined($doDelete)) ? 1 : 0;
  1139. open (inFile,"<$filename") || die $!;
  1140. my $blobContent;
  1141. binmode(inFile);
  1142. my $readBytes = read(inFile, $blobContent, $filesize);
  1143. close(inFile);
  1144. $blobContent = encode_base64($blobContent);
  1145. my $fhem_dbh = _cfgDB_Connect;
  1146. $fhem_dbh->do("delete from fhemb64filesave where filename = '$filename'");
  1147. my $sth = $fhem_dbh->prepare('INSERT INTO fhemb64filesave values (?, ?)');
  1148. # add support for postgresql by Matze
  1149. $sth->bind_param( 1, $filename );
  1150. if ($cfgDB_dbtype eq "POSTGRESQL") {
  1151. $sth->bind_param( 2, $blobContent, { pg_type => DBD::Pg::PG_BYTEA() } );
  1152. } else {
  1153. $sth->bind_param( 2, $blobContent );
  1154. }
  1155. $sth->execute($filename, $blobContent);
  1156. $sth->finish();
  1157. $fhem_dbh->commit();
  1158. $fhem_dbh->disconnect();
  1159. unlink($filename) if(($configDB{attr}{deleteimported} || $doDelete) && $readBytes);
  1160. return "$readBytes bytes written from file $filename to database";
  1161. }
  1162. # list all files stored in database
  1163. sub _cfgDB_Filelist(;$) {
  1164. my ($notitle) = @_;
  1165. my $ret = "Files found in database:\n".
  1166. "------------------------------------------------------------\n";
  1167. $ret = "" if $notitle;
  1168. my $fhem_dbh = _cfgDB_Connect;
  1169. my $sql = "SELECT filename FROM fhemb64filesave group by filename order by filename";
  1170. my $content = $fhem_dbh->selectall_arrayref($sql);
  1171. foreach my $row (@$content) {
  1172. $ret .= "@$row[0]\n" if(defined(@$row[0]));
  1173. }
  1174. $fhem_dbh->disconnect();
  1175. return $ret;
  1176. }
  1177. 1;
  1178. =pod
  1179. =item helper
  1180. =item summary configDB backend
  1181. =item summary_DE configDB backend
  1182. =begin html
  1183. <a name="configDB"></a>
  1184. <h3>configDB</h3>
  1185. <ul>
  1186. <a href="https://forum.fhem.de/index.php?board=46.0">Link to FHEM forum</a><br/><br/>
  1187. This is the core backend library for configuration from SQL database.<br/>
  1188. See <a href="#configdb">configdb command documentation</a> for detailed info.<br/>
  1189. </ul>
  1190. =end html
  1191. =begin html_DE
  1192. <a name="configDB"></a>
  1193. <h3>configDB</h3>
  1194. <ul>
  1195. configDB ist die Funktionsbibliothek f&uuml;r die Konfiguration aus einer SQL Datenbank.<br/>
  1196. Die ausf&uuml;hrliche Dokumentation findet sich in der <a href="#configdb">configdb Befehlsbeschreibung</a>.
  1197. </ul>
  1198. =end html_DE
  1199. =cut