diff --git a/fhem/FHEM/01_FHEMWEB.pm b/fhem/FHEM/01_FHEMWEB.pm index 9c948c611..e758562b7 100755 --- a/fhem/FHEM/01_FHEMWEB.pm +++ b/fhem/FHEM/01_FHEMWEB.pm @@ -7,7 +7,6 @@ use warnings; use TcpServerUtils; use HttpUtils; use Time::HiRes qw(gettimeofday); -use Errno qw(:POSIX); ######################### # Forward declaration @@ -1265,8 +1264,6 @@ FW_roomOverview($) my $class = "menu_$l1"; $class =~ s/[^A-Z0-9]/_/gi; - $class .= ($lastDefChange>$lastSavedChange) ? " changed" : "" - if($l1 eq "Save config"); # image tag if we have an icon, else empty my $icoName = "ico$l1"; @@ -1275,12 +1272,18 @@ FW_roomOverview($) my $icon = FW_iconName($icoName) ? FW_makeImage($icoName,$icoName,"icon")." " : ""; + if($l1 eq "Save config") { + $l1 .= ' ?'; + } + # Force external browser if FHEMWEB is installed as an offline app. if($l2 =~ m/.html$/ || $l2 =~ m/^http/) { FW_pO "
$icon$l1
"; } else { FW_pH $l2, "$icon$l1", 1, $class; } + FW_pO ""; } } @@ -2268,13 +2271,9 @@ FW_Notify($$) my ($ntfy, $dev) = @_; if( $dev->{NAME} eq "global" ) { - my $n = "#FHEMWEB:$ntfy->{NAME}"; - if( grep(m/^SAVE|INITIALIZED|REREADCFG|SHUTDOWN$/, @{$dev->{CHANGED}}) ) { - FW_directNotify($n, '$(".menu_Save_config").removeClass("changed")', ''); - } elsif( grep(m/^DEFINED|MODIFIED|DELETED|ATTR|DELETEATTR$/, - @{$dev->{CHANGED}}) ) { - FW_directNotify($n, '$(".menu_Save_config").addClass("changed")', ''); - } + my $vs = int(@structChangeHist) ? 'visible' : 'hidden'; + FW_directNotify( "#FHEMWEB:$ntfy->{NAME}", + "\$('#saveCheck').css('visibility','$vs')", ''); } my $h = $ntfy->{inform}; diff --git a/fhem/FHEM/TcpServerUtils.pm b/fhem/FHEM/TcpServerUtils.pm index 68b9429f0..523cfe1b3 100644 --- a/fhem/FHEM/TcpServerUtils.pm +++ b/fhem/FHEM/TcpServerUtils.pm @@ -5,7 +5,6 @@ package main; use strict; use warnings; use IO::Socket; -use Errno qw(:POSIX); sub TcpServer_Open($$$) @@ -57,7 +56,7 @@ TcpServer_Accept($$) my $name = $hash->{NAME}; my @clientinfo = $hash->{SERVERSOCKET}->accept(); if(!@clientinfo) { - Log3 $name, 1, "Accept failed ($name: $!)" if($! != EAGAIN()); + Log3 $name, 1, "Accept failed ($name: $!)" if($! != EAGAIN); return undef; } $hash->{CONNECTS}++; diff --git a/fhem/fhem.pl b/fhem/fhem.pl index 4c9ce0ea4..059526dee 100755 --- a/fhem/fhem.pl +++ b/fhem/fhem.pl @@ -219,7 +219,7 @@ use vars qw(%readyfnlist); # devices which want a "readyfn" use vars qw(%selectlist); # devices which want a "select" use vars qw(%value); # Current values, see commandref.html use vars qw($lastDefChange); # number of last def/attr change -use vars qw($lastSavedChange); # will be synced with lastDefChange on save +use vars qw(@structChangeHist); # Contains the last 10 structural changes use vars qw($cmdFromAnalyze); # used by the warnings-sub my $AttrList = "verbose:0,1,2,3,4,5 room group comment alias ". @@ -242,7 +242,6 @@ my @cmdList; # Remaining commands in a chain. Used by sleep $init_done = 0; $lastDefChange = 0; -$lastSavedChange = 0; $readytimeout = ($^O eq "MSWin32") ? 0.1 : 5.0; @@ -508,7 +507,6 @@ foreach my $d (keys %defs) { } } -$lastSavedChange = $lastDefChange; DoTrigger("global", "INITIALIZED", 1); $fhem_started = time; @@ -1234,7 +1232,7 @@ CommandRereadCfg($$) $defs{$name} = $selectlist{$name} = $cl if($name && $name ne "__anonymous__"); $inform{$name} = $informMe if($informMe); - $lastSavedChange = $lastDefChange; + @structChangeHist = (); DoTrigger("global", "REREADCFG", 1); $init_done = 1; @@ -1327,6 +1325,12 @@ CommandSave($$) { my ($cl, $param) = @_; + if($param eq "?") { + return "No structural changes." if(!@structChangeHist); + return "Last 10 structural changes:\n ".join("\n ", @structChangeHist); + } + + @structChangeHist = (); DoTrigger("global", "SAVE", 1); my $ret = WriteStatefile(); @@ -1413,7 +1417,6 @@ CommandSave($$) $ret .= "$key: $!" if(!close($fh{$key})); } - $lastSavedChange = $lastDefChange; return ($ret ? $ret : "Wrote configuration to $param"); } @@ -1619,9 +1622,9 @@ CommandDefine($$) $modules{$m}{NotifyOrderPrefix} : "50-") . $name; } %ntfyHash = (); + addStructChange("define", $name, $def); DoTrigger("global", "DEFINED $name", 1) if($init_done); } - $lastDefChange++ if(!$hash{TEMPORARY}); return $ret; } @@ -1647,11 +1650,11 @@ CommandModify($$) if($ret) { $hash->{DEF} = $hash->{OLDDEF}; } else { + addStructChange("modify", $a[0], $def); DoTrigger("global", "MODIFIED $a[0]", 1) if($init_done); } delete($hash->{OLDDEF}); - $lastDefChange++ if(!$hash->{TEMPORARY}); return $ret; } @@ -1714,7 +1717,7 @@ CommandDelete($$) my ($cl, $def) = @_; return "Usage: delete $namedef\n" if(!$def); - my (@rets, $isReal); + my @rets; foreach my $sdev (devspec2array($def)) { if(!defined($defs{$sdev})) { push @rets, "Please define $sdev first"; @@ -1732,7 +1735,6 @@ CommandDelete($$) next; } - $isReal = 1 if(!$defs{$sdev}{TEMPORARY}); # Delete releated hashes foreach my $p (keys %selectlist) { @@ -1745,13 +1747,13 @@ CommandDelete($$) if($readyfnlist{$p} && $readyfnlist{$p}{NAME} eq $sdev); } - delete($attr{$sdev}); my $temporary = $defs{$sdev}{TEMPORARY}; - delete($defs{$sdev}); # Remove the main entry + addStructChange("delete", $sdev, $sdev) if(!$temporary); + delete($attr{$sdev}); + delete($defs{$sdev}); DoTrigger("global", "DELETED $sdev", 1) if(!$temporary); } - $lastDefChange++ if($isReal); return join("\n", @rets); } @@ -1764,7 +1766,7 @@ CommandDeleteAttr($$) my @a = split(" ", $def, 2); return "Usage: deleteattr []\n$namedef" if(@a < 1); - my (@rets, $isReal); + my @rets; foreach my $sdev (devspec2array($a[0])) { if(!defined($defs{$sdev})) { @@ -1784,21 +1786,20 @@ CommandDeleteAttr($$) next; } - $isReal = 1 if(!$defs{$sdev}{TEMPORARY}); - if(@a == 1) { delete($attr{$sdev}); + addStructChange("deleteAttr", $sdev, $def); DoTrigger("global", "DELETEATTR $sdev", 1) if($init_done); } else { delete($attr{$sdev}{$a[1]}) if(defined($attr{$sdev})); + addStructChange("deleteAttr", $sdev, $def); DoTrigger("global", "DELETEATTR $sdev $a[1]", 1) if($init_done); } } - $lastDefChange++ if($isReal); return join("\n", @rets); } @@ -2101,8 +2102,8 @@ CommandRename($$) CallFn($new, "RenameFn", $new,$old);# ignore replies + addStructChange("rename", $new, $param); DoTrigger("global", "RENAMED $old $new", 1); - $lastDefChange++ if(!$defs{$new}{TEMPORARY}); return undef; } @@ -2239,7 +2240,7 @@ sub CommandAttr($$) { my ($cl, $param) = @_; - my ($ret, $isReal, @a); + my ($ret, @a); @a = split(" ", $param, 3) if($param); @@ -2315,8 +2316,6 @@ CommandAttr($$) next; } - $isReal = 1 if(!$defs{$sdev}{TEMPORARY}); - $a[0] = $sdev; $ret = CallFn($sdev, "AttrFn", "set", @a); if($ret) { @@ -2326,6 +2325,12 @@ CommandAttr($$) my $val = $a[2]; $val = 1 if(!defined($val)); + + addStructChange("attr", $sdev, $param) + if(!($attr{$sdev} && + defined($attr{$sdev}{$attrName}) && + $attr{$sdev}{$attrName} eq $val)); + $attr{$sdev}{$attrName} = $val; if($attrName eq "IODev") { @@ -2342,7 +2347,6 @@ CommandAttr($$) } - $lastDefChange++ if($isReal); Log 3, join(" ", @rets) if(!$cl && @rets); return join("\n", @rets); } @@ -4075,5 +4079,18 @@ setKeyValue($$) return FileWrite($fName, @new); } +sub +addStructChange($$$) +{ + return if(!$init_done); + + my ($cmd, $dev, $param) = @_; + return if(!$defs{$dev} || $defs{$dev}{TEMPORARY}); + + $lastDefChange++; + shift @structChangeHist if(@structChangeHist > 10); + $param = substr($param, 0, 40)."..." if(length($param) > 40); + push @structChangeHist, "$cmd $param"; +} 1; diff --git a/fhem/www/pgm2/fhemweb.js b/fhem/www/pgm2/fhemweb.js index 96db0449f..5360055b5 100644 --- a/fhem/www/pgm2/fhemweb.js +++ b/fhem/www/pgm2/fhemweb.js @@ -112,6 +112,13 @@ FW_jqueryReadyFn() }); */ + $("#saveCheck") + .css("cursor", "pointer") + .click(function(){ + FW_cmd(FW_root+"?cmd=save ?&XHR=1", function(data) { + FW_okDialog('
'+data+'
'); + }); + }); }