From e422afee7dcc61d67ba02e717ea1b14c988055c7 Mon Sep 17 00:00:00 2001 From: betateilchen Date: Mon, 21 Jan 2019 20:46:46 +0000 Subject: [PATCH] :for testing git-svn-id: https://svn.fhem.de/fhem/trunk@18362 2b470e98-0d58-463d-a4d8-8e2adae1ed80 --- fhem/contrib/betateilchen/01_FHEMWEB.pm | 4822 +++++++++++++++++++++++ 1 file changed, 4822 insertions(+) create mode 100644 fhem/contrib/betateilchen/01_FHEMWEB.pm diff --git a/fhem/contrib/betateilchen/01_FHEMWEB.pm b/fhem/contrib/betateilchen/01_FHEMWEB.pm new file mode 100644 index 000000000..f635da305 --- /dev/null +++ b/fhem/contrib/betateilchen/01_FHEMWEB.pm @@ -0,0 +1,4822 @@ +############################################## +# $Id: 01_FHEMWEB.pm 18326 2019-01-18 22:09:42Z rudolfkoenig $ +package main; + +use strict; +use warnings; +use TcpServerUtils; +use HttpUtils; +use Blocking; +use Time::HiRes qw(gettimeofday); + +######################### +# Forward declaration +sub FW_IconURL($); +sub FW_addContent(;$); +sub FW_addToWritebuffer($$@); +sub FW_answerCall($); +sub FW_dev2image($;$); +sub FW_devState($$@); +sub FW_digestCgi($); +sub FW_directNotify($@); +sub FW_doDetail($); +sub FW_fatal($); +sub FW_fileList($;$); +sub FW_htmlEscape($); +sub FW_iconName($); +sub FW_iconPath($); +sub FW_logWrapper($); +sub FW_makeEdit($$$); +sub FW_makeImage(@); +sub FW_makeTable($$$@); +sub FW_makeTableFromArray($$@); +sub FW_pF($@); +sub FW_pH(@); +sub FW_pHPlain(@); +sub FW_pO(@); +sub FW_parseColumns($); +sub FW_readIcons($); +sub FW_readIconsFrom($$); +sub FW_returnFileAsStream($$$$$); +sub FW_roomOverview($); +#sub FW_roomStatesForInform($$); # Forum 30515 +sub FW_select($$$$$@); +sub FW_serveSpecial($$$$); +sub FW_showRoom(); +sub FW_style($$); +sub FW_submit($$@); +sub FW_textfield($$$); +sub FW_textfieldv($$$$); +sub FW_updateHashes(); +sub FW_visibleDevices(;$); +sub FW_widgetOverride($$); +sub FW_Read($$); + +use vars qw($FW_dir); # base directory for web server +use vars qw($FW_icondir); # icon base directory +use vars qw($FW_cssdir); # css directory +use vars qw($FW_gplotdir);# gplot directory +use vars qw($FW_confdir); # conf dir +use vars qw($MW_dir); # moddir (./FHEM), needed by edit Files in new + # structure + +use vars qw($FW_ME); # webname (default is fhem), used by 97_GROUP/weblink +use vars qw($FW_CSRF); # CSRF Token or empty +use vars qw($FW_ss); # is smallscreen, needed by 97_GROUP/95_VIEW +use vars qw($FW_tp); # is touchpad (iPad / etc) +use vars qw($FW_sp); # stylesheetPrefix + +# global variables, also used by 97_GROUP/95_VIEW/95_FLOORPLAN +use vars qw(%FW_types); # device types, +use vars qw($FW_RET); # Returned data (html) +use vars qw($FW_RETTYPE); # image/png or the like +use vars qw($FW_wname); # Web instance +use vars qw($FW_subdir); # Sub-path in URL, used by FLOORPLAN/weblink +use vars qw(%FW_pos); # scroll position +use vars qw($FW_cname); # Current connection name +use vars qw(%FW_hiddenroom); # hash of hidden rooms, used by weblink +use vars qw($FW_plotmode);# Global plot mode (WEB attribute), used by SVG +use vars qw($FW_plotsize);# Global plot size (WEB attribute), used by SVG +use vars qw(%FW_webArgs); # all arguments specified in the GET +use vars qw(@FW_fhemwebjs);# List of fhemweb*js scripts to load +use vars qw($FW_fhemwebjs);# List of fhemweb*js scripts to load +use vars qw($FW_detail); # currently selected device for detail view +use vars qw($FW_cmdret); # Returned data by the fhem call +use vars qw($FW_room); # currently selected room +use vars qw($FW_formmethod); +use vars qw(%FW_visibleDeviceHash); +use vars qw(@FW_httpheader); # HTTP header, line by line +use vars qw(%FW_httpheader); # HTTP header, as hash +use vars qw($FW_userAgent); # user agent string +use vars qw(%FW_customConfFiles); + +$FW_formmethod = "post"; + +my %FW_use; +my $FW_activateInform = 0; +my $FW_lastWebName = ""; # Name of last FHEMWEB instance, for caching +my $FW_lastHashUpdate = 0; +my $FW_httpRetCode = ""; +my %FW_csrfTokenCache; +my %FW_id2inform; + +######################### +# As we are _not_ multithreaded, it is safe to use global variables. +# Note: for delivering SVG plots we fork +my $FW_data; # Filecontent from browser when editing a file +my %FW_icons; # List of icons +my @FW_iconDirs; # Directory search order for icons +my $FW_RETTYPE; # image/png or the like +my %FW_rooms; # hash of all rooms +my @FW_roomsArr; # ordered list of rooms +my %FW_groups; # hash of all groups +my %FW_types; # device types, for sorting +my %FW_hiddengroup;# hash of hidden groups +my $FW_inform; +my $FW_XHR; # Data only answer, no HTML +my $FW_id=""; # id of current page +my $FW_jsonp; # jasonp answer (sending function calls to the client) +my $FW_headerlines; # +my $FW_chash; # client fhem hash +my $FW_encoding="UTF-8"; +my $FW_styleStamp=time(); +my %FW_svgData; + + +##################################### +sub +FHEMWEB_Initialize($) +{ + my ($hash) = @_; + + $hash->{ReadFn} = "FW_Read"; + $hash->{GetFn} = "FW_Get"; + $hash->{SetFn} = "FW_Set"; + $hash->{AttrFn} = "FW_Attr"; + $hash->{DefFn} = "FW_Define"; + $hash->{UndefFn} = "FW_Undef"; + $hash->{NotifyFn}= "FW_Notify"; + $hash->{AsyncOutputFn} = "FW_AsyncOutput"; + $hash->{ActivateInformFn} = "FW_ActivateInform"; + $hash->{CanAuthenticate} = 1; + no warnings 'qw'; + my @attrList = qw( + CORS:0,1 + HTTPS:1,0 + CssFiles + Css:textField-long + JavaScripts + SVGcache:1,0 + addHtmlTitle:1,0 + addStateEvent + csrfToken + csrfTokenHTTPHeader:0,1 + alarmTimeout + allowedHttpMethods + allowedCommands + allowfrom + basicAuth + basicAuthMsg + closeConn:1,0 + column + confirmDelete:0,1 + confirmJSError:0,1 + defaultRoom + deviceOverview:always,iconOnly,onClick,never + editConfig:1,0 + editFileList:textField-long + endPlotNow:1,0 + endPlotToday:1,0 + forbiddenroom + fwcompress:0,1 + hiddengroup + hiddengroupRegexp + hiddenroom + hiddenroomRegexp + httpHeader + iconPath + longpoll:0,1,websocket + longpollSVG:1,0 + menuEntries + mainInputLength + nameDisplay + ploteditor:always,onClick,never + plotfork:1,0 + plotmode:gnuplot-scroll,gnuplot-scroll-svg,SVG + plotEmbed:0,1 + plotsize + plotWeekStartDay:0,1,2,3,4,5,6 + nrAxis + redirectCmds:0,1 + refresh + reverseLogs:0,1 + roomIcons + showUsedFiles:0,1 + sortRooms + sslVersion + sslCertPrefix + smallscreen:unused + smallscreenCommands:0,1 + stylesheetPrefix + styleData:textField-long + title + touchpad:unused + viewport + webname + ); + use warnings 'qw'; + $hash->{AttrList} = join(" ", @attrList); + + + ############### + # Initialize internal structures + map { addToAttrList($_) } ( + "cmdIcon", + "devStateIcon:textField-long", + "devStateStyle", + "icon", + "sortby", + "webCmd", + "webCmdLabel:textField-long", + "widgetOverride" + ); + + InternalTimer(time()+60, "FW_closeInactiveClients", 0, 0); + + $FW_confdir = "$attr{global}{modpath}/conf"; + $FW_dir = "$attr{global}{modpath}/www"; + $FW_icondir = "$FW_dir/images"; + $FW_cssdir = "$FW_dir/pgm2"; + $FW_gplotdir = "$FW_dir/gplot"; + + if(opendir(DH, "$FW_dir/pgm2")) { + $FW_fhemwebjs = join(",", map { $_ = ~m/^fhemweb_(.*).js$/; $1 } + grep { /fhemweb_(.*).js$/ } + readdir(DH)); + closedir(DH); + } + + $data{webCmdFn}{"~"} = "FW_widgetFallbackFn"; # Should be the last + + if($init_done) { # reload workaround + foreach my $pe ("fhemSVG", "openautomation", "default") { + FW_readIcons($pe); + } + } + + my %optMod = ( + zlib => { mod=>"Compress::Zlib", txt=>"compressed HTTP transfer" }, + sha => { mod=>"Digest::SHA", txt=>"longpoll via websocket" }, + base64 => { mod=>"MIME::Base64", txt=>"parallel SVG computing" } + ); + foreach my $mod (keys %optMod) { + eval "require $optMod{$mod}{mod}"; + if($@) { + Log 4, $@; + Log 3, "FHEMWEB: Can't load $optMod{$mod}{mod}, ". + "$optMod{$mod}{txt} is not available"; + } else { + $FW_use{$mod} = 1; + } + } +} + +##################################### +sub +FW_Define($$) +{ + my ($hash, $def) = @_; + my ($name, $type, $port, $global) = split("[ \t]+", $def); + return "Usage: define FHEMWEB [IPV6:] [global]" + if($port !~ m/^(IPV6:)?\d+$/); + + FW_Undef($hash, undef) if($hash->{OLDDEF}); # modify + + foreach my $pe ("fhemSVG", "openautomation", "default") { + FW_readIcons($pe); + } + + my $ret = TcpServer_Open($hash, $port, $global); + + # Make sure that fhem only runs once + if($ret && !$init_done) { + Log3 $hash, 1, "$ret. Exiting."; + exit(1); + } + + $hash->{CSRFTOKEN} = $FW_csrfTokenCache{$name}; + if(!defined($hash->{CSRFTOKEN})) { # preserve over rereadcfg + InternalTimer(1, sub(){ + if($featurelevel >= 5.8 && !AttrVal($name, "csrfToken", undef)) { + my ($x,$y) = gettimeofday(); + ($defs{$name}{CSRFTOKEN}="csrf_".(rand($y)*rand($x))) =~s/[^a-z_0-9]//g; + $FW_csrfTokenCache{$name} = $hash->{CSRFTOKEN}; + } + }, $hash, 0); + } + + return $ret; +} + +##################################### +sub +FW_Undef($$) +{ + my ($hash, $arg) = @_; + my $ret = TcpServer_Close($hash); + if($hash->{inform}) { + delete $FW_id2inform{$hash->{FW_ID}} if($hash->{FW_ID}); + %FW_visibleDeviceHash = FW_visibleDevices(); + delete($logInform{$hash->{NAME}}); + } + return $ret; +} + +##################################### +sub +FW_Read($$) +{ + my ($hash, $reread) = @_; + my $name = $hash->{NAME}; + + if($hash->{SERVERSOCKET}) { # Accept and create a child + my $nhash = TcpServer_Accept($hash, "FHEMWEB"); + return if(!$nhash); + my $wt = AttrVal($name, "alarmTimeout", undef); + $nhash->{ALARMTIMEOUT} = $wt if($wt); + $nhash->{CD}->blocking(0); + return; + } + + $FW_chash = $hash; + $FW_wname = $hash->{SNAME}; + $FW_cname = $name; + $FW_subdir = ""; + + my $c = $hash->{CD}; + + if(!$reread) { + # Data from HTTP Client + my $buf; + my $ret = sysread($c, $buf, 1024); + + if(!defined($ret) && $! == EWOULDBLOCK ){ + $hash->{wantWrite} = 1 + if(TcpServer_WantWrite($hash)); + return; + } elsif(!$ret) { # 0==EOF, undef=error + CommandDelete(undef, $name); + Log3 $FW_wname, 4, "Connection closed for $name: ". + (defined($ret) ? 'EOF' : $!); + return; + } + $hash->{BUF} .= $buf; + if($hash->{SSL} && $c->can('pending')) { + while($c->pending()) { + sysread($c, $buf, 1024); + $hash->{BUF} .= $buf; + } + } + } + + if($hash->{websocket}) { # 59713 + my $fin = (ord(substr($hash->{BUF},0,1)) & 0x80)?1:0; + my $op = (ord(substr($hash->{BUF},0,1)) & 0x0F); + my $mask = (ord(substr($hash->{BUF},1,1)) & 0x80)?1:0; + my $len = (ord(substr($hash->{BUF},1,1)) & 0x7F); + my $i = 2; + + if($op == 8) { + TcpServer_Close($hash, 1); + return; + } + + if( $len == 126 ) { + $len = unpack( 'n', substr($hash->{BUF},$i,2) ); + $i += 2; + } elsif( $len == 127 ) { + $len = unpack( 'q', substr($hash->{BUF},$i,8) ); + $i += 8; + } + + if( $mask ) { + $mask = substr($hash->{BUF},$i,4); + $i += 4; + } + + #my $data = substr($hash->{BUF}, $i, $len); + #for( my $i = 0; $i < $len; $i++ ) { + # substr( $data, $i, 1, substr( $data, $i, 1, ) ^ substr($mask, $i% , 1) ); + #} + #Log 1, "Received via websocket: ".unpack("H*",$data); + $hash->{BUF} = ""; + return; + } + + + + if(!$hash->{HDR}) { + return if($hash->{BUF} !~ m/^(.*?)(\n\n|\r\n\r\n)(.*)$/s); + $hash->{HDR} = $1; + $hash->{BUF} = $3; + if($hash->{HDR} =~ m/Content-Length:\s*([^\r\n]*)/si) { + $hash->{CONTENT_LENGTH} = $1; + } + } + + my $POSTdata = ""; + if($hash->{CONTENT_LENGTH}) { + return if(length($hash->{BUF})<$hash->{CONTENT_LENGTH}); + $POSTdata = substr($hash->{BUF}, 0, $hash->{CONTENT_LENGTH}); + $hash->{BUF} = substr($hash->{BUF}, $hash->{CONTENT_LENGTH}); + } + + @FW_httpheader = split(/[\r\n]+/, $hash->{HDR}); + %FW_httpheader = map { + my ($k,$v) = split(/: */, $_, 2); + $k =~ s/(\w+)/\u$1/g; # Forum #39203 + $k=>(defined($v) ? $v : 1); + } @FW_httpheader; + delete($hash->{HDR}); + + my @origin = grep /Origin/i, @FW_httpheader; + $FW_headerlines = (AttrVal($FW_wname, "CORS", 0) ? + (($#origin<0) ? "": "Access-Control-Allow-".$origin[0]."\r\n"). + "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n". + "Access-Control-Allow-Headers: Origin, Authorization, Accept\r\n". + "Access-Control-Allow-Credentials: true\r\n". + "Access-Control-Max-Age:86400\r\n". + "Access-Control-Expose-Headers: X-FHEM-csrfToken\r\n": ""); + $FW_headerlines .= "X-FHEM-csrfToken: $defs{$FW_wname}{CSRFTOKEN}\r\n" + if(defined($defs{$FW_wname}{CSRFTOKEN}) && + AttrVal($FW_wname, "csrfTokenHTTPHeader", 1)); + + my $hh = AttrVal($FW_wname, "httpHeader", undef); + $FW_headerlines .= "$hh\r\n" if($hh); + + ######################### + # Return 200 for OPTIONS or 405 for unsupported method + my ($method, $arg, $httpvers) = split(" ", $FW_httpheader[0], 3) + if($FW_httpheader[0]); + $method = "" if(!$method); + my $ahm = AttrVal($FW_wname, "allowedHttpMethods", "GET|POST"); + if($method !~ m/^($ahm)$/i){ + my $retCode = ($method eq "OPTIONS") ? "200 OK" : "405 Method Not Allowed"; + TcpServer_WriteBlocking($FW_chash, + "HTTP/1.1 $retCode\r\n" . + $FW_headerlines. + "Content-Length: 0\r\n\r\n"); + delete $hash->{CONTENT_LENGTH}; + FW_Read($hash, 1) if($hash->{BUF}); + Log 3, "$FW_cname: unsupported HTTP method $method, rejecting it." + if($retCode ne "200 OK"); + FW_closeConn($hash); + return; + } + + ############################# + # AUTH + if(!defined($FW_chash->{Authenticated})) { + my $ret = Authenticate($FW_chash, \%FW_httpheader); + if($ret == 0) { + $FW_chash->{Authenticated} = 0; # not needed + + } elsif($ret == 1) { + $FW_chash->{Authenticated} = 1; # ok + # Need to send set-cookie (if set) after succesful authentication + my $ah = $FW_chash->{".httpAuthHeader"}; + $FW_headerlines .= $ah if($ah); + delete $FW_chash->{".httpAuthHeader"}; + + } else { + my $ah = $FW_chash->{".httpAuthHeader"}; + TcpServer_WriteBlocking($hash, + ($ah ? $ah : ""). + $FW_headerlines. + "Content-Length: 0\r\n\r\n"); + delete $hash->{CONTENT_LENGTH}; + FW_Read($hash, 1) if($hash->{BUF}); + return; + } + } else { + my $ah = $FW_chash->{".httpAuthHeader"}; + $FW_headerlines .= $ah if($ah); + } + ############################# + + my $now = time(); + $arg .= "&".$POSTdata if($POSTdata); + delete $hash->{CONTENT_LENGTH}; + $hash->{LASTACCESS} = $now; + + $FW_userAgent = $FW_httpheader{"User-Agent"}; + $FW_userAgent = "" if(!defined($FW_userAgent)); + + $FW_ME = "/" . AttrVal($FW_wname, "webname", "fhem"); + $FW_CSRF = (defined($defs{$FW_wname}{CSRFTOKEN}) ? + "&fwcsrf=".$defs{$FW_wname}{CSRFTOKEN} : ""); + + if($FW_use{sha} && $method eq 'GET' && + $FW_httpheader{Connection} && $FW_httpheader{Connection} =~ /Upgrade/i) { + + my $shastr = Digest::SHA::sha1_base64($FW_httpheader{'Sec-WebSocket-Key'}. + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + + TcpServer_WriteBlocking($FW_chash, + "HTTP/1.1 101 Switching Protocols\r\n" . + "Upgrade: websocket\r\n" . + "Connection: Upgrade\r\n" . + "Sec-WebSocket-Accept:$shastr=\r\n". + $FW_headerlines. + "\r\n" ); + $FW_chash->{websocket} = 1; + + my $me = $FW_chash; + my ($cmd, $cmddev) = FW_digestCgi($arg); + if($FW_id) { + $me->{FW_ID} = $FW_id; + $me->{canAsyncOutput} = 1; + } + FW_initInform($me, 0) if($FW_inform); + return -1; + } + + $arg = "" if(!defined($arg)); + Log3 $FW_wname, 4, "$name $method $arg; BUFLEN:".length($hash->{BUF}); + my $pf = AttrVal($FW_wname, "plotfork", 0); + if($pf) { # 0 disables + # Process SVG rendering as a parallel process + my $p = $data{FWEXT}; + if(grep { $p->{$_}{FORKABLE} && $arg =~ m+^$FW_ME$_+ } keys %{$p}) { + my $pid = fhemFork(); + if($pid) { # success, parent + use constant PRIO_PROCESS => 0; + setpriority(PRIO_PROCESS, $pid, getpriority(PRIO_PROCESS,$pid) + $pf) + if($^O !~ m/Win/); + # a) while child writes a new request might arrive if client uses + # pipelining or + # b) parent doesn't know about ssl-session changes due to child writing + # to socket + # -> have to close socket in parent... so that its only used in this + # child. + TcpServer_Disown( $hash ); + delete($defs{$name}); + delete($attr{$name}); + FW_Read($hash, 1) if($hash->{BUF}); + return; + + } elsif(defined($pid)){ # child + delete $hash->{BUF}; + $hash->{isChild} = 1; + + } # fork failed and continue in parent + } + } + + $FW_httpRetCode = "200 OK"; + my $cacheable = FW_answerCall($arg); + if($cacheable == -1) { + FW_closeConn($hash); + return; + } + return if($cacheable == -2); # async op, well be answered later + FW_finishRead($hash, $cacheable, $arg); + +} + +sub +FW_finishRead($$$) +{ + my ($hash, $cacheable, $arg) = @_; + my $name = $hash->{NAME}; + + my $compressed = ""; + if($FW_RETTYPE =~ m/(text|xml|json|svg|script)/i && + ($FW_httpheader{"Accept-Encoding"} && + $FW_httpheader{"Accept-Encoding"} =~ m/gzip/) && + $FW_use{zlib}) { + utf8::encode($FW_RET) + if(utf8::is_utf8($FW_RET) && $FW_RET =~ m/[^\x00-\xFF]/ ); + eval { $FW_RET = Compress::Zlib::memGzip($FW_RET); }; + if($@) { + Log 1, "memGzip: $@"; $FW_RET=""; #Forum #29939 + } else { + $compressed = "Content-Encoding: gzip\r\n"; + } + } + + my $length = length($FW_RET); + my $expires = ($cacheable ? + "Expires: ".FmtDateTimeRFC1123($hash->{LASTACCESS}+900)."\r\n" : + "Cache-Control: no-cache, no-store, must-revalidate\r\n"); + Log3 $FW_wname, 4, + "$FW_wname: $arg / RL:$length / $FW_RETTYPE / $compressed / $expires"; + if( ! FW_addToWritebuffer($hash, + "HTTP/1.1 $FW_httpRetCode\r\n" . + "Content-Length: $length\r\n" . + $expires . $compressed . $FW_headerlines . + "Content-Type: $FW_RETTYPE\r\n\r\n" . + $FW_RET, "FW_closeConn", 1) ){ + Log3 $name, 4, "Closing connection $name due to full buffer in FW_Read" + if(!$hash->{isChild}); + FW_closeConn($hash); + TcpServer_Close($hash, 1); + } +} + +sub +FW_initInform($$) +{ + my ($me, $longpoll) = @_; + + if($FW_inform =~ /type=/) { + foreach my $kv (split(";", $FW_inform)) { + my ($key,$value) = split("=", $kv, 2); + $me->{inform}{$key} = $value; + } + + } else { # Compatibility mode + $me->{inform}{type} = ($FW_room ? "status" : "raw"); + $me->{inform}{filter} = ($FW_room ? $FW_room : ".*"); + } + $FW_id2inform{$FW_id} = $me if($FW_id); + + my $filter = $me->{inform}{filter}; + $filter =~ s/([[\]().+?])/\\$1/g if($filter =~ m/room=/); # Forum #80390 + $filter = "NAME=.*" if($filter eq "room=all"); + $filter = "room!=.+" if($filter eq "room=Unsorted"); + + my %h = map { $_ => 1 } devspec2array($filter); + $h{global} = 1 if( $me->{inform}{addglobal} ); + $h{"#FHEMWEB:$FW_wname"} = 1; + $me->{inform}{devices} = \%h; + %FW_visibleDeviceHash = FW_visibleDevices(); + + # NTFY_ORDER is larger than the normal order (50-) + $me->{NTFY_ORDER} = $FW_cname; # else notifyfn won't be called + %ntfyHash = (); + $me->{inform}{since} = time()-5 + if(!defined($me->{inform}{since}) || $me->{inform}{since} !~ m/^\d+$/); + + my $sinceTimestamp = FmtDateTime($me->{inform}{since}); + if($longpoll) { + TcpServer_WriteBlocking($me, + "HTTP/1.1 200 OK\r\n". + $FW_headerlines. + "Content-Type: application/octet-stream; charset=$FW_encoding\r\n\r\n". + FW_roomStatesForInform($me, $sinceTimestamp)); + + } else { # websocket + FW_addToWritebuffer($me, + FW_roomStatesForInform($me, $sinceTimestamp)); + } + + if($FW_id && $defs{$FW_wname}{asyncOutput}) { + my $data = $defs{$FW_wname}{asyncOutput}{$FW_id}; + if($data) { + FW_addToWritebuffer($me, $data."\n"); + delete $defs{$FW_wname}{asyncOutput}{$FW_id}; + } + } + + if($me->{inform}{withLog}) { + $logInform{$me->{NAME}} = "FW_logInform"; + } else { + delete($logInform{$me->{NAME}}); + } +} + + +sub +FW_addToWritebuffer($$@) +{ + my ($hash, $txt, $callback, $nolimit) = @_; + + if( $hash->{websocket} ) { + my $len = length($txt); + if( $len < 126 ) { + $txt = chr(0x81) . chr($len) . $txt; + } else { + if ( $len < 65536 ) { + $txt = chr(0x81) . chr(0x7E) . pack('n', $len) . $txt; + } else { + $txt = chr(0x81) . chr(0x7F) . chr(0x00) . chr(0x00) . + chr(0x00) . chr(0x00) . pack('N', $len) . $txt; + } + } + } + return addToWritebuffer($hash, $txt, $callback, $nolimit); +} + +sub +FW_AsyncOutput($$) +{ + my ($hash, $ret) = @_; + + return if(!$hash || !$hash->{FW_ID}); + if( $ret =~ m/^(.*)<\/html>$/s ) { + $ret = $1; + + } else { + $ret = FW_htmlEscape($ret); + $ret = "
$ret
" if($ret =~ m/\n/ ); + $ret =~ s/\n/
/g; + } + + my $data = FW_longpollInfo('JSON', + "#FHEMWEB:$FW_wname","FW_okDialog('$ret')",""); + + # find the longpoll connection with the same fw_id as the page that was the + # origin of the get command + my $fwid = $hash->{FW_ID}; + if(!$fwid) { + Log3 $hash->{SNAME}, 4, "AsyncOutput from $hash->{NAME} without FW_ID"; + return; + } + Log3 $hash->{SNAME}, 4, "AsyncOutput from $hash->{NAME}"; + $hash = $FW_id2inform{$fwid}; + if($hash) { + FW_addToWritebuffer($hash, $data."\n"); + } else { + $defs{$FW_wname}{asyncOutput}{$fwid} = $data; + } + return undef; +} + +sub +FW_closeConn($) +{ + my ($hash) = @_; + if(!$hash->{inform} && !$hash->{BUF}) { # Forum #41125 + my $cc = AttrVal($hash->{SNAME}, "closeConn", + $FW_userAgent =~ m/(iPhone|iPad|iPod)/); + if(!$FW_httpheader{Connection} || $cc) { + TcpServer_Close($hash, 1); + } + } + + POSIX::exit(0) if($hash->{isChild}); + FW_Read($hash, 1) if($hash->{BUF}); +} + +########################### +sub +FW_serveSpecial($$$$) +{ + my ($file,$ext,$dir,$cacheable)= @_; + $file =~ s,\.\./,,g; # little bit of security + + $file = "$FW_sp$file" if($ext eq "css" && -f "$dir/$FW_sp$file.$ext"); + $FW_RETTYPE = ext2MIMEType($ext); + my $fname = ($ext ? "$file.$ext" : $file); + return FW_returnFileAsStream("$dir/$fname", "", $FW_RETTYPE, 0, $cacheable); +} + +sub +FW_answerCall($) +{ + my ($arg) = @_; + my $me=$defs{$FW_cname}; # cache, else rereadcfg will delete us + + $FW_RET = ""; + $FW_RETTYPE = "text/html; charset=$FW_encoding"; + + $MW_dir = "$attr{global}{modpath}/FHEM"; + $FW_sp = AttrVal($FW_wname, "stylesheetPrefix", "f18"); + $FW_ss = ($FW_sp =~ m/smallscreen/); + $FW_tp = ($FW_sp =~ m/smallscreen|touchpad/); + my $spDir = ($FW_sp eq "default" ? "" : "$FW_sp:"); + @FW_iconDirs = grep { $_ } split(":", AttrVal($FW_wname, "iconPath", + "${spDir}fhemSVG:openautomation:default")); + @FW_fhemwebjs = ("fhemweb.js"); + push(@FW_fhemwebjs, "$FW_sp.js") if(-r "$FW_dir/pgm2/$FW_sp.js"); + + if($arg =~ m,$FW_ME/floorplan/([a-z0-9.:_]+),i) { # FLOORPLAN: special icondir + unshift @FW_iconDirs, $1; + FW_readIcons($1); + } + + # /icons/... => current state of ... + # also used for static images: unintended, but too late to change + + my ($dir1, $dirN, $ofile) = ($1, $2, $3) + if($arg =~ m,^$FW_ME/([^/]*)(.*/)([^/]*)$,); + if($arg =~ m,\brobots.txt$,) { + Log3 $FW_wname, 1, "NOTE: $FW_wname is probed by a search engine"; + $FW_RETTYPE = "text/plain; charset=$FW_encoding"; + FW_pO "User-agent: *\r"; + FW_pO "Disallow: *\r"; + return 0; + + } elsif($arg =~ m,^$FW_ME/icons/(.*)$,) { + my ($icon,$cacheable) = (urlDecode($1), 1); + my $iconPath = FW_iconPath($icon); + + # if we do not have the icon, we convert the device state to the icon name + if(!$iconPath) { + my ($img, $link, $isHtml) = FW_dev2image($icon); + $cacheable = 0; + return 0 if(!$img); + $iconPath = FW_iconPath($img); + if($iconPath =~ m/\.svg$/i) { + $FW_RETTYPE = ext2MIMEType("svg"); + FW_pO FW_makeImage($img, $img); + return 0; + } + } elsif($iconPath =~ m/\.svg$/i && $icon=~ m/@/) { + $FW_RETTYPE = ext2MIMEType("svg"); + FW_pO FW_makeImage($icon, $icon); + return 0; + } + $iconPath =~ m/(.*)\.([^.]*)/; + return FW_serveSpecial($1, $2, $FW_icondir, $cacheable); + + } elsif($dir1 && !$data{FWEXT}{"/$dir1"}) { + my $dir = "$dir1$dirN"; + my $ext = ""; + $dir =~ s,/$,,; + $dir =~ s/\.\.//g; + $dir =~ s,www/,,g; # Want commandref.html to work from file://... + + my $file = urlDecode($ofile); # 69164 + $file =~ s/\?.*//; # Remove timestamp of CSS reloader + if($file =~ m/^(.*)\.([^.]*)$/) { + $file = $1; $ext = $2; + } + my $ldir = "$FW_dir/$dir"; + $ldir = "$FW_dir/pgm2" if($dir eq "css" || $dir eq "js"); # FLOORPLAN compat + $ldir = "$attr{global}{modpath}/docs" if($dir eq "docs"); + + # pgm2 check is for jquery-ui images + my $static = ($ext =~ m/(css|js|png|jpg)/i || $dir =~ m/^pgm2/); + my $fname = ($ext ? "$file.$ext" : $file); + return FW_serveSpecial($file, $ext, $ldir, ($arg =~ m/nocache/) ? 0 : 1) + if(-r "$ldir/$fname" || $static); # no return for FLOORPLAN + $arg = "/$dir/$ofile"; + + } elsif($arg =~ m/^$FW_ME(.*)/s) { + $arg = $1; # The stuff behind FW_ME, continue to check for commands/FWEXT + + } else { + Log3 $FW_wname, 4, "$FW_wname: redirecting $arg to $FW_ME"; + TcpServer_WriteBlocking($me, + "HTTP/1.1 302 Found\r\n". + "Content-Length: 0\r\n". + $FW_headerlines. + "Location: $FW_ME\r\n\r\n"); + FW_closeConn($FW_chash); + return -1; + } + + + $FW_plotmode = AttrVal($FW_wname, "plotmode", "SVG"); + $FW_plotsize = AttrVal($FW_wname, "plotsize", $FW_ss ? "480,160" : + $FW_tp ? "640,160" : "800,160"); + my ($cmd, $cmddev) = FW_digestCgi($arg); + if($cmd && $FW_CSRF && $cmd !~ m/style (list|select|eventMonitor)/) { + my $supplied = defined($FW_webArgs{fwcsrf}) ? $FW_webArgs{fwcsrf} : ""; + my $want = $defs{$FW_wname}{CSRFTOKEN}; + if($supplied ne $want) { + Log3 $FW_wname, 3, "FHEMWEB $FW_wname CSRF error: $supplied ne $want ". + "for client $FW_chash->{NAME} / command $cmd. ". + "For details see the csrfToken FHEMWEB attribute."; + $FW_httpRetCode = "400 Bad Request"; + return 0; + } + } + + if( $FW_id ) { + $me->{FW_ID} = $FW_id; + $me->{canAsyncOutput} = 1; + } + + if($FW_inform) { # Longpoll header + FW_initInform($me, 1); + return -1; + } + + my $docmd = 0; + $docmd = 1 if($cmd && + $cmd !~ /^showlog/ && + $cmd !~ /^style / && + $cmd !~ /^edit/); + + #If we are in XHR or json mode, execute the command directly + if($FW_XHR || $FW_jsonp) { + $FW_cmdret = $docmd ? FW_fC($cmd, $cmddev) : undef; + $FW_RETTYPE = $FW_chash->{contenttype} ? + $FW_chash->{contenttype} : "text/plain; charset=$FW_encoding"; + delete($FW_chash->{contenttype}); + + if($FW_jsonp) { + $FW_cmdret =~ s/'/\\'/g; + # Escape newlines in JavaScript string + $FW_cmdret =~ s/\n/\\\n/g; + FW_pO "$FW_jsonp('$FW_cmdret');"; + + } else { + $FW_cmdret = FW_addLinks($FW_cmdret) if($FW_webArgs{addLinks}); + FW_pO $FW_cmdret; + + } + return 0; + } + ############################## + # FHEMWEB extensions (FLOORPLOAN, SVG_WriteGplot, etc) + my $FW_contentFunc; + if(defined($data{FWEXT})) { + foreach my $k (sort keys %{$data{FWEXT}}) { + my $h = $data{FWEXT}{$k}; + next if($arg !~ m/^$k/); + $FW_contentFunc = $h->{CONTENTFUNC}; + next if($h !~ m/HASH/ || !$h->{FUNC}); + #Returns undef as FW_RETTYPE if it already sent a HTTP header + no strict "refs"; + ($FW_RETTYPE, $FW_RET) = &{$h->{FUNC}}($arg); + if(defined($FW_RETTYPE) && $FW_RETTYPE =~ m,text/html,) { + my $dataAttr = FW_dataAttr(); + $FW_RET =~ s/'; + FW_pO ''; + FW_pO "\n$t"; + FW_pO ''; + FW_pO ""; # Forum 28666 + FW_pO "";#Forum 18316 + + # Enable WebApps + if($FW_tp || $FW_ss) { + my $icon = FW_iconPath("fhemicon_ios.png"); + $icon = $FW_ME."/images/".($icon ? $icon : "default/fhemicon_ios.png"); + my $viewport = ''; + if($FW_ss) { + my $stf = $FW_userAgent =~ m/iPad|iPhone|iPod/ ? ",shrink-to-fit=no" :""; + $viewport = "initial-scale=1.0,user-scalable=1$stf"; + } elsif($FW_tp) { + $viewport = "width=768"; + } + $viewport = AttrVal($FW_wname, "viewport", $viewport); + FW_pO '' if ($viewport); + FW_pO ''; + FW_pO ''; # Forum #36183 + FW_pO ''; + FW_pO ''; + } + + if(!$FW_detail) { + my $rf = AttrVal($FW_wname, "refresh", ""); + FW_pO "" if($rf); + } + + ######################## + # CSS + my $cssTemplate = ""; + FW_pO sprintf($cssTemplate, "pgm2/style.css?v=$FW_styleStamp"); + FW_pO sprintf($cssTemplate, "pgm2/jquery-ui.min.css"); + map { FW_pO sprintf($cssTemplate, $_); } + split(" ", AttrVal($FW_wname, "CssFiles", "")); + + my $sd = AttrVal($FW_wname, "styleData", ""); # Avoid flicker in f18 + if($sd && $sd =~ m/"$FW_sp":/s) { + my $bg; + $bg = $1 if($FW_room && $sd =~ m/"Room\.$FW_room\.cols.bg": "([^"]*)"/s); + $bg = $1 if(!defined($bg) && $sd =~ m/"cols.bg": "([^"]*)"/s); + + my $bgImg; + $bgImg = $1 if($FW_room && $sd =~ m/"Room\.$FW_room\.bgImg": "([^"]*)"/s); + $bgImg = $1 if(!defined($bgImg) && $sd =~ m/"bgImg": "([^"]*)"/s); + + FW_pO ""; + } + + my $css = AttrVal($FW_wname, "Css", ""); + FW_pO "\n" if($css); + + ######################## + # JavaScripts + my $jsTemplate = + ''; + FW_pO sprintf($jsTemplate, "", "$FW_ME/pgm2/jquery.min.js"); + FW_pO sprintf($jsTemplate, "", "$FW_ME/pgm2/jquery-ui.min.js"); + + my (%jsNeg, @jsList); # jsNeg was used to exclude automatically loaded files + map { $_ =~ m/^-(.*)$/ ? $jsNeg{$1} = 1 : push(@jsList, $_); } + split(" ", AttrVal($FW_wname, "JavaScripts", "")); + map { FW_pO sprintf($jsTemplate, "", "$FW_ME/pgm2/$_") if(!$jsNeg{$_}); } + @FW_fhemwebjs; + + ####################### + # "Own" JavaScripts + their Attributes + map { + my $n = $_; $n =~ s+.*/++; $n =~ s/.js$//; $n =~ s/fhem_//; $n .= "Param"; + FW_pO sprintf($jsTemplate, AttrVal($FW_wname, $n, ""), "$FW_ME/$_"); + } @jsList; + + ######################## + # FW Extensions + if(defined($data{FWEXT})) { + foreach my $k (sort keys %{$data{FWEXT}}) { + my $h = $data{FWEXT}{$k}; + next if($h !~ m/HASH/ || !$h->{SCRIPT} || $h->{SCRIPT} =~ m+pgm2/jquery+); + my $script = $h->{SCRIPT}; + $script = ($script =~ m,^/,) ? "$FW_ME$script" : "$FW_ME/pgm2/$script"; + FW_pO sprintf($jsTemplate, "", $script); + } + } + + my $csrf= ($FW_CSRF ? "fwcsrf='$defs{$FW_wname}{CSRFTOKEN}'" : ""); + my $gen = 'generated="'.(time()-1).'"'; + my $lp = 'longpoll="'.AttrVal($FW_wname,"longpoll", + $FW_use{sha} && $FW_userAgent=~m/Chrome/ ? "websocket": 1).'"'; + $FW_id = $FW_chash->{NR} if( !$FW_id ); + + my $dataAttr = FW_dataAttr(); + FW_pO "\n"; + + if($FW_activateInform) { + $cmd = "style eventMonitor $FW_activateInform"; + $FW_cmdret = undef; + $FW_activateInform = ""; + } + + FW_roomOverview($cmd); + + if(defined($FW_cmdret)) { + $FW_detail = ""; + $FW_room = ""; + + if( $FW_cmdret =~ m/^(.*)<\/html>$/s ) { + $FW_cmdret = $1; + + } else { # "linkify" output (e.g. for list) + $FW_cmdret = FW_addLinks(FW_htmlEscape($FW_cmdret)); + $FW_cmdret =~ s/:\S+//g if($FW_cmdret =~ m/unknown.*choose one of/i); + $FW_cmdret = "
$FW_cmdret
" if($FW_cmdret =~ m/\n/); + } + + FW_addContent(); + if($FW_ss) { + FW_pO "
$FW_cmdret
"; + } else { + FW_pO $FW_cmdret; + } + FW_pO ""; + + } + + if($FW_contentFunc) { + no strict "refs"; + my $ret = &{$FW_contentFunc}($arg); + use strict "refs"; + return $ret if($ret); + } + + my $srVal = 0; + if($cmd =~ m/^style /) { FW_style($cmd,undef); } + elsif($FW_detail) { FW_doDetail($FW_detail); } + elsif($FW_room) { $srVal = FW_showRoom(); } + elsif(!defined($FW_cmdret) && + !$FW_contentFunc) { + + $FW_room = AttrVal($FW_wname, "defaultRoom", ''); + if($FW_room ne '') { + $srVal = FW_showRoom(); + + } else { + my $motd = AttrVal("global","motd","none"); + if($motd ne "none") { + FW_addContent(">
$motd
"; + return 0; +} + +sub +FW_dataAttr() +{ + sub + addParam($$) + { + my ($p, $default) = @_; + my $val = AttrVal($FW_wname,$p, $default); + $val =~ s/&/&/g; + $val =~ s/'/"/g; + return "data-$p='$val' "; + } + + return + addParam("confirmDelete", 1). + addParam("confirmJSError", 1). + addParam("addHtmlTitle", 1). + addParam("styleData", ""). + "data-availableJs='$FW_fhemwebjs' ". + "data-webName='$FW_wname '"; +} + +sub +FW_addContent(;$) +{ + my $add = ($_[0] ? " $_[0]" : ""); + FW_pO "
"; +} + +sub +FW_addLinks($) +{ + my ($txt) = @_; + return undef if(!defined($txt)); + $txt =~ s,\b([a-z0-9._]+)\b, + $defs{$1} ? "$1" : $1,gei; + return $txt; +} + + +########################### +# Digest CGI parameters +sub +FW_digestCgi($) +{ + my ($arg) = @_; + my (%arg, %val, %dev); + my ($cmd, $c) = ("","",""); + + %FW_pos = (); + $FW_room = ""; + $FW_detail = ""; + $FW_XHR = undef; + $FW_id = ""; + $FW_jsonp = undef; + $FW_inform = undef; + + %FW_webArgs = (); + #Remove (nongreedy) everything including the first '?' + $arg =~ s,^.*?[?],,; + foreach my $pv (split("&", $arg)) { + next if($pv eq ""); # happens when post forgot to set FW_ME + $pv =~ s/\+/ /g; + $pv =~ s/%([\dA-F][\dA-F])/chr(hex($1))/ige; + my ($p,$v) = split("=",$pv, 2); + $v = "" if(!defined($v)); + + # Multiline: escape the NL for fhem + $v =~ s/[\r]//g if($v && $p && $p ne "data"); + $FW_webArgs{$p} = $v; + + if($p eq "detail") { $FW_detail = $v; } + if($p eq "room") { $FW_room = $v; } + if($p eq "cmd") { $cmd = $v; } + if($p =~ m/^arg\.(.*)$/) { $arg{$1} = $v; } + if($p =~ m/^val\.(.*)$/) { $val{$1} = ($val{$1} ? $val{$1}.",$v" : $v) } + if($p =~ m/^dev\.(.*)$/) { $dev{$1} = $v; } + if($p =~ m/^cmd\.(.*)$/) { $cmd = $v; $c = $1; } + if($p eq "pos") { %FW_pos = split(/[=;]/, $v); } + if($p eq "data") { $FW_data = $v; } + if($p eq "XHR") { $FW_XHR = 1; } + if($p eq "fw_id") { $FW_id = $v; } + if($p eq "jsonp") { $FW_jsonp = $v; } + if($p eq "inform") { $FW_inform = $v; } + + } + $cmd.=" $dev{$c}" if(defined($dev{$c})); + $cmd.=" $arg{$c}" if(defined($arg{$c})); + $cmd.=" $val{$c}" if(defined($val{$c})); + + #replace unicode newline symbol \u2424 with real newline + my $nl = chr(226) . chr(144) . chr(164); + $cmd =~ s/$nl/\n/g; + + return ($cmd, $c); +} + +##################### +# create FW_rooms && FW_types +sub +FW_updateHashes() +{ + %FW_rooms = (); # Make a room hash + %FW_groups = (); # Make a group hash + %FW_types = (); # Needed for type sorting + + my $hre = AttrVal($FW_wname, "hiddenroomRegexp", ""); + foreach my $d (keys %defs ) { + next if(IsIgnored($d)); + + foreach my $r (split(",", AttrVal($d, "room", "Unsorted"))) { + next if($hre && $r =~ m/$hre/); + $FW_rooms{$r}{$d} = 1; + } + foreach my $r (split(",", AttrVal($d, "group", ""))) { + $FW_groups{$r}{$d} = 1; + } + my $t = AttrVal($d, "subType", $defs{$d}{TYPE}); + $t = AttrVal($d, "model", $t) if($t && $t eq "unknown"); # RKO: ??? + $FW_types{$d} = $t; + } + + $FW_room = AttrVal($FW_detail, "room", "Unsorted") if($FW_detail); + + if(AttrVal($FW_wname, "sortRooms", "")) { # Slow! + my @sortBy = split( " ", AttrVal( $FW_wname, "sortRooms", "" ) ); + my %sHash; + map { $sHash{$_} = FW_roomIdx(\@sortBy,$_) } keys %FW_rooms; + @FW_roomsArr = sort { $sHash{$a} cmp $sHash{$b} } keys %FW_rooms; + + } else { + @FW_roomsArr = sort keys %FW_rooms; + + } +} + +############################## +sub +FW_makeTable($$$@) +{ + my($title, $name, $hash, $cmd) = (@_); + + return if(!$hash || !int(keys %{$hash})); + my $class = lc($title); + $class =~ s/[^A-Za-z]/_/g; + FW_pO "
"; + FW_pO "$title"; + FW_pO ""; + my $si = AttrVal("global", "showInternalValues", 0); + + my $row = 1; + my $prefix = ($title eq "Attributes" ? "a-" : ""); + foreach my $n (sort keys %{$hash}) { + next if(!$si && $n =~ m/^\./); # Skip "hidden" Values + my $val = $hash->{$n}; + $val = "" if(!defined($val)); + + $val = $hash->{$n}{NAME} # Exception + if($n eq "IODev" && ref($val) eq "HASH" && defined($hash->{$n}{NAME})); + + my $r = ref($val); + next if($r && ($r ne "HASH" || !defined($hash->{$n}{VAL}))); + + FW_pF "", ($row&1)?"odd":"even"; + $row++; + + if($n eq "DEF" && !$FW_hiddenroom{input}) { + FW_makeEdit($name, $n, $val); + + } else { + FW_pO ""; + + if(ref($val)) { #handle readings + my ($v, $t) = ($val->{VAL}, $val->{TIME}); + if($v =~ m,^(.*)$,) { + $v = $1; + } else { + $v = FW_htmlEscape($v); + $v = "
$v
" if($v =~ m/\n/); + } + + my $ifid = "class='dval' informId='$name-$prefix$n'"; + my $ifidts = "informId='$name-$prefix$n-ts'"; + if($FW_ss) { + $t = ($t ? "
$t
" : ""); + FW_pO ""; + } else { + $t = "" if(!$t); + FW_pO ""; + FW_pO ""; + } + } else { + $val = FW_htmlEscape($val); + my $tattr = "informId=\"$name-$prefix$n\" class=\"dval\""; + + # if possible provide some links + if ($n eq "room"){ + FW_pO ""; + + } elsif ($n =~ m/^fp_(.*)/ && $defs{$1}){ #special for Floorplan + FW_pH "detail=$1", $val,1; + + } elsif ($modules{$val} ) { + FW_pH "cmd=list%20TYPE=$val", $val,1; + + } else { + $val = "
$val
" if($val =~ m/\n/); + FW_pO ""; + } + } + + } + + FW_pH "cmd.$name=$cmd $name $n&detail=$name", $cmd, 1 + if($cmd && !$FW_ss); + FW_pO ""; + } + FW_pO "
$n
$v$t
$v
$t
". + join(",", map { FW_pH("room=$_",$_,0,"",1,1) } split(",",$val)). + "
". + join(",", map { ($_ ne $name && $defs{$_}) ? + FW_pH( "detail=$_", $_ ,0,"",1,1) : $_ } split(",",$val)). + "
"; + FW_pO "
"; + +} + +############################## +# Used only for set or attr lists. +sub +FW_detailSelect(@) +{ + my ($d, $cmd, $list, $param) = @_; + return "" if(!$list || $FW_hiddenroom{input}); + my %al = map { s/:.*//;$_ => 1 } split(" ", $list); + my @al = sort keys %al; # remove duplicate items in list + + my $selEl = (defined($al[0]) ? $al[0] : " "); + $selEl = $1 if($list =~ m/([^ ]*):slider,/); # promote a slider if available + $selEl = "room" if($list =~ m/room:/); + $list =~ s/"/"/g; + + my $ret =""; + my $psc = AttrVal("global", "perlSyntaxCheck", ($featurelevel>5.7) ? 1 : 0); + $ret .= "
"; + $ret .= "
"; + $ret .= FW_hidden("detail", $d); + $ret .= FW_hidden("dev.$cmd$d", $d.($param ? " $param":"")); + $ret .= FW_submit("cmd.$cmd$d", $cmd, $cmd.($psc?" psc":"")); + $ret .= "
 $d ". + ($param ? " $param":"")."
"; + $ret .= FW_select("sel_$cmd$d","arg.$cmd$d",\@al, $selEl, $cmd); + $ret .= FW_textfield("val.$cmd$d", 30, $cmd); + $ret .= "
"; + return $ret; +} + +############################## +sub +FW_doDetail($) +{ + my ($d) = @_; + + return if($FW_hiddenroom{detail}); + return if(!defined($defs{$d})); + my $h = $defs{$d}; + my $t = $h->{TYPE}; + $t = "MISSING" if(!defined($t)); + FW_addContent(); + + if($FW_ss) { + my $webCmd = AttrVal($d, "webCmd", undef); + if($webCmd) { + FW_pO ""; + foreach my $cmd (split(":", $webCmd)) { + FW_pO ""; + FW_pH "cmd.$d=set $d $cmd&detail=$d", $cmd, 1, "col1"; + FW_pO ""; + } + FW_pO "
"; + } + } + FW_pO "
"; + + if(!$modules{$t}{FW_detailFn} || $modules{$t}{FW_deviceOverview}) { + my $show = AttrVal($FW_wname, "deviceOverview", "always"); + + if( $show ne 'never' ) { + my %extPage = (); + + if( $show eq 'iconOnly' ) { + my ($allSets, $cmdlist, $txt) = FW_devState($d, $FW_room, \%extPage); + FW_pO "
$txt
"; + + } else { + my $nameDisplay = AttrVal($FW_wname,"nameDisplay",undef); + my %usuallyAtEnd = (); + + my $style = ""; + if( $show eq 'onClick' ) { + my $pgm = "Javascript:" . + "s=document.getElementById('ddtable').style;". + "s.display = s.display=='none' ? 'block' : 'none';". + "s=document.getElementById('ddisp').style;". + "s.display = s.display=='none' ? 'block' : 'none';"; + FW_pO ""; + $style = 'style="display:none"'; + } + + FW_pO "
"; + FW_pO "DeviceOverview"; + FW_pO ""; + FW_makeDeviceLine($d,1,\%extPage,$nameDisplay,\%usuallyAtEnd); + FW_pO "
"; + } + } + } + if($modules{$t}{FW_detailFn}) { + no strict "refs"; + my $txt = &{$modules{$t}{FW_detailFn}}($FW_wname, $d, $FW_room); + FW_pO "
$txt
" if(defined($txt)); + use strict "refs"; + } + + + FW_pO FW_detailSelect($d, "set", + FW_widgetOverride($d, getAllSets($d, $FW_chash))); + FW_pO FW_detailSelect($d, "get", + FW_widgetOverride($d, getAllGets($d, $FW_chash))); + + FW_makeTable("Internals", $d, $h); + FW_makeTable("Readings", $d, $h->{READINGS}); + + my $attrList = getAllAttr($d); + my $roomList = "multiple,".join(",", + sort map { $_ =~ s/ /#/g ;$_} keys %FW_rooms); + my $groupList = "multiple,".join(",", + sort map { $_ =~ s/ /#/g ;$_} keys %FW_groups); + $attrList =~ s/room /room:$roomList /; + $attrList =~ s/group /group:$groupList /; + $attrList = FW_widgetOverride($d, $attrList); + $attrList =~ s/\\/\\\\/g; + $attrList =~ s/'/\\'/g; + FW_pO FW_detailSelect($d, "attr", $attrList); + + FW_makeTable("Attributes", $d, $attr{$d}, "deleteattr"); + FW_makeTableFromArray("Probably associated with", "assoc", getPawList($d)); + + FW_pO "
"; + + my ($link, $txt, $td, $class, $doRet,$nonl) = @_; + + FW_pH "cmd=style iconFor $d", "Select icon", undef, "detLink iconFor"; + FW_pH "cmd=style showDSI $d", "Extend devStateIcon", undef, "detLink showDSI"; + FW_pH "cmd=rawDef $d", "Raw definition", undef, "detLink rawDef"; + FW_pH "cmd=delete $d", "Delete this device ($d)", undef, "detLink delDev" + if($d ne "global"); + my $sfx = AttrVal("global", "language", "EN"); + $sfx = ($sfx eq "EN" ? "" : "_$sfx"); + FW_pH "$FW_ME/docs/commandref${sfx}.html#${t}", "Device specific help", + undef, "detLink devSpecHelp"; + FW_pO "

"; + FW_pO "
"; + +} + +############################## +sub +FW_makeTableFromArray($$@) { + my ($txt,$class,@obj) = @_; + if (@obj>0) { + my $row=1; + FW_pO "
"; + FW_pO "$txt"; + FW_pO ""; + foreach (sort @obj) { + FW_pF ""; + FW_pO ""; + } + FW_pO "
", (($row++)&1)?"odd":"even"; + FW_pH "detail=$_", $_; + FW_pO ""; + FW_pO $defs{$_}{STATE} if(defined($defs{$_}{STATE})); + FW_pO ""; + FW_pH "cmd=list TYPE=$defs{$_}{TYPE}", $defs{$_}{TYPE}; + FW_pO "
"; + } +} + +sub +FW_roomIdx($$) +{ + my ($arr,$v) = @_; + my ($index) = grep { $v =~ /^$arr->[$_]$/ } 0..$#$arr; + + if( !defined($index) ) { + $index = 9999; + } else { + $index = sprintf( "%03i", $index ); + } + + return "$index-$v"; +} + + +############## +# Header, Zoom-Icons & list of rooms at the left. +sub +FW_roomOverview($) +{ + my ($cmd) = @_; + + %FW_hiddenroom = (); + map { $FW_hiddenroom{$_}=1 } split(",",AttrVal($FW_wname,"hiddenroom", "")); + map { $FW_hiddenroom{$_}=1 } split(",",AttrVal($FW_wname,"forbiddenroom","")); + + ############## + # LOGO + my $hasMenuScroll; + if($FW_detail && $FW_ss) { + $FW_room = AttrVal($FW_detail, "room", undef); + $FW_room = $1 if($FW_room && $FW_room =~ m/^([^,]*),/); + $FW_room = "" if(!$FW_room); + FW_pO(FW_pHPlain("room=$FW_room", + "
" . FW_makeImage("back") . "
")); + FW_pO "
$FW_detail details
"; + return; + + } else { + $hasMenuScroll = 1; + FW_pO '" if($hasMenuScroll); + + ############## + # HEADER + FW_pO "
"; + FW_pO '
'; + FW_pO "
"; + FW_pO FW_hidden("fw_id", $FW_id) if($FW_id); + FW_pO FW_hidden("room", $FW_room) if($FW_room); + FW_pO FW_hidden("fwcsrf", $defs{$FW_wname}{CSRFTOKEN}) if($FW_CSRF); + FW_pO FW_textfield("cmd", + AttrVal($FW_wname, "mainInputLength", $FW_ss ? 25 : 40), "maininput"); + FW_pO "
"; + FW_pO "
"; + FW_pO "
"; + +} + +sub +FW_alias($) +{ + my ($d) = @_; + if($FW_room) { + return AttrVal($d, "alias_$FW_room", AttrVal($d, "alias", $d)); + } else { + return AttrVal($d, "alias", $d); + } +} + +sub +FW_makeDeviceLine($$$$$) +{ + my ($d,$row,$extPage,$nameDisplay,$usuallyAtEnd) = @_; + my $rf = ($FW_room ? "&room=$FW_room" : ""); # stay in the room + + FW_pF "\n", ($row&1)?"odd":"even"; + my $devName = FW_alias($d); + if(defined($nameDisplay)) { + my ($DEVICE, $ALIAS) = ($d, $devName); + $devName = eval $nameDisplay; + } + my $icon = AttrVal($d, "icon", ""); + $icon = FW_makeImage($icon,$icon,"icon") . " " if($icon); + + $devName="" if($modules{$defs{$d}{TYPE}}{FW_hideDisplayName}); # Forum 88667 + if(!$usuallyAtEnd->{$d}) { + if($FW_hiddenroom{detail}) { + FW_pO "
$icon$devName
"; + } else { + FW_pH "detail=$d", "$icon$devName", 1, "col1"; + } + } + + my ($allSets, $cmdlist, $txt) = FW_devState($d, $rf, $extPage); + if($cmdlist) { + my $cl2 = $cmdlist; $cl2 =~ s/ [^:]*//g; $cl2 =~ s/:/ /g; # Forum #74053 + $allSets = "$allSets $cl2"; + } + $allSets = FW_widgetOverride($d, $allSets); + + my $colSpan = ($usuallyAtEnd->{$d} ? ' colspan="2"' : ''); + FW_pO "$txt"; + + ###### + # Commands, slider, dropdown + my $smallscreenCommands = AttrVal($FW_wname, "smallscreenCommands", ""); + if((!$FW_ss || $smallscreenCommands) && $cmdlist) { + my @a = split("[: ]", AttrVal($d, "cmdIcon", "")); + Log 1, "ERROR: bad cmdIcon definition for $d" if(@a % 2); + my %cmdIcon = @a; + + my @cl = split(":", $cmdlist); + my @wcl = split(":", AttrVal($d, "webCmdLabel", "")); + my $nRows; + $nRows = split("\n", AttrVal($d, "webCmdLabel", "")) if(@wcl); + @wcl = () if(@wcl != @cl); # some safety + + for(my $i1=0; $i1<@cl; $i1++) { + my $cmd = $cl[$i1]; + my $htmlTxt; + my @c = split(' ', $cmd); # @c==0 if $cmd==" "; + if(int(@c) && $allSets && $allSets =~ m/\b$c[0]:([^ ]*)/) { + my $values = $1; + foreach my $fn (sort keys %{$data{webCmdFn}}) { + no strict "refs"; + $htmlTxt = &{$data{webCmdFn}{$fn}}($FW_wname, + $d, $FW_room, $cmd, $values); + use strict "refs"; + last if(defined($htmlTxt)); + } + } + + if($htmlTxt) { + $htmlTxt =~ s,^]*>(.*)$,$1,; + } else { + my $nCmd = $cmdIcon{$cmd} ? + FW_makeImage($cmdIcon{$cmd},$cmd,"webCmd") : $cmd; + $htmlTxt = FW_pH "cmd.$d=set $d $cmd$rf", $nCmd, 0, "", 1, 1; + } + + if(@wcl > $i1) { + if($nRows > 1) { + FW_pO "" if($i1 == 0); + FW_pO ""; + FW_pO "" if($wcl[$i1] =~ m/\n/); + FW_pO "
$wcl[$i1]$htmlTxt
" if($i1 == @cl-1); + } else { + FW_pO "
$wcl[$i1]$ htmlTxt
"; + } + + } else { + FW_pO "
$htmlTxt
"; + } + } + } + FW_pO ""; +} + +sub +FW_sortIndex($) +{ + my ($d) = @_; + return $d if(!$attr{$d}); + + my $val = $attr{$d}{sortby}; + if($val) { + if($val =~ m/^{.*}/) { + my %specials=("%NAME" => $d); + my $exec = EvalSpecials($val, %specials); + return AnalyzePerlCommand($FW_chash, $exec); + } + return lc($val); + } + + if($FW_room) { + $val = $attr{$d}{"alias_$FW_room"}; + return $val if($val); + } + + $val = $attr{$d}{"alias"}; + return $val if($val); + return $d; +} + +######################## +# Show the overview of devices in one room +# room can be a room, all or Unsorted +sub +FW_showRoom() +{ + return 0 if(!$FW_room || + AttrVal($FW_wname,"forbiddenroom","") =~ m/\b$FW_room\b/); + + %FW_hiddengroup = (); + foreach my $r (split(",",AttrVal($FW_wname, "hiddengroup", ""))) { + $FW_hiddengroup{$r} = 1; + } + my $hge = AttrVal($FW_wname, "hiddengroupRegexp", undef); + + FW_pO "
"; + FW_addContent("room='$FW_room'"); + FW_pO ""; # Need for equal width of subtables + + # array of all device names in the room (exception weblinks without group + # attribute) + my @devs= grep { (($FW_rooms{$FW_room} && $FW_rooms{$FW_room}{$_}) || + $FW_room eq "all") && !IsIgnored($_) } keys %defs; + my (%group, @atEnds, %usuallyAtEnd, %sortIndex); + my $nDevsInRoom = 0; + foreach my $dev (@devs) { + if($modules{$defs{$dev}{TYPE}}{FW_atPageEnd}) { + $usuallyAtEnd{$dev} = 1; + if(!AttrVal($dev, "group", undef)) { + $sortIndex{$dev} = FW_sortIndex($dev); + push @atEnds, $dev; + next; + } + } + next if(!$FW_types{$dev}); # FHEMWEB connection, missed due to caching + foreach my $grp (split(",", AttrVal($dev, "group", $FW_types{$dev}))) { + next if($FW_hiddengroup{$grp}); + next if($hge && $grp =~ m/$hge/); + $sortIndex{$dev} = FW_sortIndex($dev); + $group{$grp}{$dev} = 1; + $nDevsInRoom++; + } + } + + # row counter + my $row=1; + my %extPage = (); + my $nameDisplay = AttrVal($FW_wname,"nameDisplay",undef); + + my ($columns, $maxc) = FW_parseColumns(\%group); + FW_pO "" if($maxc != -1); + for(my $col=1; $col < ($maxc==-1 ? 2 : $maxc); $col++) { + FW_pO "" if($maxc != -1); # Column + } + FW_pO "" if($maxc != -1); + + FW_pO "
" if($maxc != -1); + + # iterate over the distinct groups + foreach my $g (sort { $maxc==-1 ? + $a cmp $b : + ($columns->{$a} ? $columns->{$a}->[0] : 99) <=> + ($columns->{$b} ? $columns->{$b}->[0] : 99) } keys %group) { + + next if($maxc != -1 && (!$columns->{$g} || $columns->{$g}->[1] != $col)); + + ################# + # Check if there is a device of this type in the room + FW_pO ""; + FW_pO ""; + } + FW_pO "
$g
"; + FW_pO ""; + + foreach my $d (sort { $sortIndex{$a} cmp $sortIndex{$b} } + keys %{$group{$g}}) { + my $type = $defs{$d}{TYPE}; + $extPage{group} = $g; + + FW_makeDeviceLine($d,$row,\%extPage,$nameDisplay,\%usuallyAtEnd); + + if($modules{$type}{FW_addDetailToSummary}) { + no strict "refs"; + my $txt = &{$modules{$type}{FW_detailFn}}($FW_wname, $d, $FW_room); + use strict "refs"; + if(defined($txt)) { + FW_pO ""; + } + } + $row++; + } + FW_pO "
$txt
"; + FW_pO "
"; + FW_pO "
" if(@atEnds && $nDevsInRoom); + + # Now the "atEnds" + my $doBC = (AttrVal($FW_wname, "plotfork", 0) && + AttrVal($FW_wname, "plotEmbed", 0) == 0); + my %res; + my ($idx,$svgIdx) = (1,1); + @atEnds = sort { $sortIndex{$a} cmp $sortIndex{$b} } @atEnds; + $FW_svgData{$FW_cname} = { FW_RET=>$FW_RET, RES=>\%res, ATENDS=>\@atEnds }; + foreach my $d (@atEnds) { + no strict "refs"; + my $fn = $modules{$defs{$d}{TYPE}}{FW_summaryFn}; + $extPage{group} = "atEnd"; + $extPage{index} = $idx++; + if($doBC && $defs{$d}{TYPE} eq "SVG" && $FW_use{base64}) { + $extPage{svgIdx} = $svgIdx++; + BlockingCall(sub { + return "$FW_cname,$d,". + encode_base64(&{$fn}($FW_wname,$d,$FW_room,\%extPage),''); + }, undef, "FW_svgCollect"); + } else { + $res{$d} = &{$fn}($FW_wname,$d,$FW_room,\%extPage); + } + use strict "refs"; + } + return FW_svgDone(\%res, \@atEnds, undef); +} + +sub +FW_svgDone($$$) +{ + my ($res, $atEnds, $delayedReturn) = @_; + return -2 if(int(keys %{$res}) != int(@{$atEnds})); + + foreach my $d (@{$atEnds}) { + FW_pO $res->{$d}; + } + FW_pO ""; + FW_pO "
"; + FW_pO "" if($delayedReturn); + return 0; +} + +sub +FW_svgCollect($) +{ + my ($cname,$d,$enc) = split(",",$_[0],3); + my $h = $FW_svgData{$cname}; + my ($res, $atEnds) = ($h->{RES}, $h->{ATENDS}); + $res->{$d} = decode_base64($enc); + return if(int(keys %{$res}) != int(@{$atEnds})); + $FW_RET = $h->{FW_RET}; + delete($FW_svgData{$cname}); + FW_svgDone($res, $atEnds, 1); + FW_finishRead($defs{$cname}, 0, ""); +} + +# Room1:col1group1,col1group2|col2group1,col2group2 Room2:... +sub +FW_parseColumns($) +{ + my ($aGroup) = @_; + my %columns; + my $colNo = -1; + + foreach my $roomgroup (split("[ \t\r\n]+", AttrVal($FW_wname,"column",""))) { + my ($room, $groupcolumn)=split(":",$roomgroup,2); + $room =~ s/%20/ /g; # Space + next if(!defined($groupcolumn) || $FW_room !~ m/^$room$/); + $colNo = 1; + my @grouplist = keys %$aGroup; + my %handled; + + foreach my $groups (split(/\|/,$groupcolumn)) { + my $lineNo = 1; + foreach my $group (split(",",$groups)) { + $group =~ s/%20/ /g; # Forum #33612 + $group = "^$group\$"; #71381 + eval { "Hallo" =~ m/^$group$/ }; + if($@) { + Log3 $FW_wname, 1, "Bad regexp in column spec: $@"; + } else { + foreach my $g (grep /$group/ ,@grouplist) { + next if($handled{$g}); + $handled{$g} = 1; + $columns{$g} = [$lineNo++, $colNo]; #23212 + } + } + } + $colNo++; + } + last; + } + return (\%columns, $colNo); +} + + +################# +# return a sorted list of actual files for a given regexp +sub +FW_fileList($;$) +{ + my ($fname,$mtime) = @_; + $fname =~ s/%L/$attr{global}{logdir}/g #Forum #89744 + if($fname =~ m/%/ && $attr{global}{logdir}); + $fname =~ m,^(.*)/([^/]*)$,; # Split into dir and file + my ($dir,$re) = ($1, $2); + return $fname if(!$re); + $re =~ s/%./[A-Za-z0-9]*/g; # logfile magic (%Y, etc) + my @ret; + return @ret if(!opendir(DH, $dir)); + while(my $f = readdir(DH)) { + next if($f !~ m,^$re$, || $f eq "99_Utils.pm"); + push(@ret, $f); + } + closedir(DH); + return sort { (CORE::stat("$dir/$a"))[9] <=> (CORE::stat("$dir/$b"))[9] } + @ret if($mtime); + @ret = cfgDB_FW_fileList($dir,$re,@ret) if (configDBUsed()); + return sort @ret; +} + + +################################### +# Stream big files in chunks, to avoid bloating ourselves. +# This is a "terminal" function, no data can be appended after it is called. +sub +FW_outputChunk($$$) +{ + my ($hash, $buf, $d) = @_; + $buf = $d->deflate($buf) if($d); + if( length($buf) ){ + TcpServer_WriteBlocking($hash, sprintf("%x\r\n",length($buf)) .$buf."\r\n"); + } +} + +sub +FW_returnFileAsStream($$$$$) +{ + my ($path, $suffix, $type, $doEsc, $cacheable) = @_; + my $etag; + + if($cacheable) { + #Check for If-None-Match header (ETag) + my $if_none_match = $FW_httpheader{"If-None-Match"}; + $if_none_match =~ s/"(.*)"/$1/ if($if_none_match); + $etag = (stat($path))[9]; #mtime + if(defined($etag) && defined($if_none_match) && $etag eq $if_none_match) { + my $now = time(); + my $rsp = "Date: ".FmtDateTimeRFC1123($now)."\r\n". + "ETag: $etag\r\n". + "Expires: ".FmtDateTimeRFC1123($now+900)."\r\n"; + Log3 $FW_wname, 4, "$FW_chash->{NAME} => 304 Not Modified"; + TcpServer_WriteBlocking($FW_chash,"HTTP/1.1 304 Not Modified\r\n". + $rsp . $FW_headerlines . "\r\n"); + return -1; + } + } + + if(!open(FH, $path)) { + Log3 $FW_wname, 4, "FHEMWEB $FW_wname $path: $!"; + TcpServer_WriteBlocking($FW_chash, + "HTTP/1.1 404 Not Found\r\n". + "Content-Length:0\r\n\r\n"); + FW_closeConn($FW_chash); + return -1; + } + binmode(FH) if($type !~ m/text/); # necessary for Windows + my $sz = -s $path; + + $etag = defined($etag) ? "ETag: \"$etag\"\r\n" : ""; + my $expires = $cacheable ? ("Expires: ".gmtime(time()+900)." GMT\r\n"): ""; + my $compr = ($FW_httpheader{"Accept-Encoding"} && + $FW_httpheader{"Accept-Encoding"} =~ m/gzip/ && $FW_use{zlib}) ? + "Content-Encoding: gzip\r\n" : ""; + TcpServer_WriteBlocking($FW_chash, "HTTP/1.1 200 OK\r\n". + $compr . $expires . $FW_headerlines . $etag . + "Transfer-Encoding: chunked\r\n" . + "Content-Type: $type; charset=$FW_encoding\r\n\r\n"); + + my $d = Compress::Zlib::deflateInit(-WindowBits=>31) if($compr); + FW_outputChunk($FW_chash, $FW_RET, $d); + FW_outputChunk($FW_chash, "
". + "jump to the end

", $d) + if($doEsc && $sz > 2048); + my $buf; + while(sysread(FH, $buf, 2048)) { + if($doEsc) { # FileLog special + $buf =~ s//>/g; + } + FW_outputChunk($FW_chash, $buf, $d); + } + close(FH); + FW_outputChunk($FW_chash, "
". + "jump to the top

", $d) + if($doEsc && $sz > 2048); + FW_outputChunk($FW_chash, $suffix, $d); + + if($compr) { + $buf = $d->flush(); + if($buf){ + TcpServer_WriteBlocking($FW_chash, + sprintf("%x\r\n",length($buf)) .$buf."\r\n"); + } + } + TcpServer_WriteBlocking($FW_chash, "0\r\n\r\n"); + FW_closeConn($FW_chash); + return -1; +} + + +################## +sub +FW_fatal($) +{ + my ($msg) = @_; + FW_pO "$msg"; +} + +################## +sub +FW_hidden($$) +{ + my ($n, $v) = @_; + return ""; +} + +################## +# Generate a select field with option list +sub +FW_select($$$$$@) +{ + my ($id, $name, $valueArray, $selected, $class, $jSelFn) = @_; + $jSelFn = ($jSelFn ? "onchange=\"$jSelFn\"" : ""); + $id =~ s/\./_/g if($id); # to avoid problems in JS DOM Search + $id = ($id ? "id=\"$id\" informId=\"$id\"" : ""); + my $s = ""; + return $s; +} + +################## +sub +FW_textfieldv($$$$) +{ + my ($n, $z, $class, $value) = @_; + my $v; + $v=" value='$value'" if(defined($value)); + return if($FW_hiddenroom{input}); + my $s = ""; + return $s; +} + +sub +FW_textfield($$$) +{ + return FW_textfieldv($_[0], $_[1], $_[2], ""); +} + +################## +sub +FW_submit($$@) +{ + my ($n, $v, $class) = @_; + $class = ($class ? "class=\"$class\"" : ""); + my $s =""; + $s = FW_hidden("fwcsrf", $defs{$FW_wname}{CSRFTOKEN}).$s if($FW_CSRF); + return $s; +} + +################## +sub +FW_displayFileList($@) +{ + my ($heading,@files)= @_; + my $hid = lc($heading); + $hid =~ s/[^A-Za-z]/_/g; + FW_pO "
$heading
"; + FW_pO ""; + my $cfgDB = ""; + my $row = 0; + foreach my $f (@files) { + $cfgDB = ($f =~ s,\.configDB$,,); + $cfgDB = ($cfgDB) ? "configDB" : ""; + FW_pO ""; + FW_pH "cmd=style edit $f $cfgDB", $f, 1; + FW_pO ""; + $row = ($row+1)%2; + } + FW_pO "
"; + FW_pO "
"; +} + +################## +sub +FW_fileNameToPath($) +{ + my $name = shift; + + my @f = sort keys %FW_customConfFiles; + return "$FW_confdir/$name" if ( map { $name =~ $_ } @f ); + + $attr{global}{configfile} =~ m,([^/]*)$,; + my $cfgFileName = $1; + if($name eq $cfgFileName) { + return $attr{global}{configfile}; + } elsif($name =~ m/.*(js|css|_defs.svg)$/) { + return "$FW_cssdir/$name"; + } elsif($name =~ m/.*(png|svg)$/) { + my $d=""; + map { $d = $_ if(!$d && -d "$FW_icondir/$_") } @FW_iconDirs; + return "$FW_icondir/$d/$name"; + } elsif($name =~ m/.*gplot$/) { + return "$FW_gplotdir/$name"; + } elsif($name =~ m/.*log$/) { + return AttrVal("global", "logdir", "log")."/$name"; + } else { + return "$MW_dir/$name"; + } +} + +sub FW_confFiles() { + # create and return regexp for editFileList + return "(".join ( "|" , sort keys %FW_customConfFiles ).")"; +} + +################## +# List/Edit/Save files +sub +FW_style($$) +{ + my ($cmd, $msg) = @_; + my @a = split(" ", $cmd); + + return if(!Authorized($FW_chash, "cmd", $a[0])); + + my $start = '>
" if($msg); + + $attr{global}{configfile} =~ m,([^/]*)$,; + my $cfgFileName = $1; + FW_displayFileList("config file", $cfgFileName) + if(!configDBUsed()); + + my $efl = AttrVal($FW_wname, 'editFileList', + "Own modules and helper files:\$MW_dir:^(.*sh|[0-9][0-9].*Util.*pm|". + ".*cfg|.*\.holiday|myUtilsTemplate.pm|.*layout)\$\n". + "Config files:\$FW_confdir:^".FW_confFiles."\$\n". + "Gplot files:\$FW_gplotdir:^.*gplot\$\n". + "Style files:\$FW_cssdir:^.*(css|svg)\$"); + foreach my $l (split(/[\r\n]/, $efl)) { + my ($t, $v, $re) = split(":", $l, 3); + $v = eval $v; + my @fList; + if($v eq $FW_gplotdir && AttrVal($FW_wname,'showUsedFiles',0)) { + @fList = defInfo('TYPE=SVG','GPLOTFILE'); + @fList = map { "$_.gplot" } @fList; + @fList = map { "$_.configDB" } @fList if configDBUsed(); + my %fListUnique = map { $_, 1 } @fList; + @fList = sort keys %fListUnique; + } else { + @fList = FW_fileList("$v/$re"); + } + FW_displayFileList($t, @fList); + } + FW_pO $end; + + } elsif($a[1] eq "select") { + my @fl = grep { $_ !~ m/(floorplan|dashboard)/ } + FW_fileList("$FW_cssdir/.*style.css"); + FW_addContent($start); + FW_pO "
Styles
"; + FW_pO "
"; + my $row = 0; + foreach my $file (@fl) { + next if($file =~ m/svg_/); + $file =~ s/style.css//; + $file = "default" if($file eq ""); + FW_pO ""; + FW_pH "cmd=style set $file", "$file", 1; + FW_pO ""; + $row = ($row+1)%2; + } + FW_pO "
$end"; + + } elsif($a[1] eq "set") { + CommandAttr(undef, "$FW_wname stylesheetPrefix $a[2]"); + $FW_styleStamp = time(); + $FW_RET =~ s,/style.css\?v=\d+,/style.css?v=$FW_styleStamp,; + FW_addContent($start); + FW_pO "Reload the page in the browser.$end"; + + } elsif($a[1] eq "edit") { + my $fileName = $a[2]; + my $data = ""; + my $cfgDB = defined($a[3]) ? $a[3] : ""; + my $forceType = ($cfgDB eq 'configDB') ? $cfgDB : "file"; + $fileName =~ s,.*/,,g; # Little bit of security + my $filePath = FW_fileNameToPath($fileName); + my($err, @content) = FileRead({FileName=>$filePath, ForceType=>$forceType}); + if($err) { + FW_addContent(">$err"; + if($readOnly) { + FW_pO "You can enable saving this file by setting the editConfig "; + FW_pO "attribute, but read the documentation first for the side effects."; + FW_pO "

"; + } else { + FW_pO FW_submit("save", "Save $fileName"); + FW_pO "  "; + FW_pO FW_submit("saveAs", "Save as"); + FW_pO FW_textfieldv("saveName", 30, "saveName", $fileName); + FW_pO "

"; + } + FW_pO FW_hidden("cmd", "style save $fileName $cfgDB"); + FW_pO ""; + FW_pO ""; + FW_pO ""; + + } elsif($a[1] eq "save") { + my $fileName = $a[2]; + my $cfgDB = defined($a[3]) ? $a[3] : ""; + $fileName = $FW_webArgs{saveName} + if($FW_webArgs{saveAs} && $FW_webArgs{saveName}); + $fileName =~ s,.*/,,g; # Little bit of security + my $filePath = FW_fileNameToPath($fileName); + my $isImg = ($fileName =~ m,\.(svg|png)$,i); + my $forceType = ($cfgDB eq 'configDB' && !$isImg) ? $cfgDB : "file"; + + $FW_data =~ s/\r//g if(!$isImg); + my $err; + if($fileName =~ m,\.png$,) { + $err = FileWrite({FileName=>$filePath,ForceType=>$forceType,NoNL=>1}, + $FW_data); + } else { + $err = FileWrite({ FileName=>$filePath, ForceType=>$forceType }, + split("\n", $FW_data)); + } + + if($err) { + FW_addContent(">$filePath: $!ERROR:$ret" : "Saved $fileName$sfx"); + FW_style("style list", $ret); + $ret = ""; + + } elsif($a[1] eq "iconFor") { + FW_iconTable("iconFor", "icon", "style setIF $a[2] %s", undef); + + } elsif($a[1] eq "setIF") { + FW_fC("attr $a[2] icon $a[3]"); + FW_doDetail($a[2]); + + } elsif($a[1] eq "showDSI") { + FW_iconTable("devStateIcon", "", + "style addDSI $a[2] %s", "Enter value/regexp for STATE"); + + } elsif($a[1] eq "addDSI") { + my $dsi = AttrVal($a[2], "devStateIcon", ""); + $dsi .= " " if($dsi); + FW_fC("attr $a[2] devStateIcon $dsi$FW_data:$a[3]"); + FW_doDetail($a[2]); + + } elsif($a[1] eq "eventMonitor") { + FW_pO ""; + FW_addContent(); + my $filter = $a[2] ? ($a[2] eq "log" ? "global" : $a[2]) : ".*"; + FW_pO "Events (Filter: $filter) ". + "  FHEM log ". + "". + "  

\n"; + FW_pO "
"; + FW_pO ""; + + } + +} + +sub +FW_iconTable($$$$) +{ + my ($name, $class, $cmdFmt, $textfield) = @_; + + my %icoList = (); + foreach my $style (@FW_iconDirs) { + foreach my $imgName (sort keys %{$FW_icons{$style}}) { + next if($imgName =~ m+^\.+ || $imgName =~ m+/\.+); # Skip dot files + $imgName =~ s/\.[^.]*$//; # Cut extension + next if(!$FW_icons{$style}{$imgName}); # Dont cut it twice: FS20.on.png + next if($FW_icons{$style}{$imgName} !~ m/$imgName/); # Skip alias + next if($imgName=~m+^(weather/|shutter.*big|fhemicon|favicon|ws_.*_kl)+); + next if($imgName=~m+^(dashboardicons)+); + $icoList{$imgName} = 1; + } + } + + FW_addContent(); + FW_pO "
"; + FW_pO "Filter: ".FW_textfieldv("icon-filter",20,"iconTable","")."
"; + if($textfield) { + FW_pO "$textfield: ".FW_textfieldv("data",20,"iconTable",".*")."
"; + } + foreach my $i (sort keys %icoList) { + FW_pF "", $i, $i, FW_makeImage($i,$i,$class); + } + FW_pO "
"; + FW_pO ""; +} + +################## +# print (append) to output +sub +FW_pO(@) +{ + my $arg = shift; + return if(!defined($arg)); + $FW_RET .= $arg; + $FW_RET .= "\n"; +} + +################# +# add href +sub +FW_pH(@) +{ + my ($link, $txt, $td, $class, $doRet,$nonl) = @_; + my $ret; + + $link .= $FW_CSRF if($link =~ m/cmd/ && + $link !~m/cmd=style%20(list|select|eventMonitor)/); + $link = ($link =~ m,^/,) ? $link : "$FW_ME$FW_subdir?$link"; + + # Using onclick, as href starts safari in a webapp. + # Known issue: the pointer won't change + if($FW_ss || $FW_tp) { + $ret = "$txt"; + } else { + $ret = "$txt"; + } + + #actually 'div' should be removed if no class is defined + # as I can't check all code for consistancy I add nonl instead + $class = ($class)?" class=\"$class\"":""; + $ret = "$ret" if (!$nonl); + + $ret = "$ret" if($td); + return $ret if($doRet); + FW_pO $ret; +} + +################# +# href without class/div, returned as a string +sub +FW_pHPlain(@) +{ + my ($link, $txt, $td) = @_; + + $link = "?$link" if($link !~ m+^/+); + my $ret = ""; + $ret .= "" if($td); + $link .= $FW_CSRF; + if($FW_ss || $FW_tp) { + $ret .= "$txt"; + } else { + $ret .= "$txt"; + } + $ret .= "" if($td); + return $ret; +} + + +############################## +sub +FW_makeImage(@) +{ + my ($name, $txt, $class)= @_; + + $txt = $name if(!defined($txt)); + $class = "" if(!$class); + $class = "$class $name"; + $class =~ s/\./_/g; + $class =~ s/@/ /g; + + my $p = FW_iconPath($name); + return $name if(!$p); + if($p =~ m/\.svg$/i) { + if(open(FH, "$FW_icondir/$p")) { + my $data; + do { + $data = ; + if(!defined($data)) { + Log 1, "$FW_icondir/$p is not useable"; + return ""; + } + } until( $data =~ m/^); + close(FH); + $data =~ s/[\r\n]/ /g; + $data =~ s/ *$//g; + $data =~ s/"; + } +} + +#### +sub +FW_IconURL($) +{ + my ($name)= @_; + return "$FW_ME/icons/$name"; +} + +################## +# print formatted +sub +FW_pF($@) +{ + my $fmt = shift; + $FW_RET .= sprintf $fmt, @_; +} + +################## +# fhem command +sub +FW_fC($@) +{ + my ($cmd, $unique) = @_; + my $ret; + if($unique) { + $ret = AnalyzeCommand($FW_chash, $cmd); + } else { + $ret = AnalyzeCommandChain($FW_chash, $cmd); + } + return $ret; +} + +sub +FW_Attr(@) +{ + my ($type, $devName, $attrName, @param) = @_; + my $hash = $defs{$devName}; + my $sP = "stylesheetPrefix"; + my $retMsg; + + if($type eq "set" && $attrName eq "HTTPS") { + TcpServer_SetSSL($hash); + } + + if($type eq "set") { # Converting styles + if($attrName eq "smallscreen" || $attrName eq "touchpad") { + $attr{$devName}{$sP} = $attrName; + $retMsg="$devName: attribute $attrName deprecated, converted to $sP"; + $param[0] = $attrName; $attrName = $sP; + } + } + + if($attrName eq $sP) { + # AttrFn is called too early, we have to set/del the attr here + if($type eq "set") { + $attr{$devName}{$sP} = (defined($param[0]) ? $param[0] : "default"); + FW_readIcons($attr{$devName}{$sP}); + } else { + delete $attr{$devName}{$sP}; + } + } + + if(($attrName eq "allowedCommands" || + $attrName eq "basicAuth" || + $attrName eq "basicAuthMsg") + && $type eq "set") { + my $aName = "allowed_$devName"; + my $exists = ($defs{$aName} ? 1 : 0); + AnalyzeCommand(undef, "defmod $aName allowed"); + AnalyzeCommand(undef, "attr $aName validFor $devName"); + AnalyzeCommand(undef, "attr $aName $attrName ".join(" ",@param)); + return "$devName: ".($exists ? "modifying":"creating"). + " device $aName for attribute $attrName"; + } + + if($attrName eq "iconPath" && $type eq "set") { + foreach my $pe (split(":", $param[0])) { + $pe =~ s+\.\.++g; + FW_readIcons($pe); + } + } + + if($attrName eq "JavaScripts" && $type eq "set") { # create some attributes + my (%a, @add); + map { $a{$_} = 1 } split(" ", $modules{FHEMWEB}{AttrList}); + map { + $_ =~ s+.*/++; $_ =~ s/.js$//; $_ =~ s/fhem_//; $_ .= "Param"; + push @add, $_ if(!$a{$_} && $_ !~ m/^-/); + } split(" ", $param[0]); + $modules{FHEMWEB}{AttrList} .= " ".join(" ",@add) if(@add); + } + + if($attrName eq "csrfToken") { + return undef if($FW_csrfTokenCache{$devName} && !$init_done); + my $csrf = $param[0]; + if($type eq "del" || $csrf eq "random") { + my ($x,$y) = gettimeofday(); + ($csrf = "csrf_".(rand($y)*rand($x))) =~ s/[^a-z_0-9]//g; + } + + if($csrf eq "none") { + delete($hash->{CSRFTOKEN}); + delete($FW_csrfTokenCache{$devName}); + } else { + $hash->{CSRFTOKEN} = $csrf; + $FW_csrfTokenCache{$devName} = $hash->{CSRFTOKEN}; + } + } + + if($attrName eq "longpoll" && $type eq "set" && $param[0] eq "websocket") { + return "$devName: Could not load Digest::SHA on startup, no websocket" + if(!$FW_use{sha}); + } + + return $retMsg; +} + + +# recursion starts at $FW_icondir/$dir +# filenames are relative to $FW_icondir +sub +FW_readIconsFrom($$) +{ + my ($dir,$subdir)= @_; + + my $ldir = ($subdir ? "$dir/$subdir" : $dir); + my @entries; + if(opendir(DH, "$FW_icondir/$ldir")) { + @entries= sort readdir(DH); # assures order: .gif .ico .jpg .png .svg + closedir(DH); + } + + foreach my $entry (@entries) { + if( -d "$FW_icondir/$ldir/$entry" ) { # directory -> recurse + FW_readIconsFrom($dir, $subdir ? "$subdir/$entry" : $entry) + unless($entry eq "." || $entry eq ".." || $entry eq ".svn"); + + } else { + if($entry =~ m/^iconalias.txt$/i && open(FH, "$FW_icondir/$ldir/$entry")){ + while(my $l = ) { + chomp($l); + my @a = split(" ", $l); + next if($l =~ m/^#/ || @a < 2); + $FW_icons{$dir}{$a[0]} = $a[1]; + } + close(FH); + } elsif($entry =~ m/(gif|ico|jpg|png|jpeg|svg)$/i) { + my $filename = $subdir ? "$subdir/$entry" : $entry; + $FW_icons{$dir}{$filename} = $filename; + + my $tag = $filename; # Add it without extension too + $tag =~ s/\.[^.]*$//; + $FW_icons{$dir}{$tag} = $filename; + } + } + } + $FW_icons{$dir}{""} = 1; # Do not check empty directories again. +} + +sub +FW_readIcons($) +{ + my ($dir)= @_; + return if($FW_icons{$dir}); + FW_readIconsFrom($dir, ""); +} + + +# check if the icon exists, and if yes, returns its "logical" name; +sub +FW_iconName($) +{ + my ($oname)= @_; + return undef if(!defined($oname)); + my $name = $oname; + $name =~ s/@.*//; + foreach my $pe (@FW_iconDirs) { + return $oname if($pe && $FW_icons{$pe} && $FW_icons{$pe}{$name}); + } + return undef; +} + +# returns the physical absolute path relative for the logical path +# examples: +# FS20.on -> dark/FS20.on.png +# weather/sunny -> default/weather/sunny.gif +sub +FW_iconPath($) +{ + my ($name) = @_; + $name =~ s/@.*//; + foreach my $pe (@FW_iconDirs) { + return "$pe/$FW_icons{$pe}{$name}" + if($pe && $FW_icons{$pe} && $FW_icons{$pe}{$name}); + } + return undef; +} + +sub +FW_dev2image($;$) +{ + my ($name, $state) = @_; + my $d = $defs{$name}; + return "" if(!$name || !$d); + my $devStateIcon = AttrVal($name, "devStateIcon", undef); + return "" if(defined($devStateIcon) && lc($devStateIcon) eq 'none'); + + my $type = $d->{TYPE}; + $state = $d->{STATE} if(!defined($state)); + return "" if(!$type || !defined($state)); + + my $model = AttrVal($name, "model", ""); + my (undef, $rstate) = ReplaceEventMap($name, [undef, $state], 0); + + my ($icon, $rlink); + if(defined($devStateIcon) && $devStateIcon =~ m/^{.*}$/s) { + my ($html, $link) = eval $devStateIcon; + Log3 $FW_wname, 1, "devStateIcon $name: $@" if($@); + return ($html, $link, 1) if(defined($html) && $html =~ m/^<.*>$/s); + $devStateIcon = $html; + } + + if(defined($devStateIcon)) { + my @list = split(" ", $devStateIcon); + foreach my $l (@list) { + my ($re, $iconName, $link) = split(":", $l, 3); + if(defined($re) && $state =~ m/^$re$/) { + if(defined($iconName) && $iconName eq "") { + $rlink = $link; + last; + } + if(defined($iconName) && defined(FW_iconName($iconName))) { + return ($iconName, $link, 0); + } else { + return ($state, $link, 1); + } + } + } + } + + $state =~ s/ .*//; # Want to be able to have icons for "on-for-timer xxx" + + $icon = FW_iconName("$name.$state") if(!$icon); # lamp.Aus.png + $icon = FW_iconName("$name.$rstate") if(!$icon); # lamp.on.png + $icon = FW_iconName($name) if(!$icon); # lamp.png + $icon = FW_iconName("$model.$state") if(!$icon && $model); # fs20st.off.png + $icon = FW_iconName($model) if(!$icon && $model); # fs20st.png + $icon = FW_iconName("$type.$state") if(!$icon); # FS20.Aus.png + $icon = FW_iconName("$type.$rstate") if(!$icon); # FS20.on.png + $icon = FW_iconName($type) if(!$icon); # FS20.png + $icon = FW_iconName($state) if(!$icon); # Aus.png + $icon = FW_iconName($rstate) if(!$icon); # on.png + return ($icon, $rlink, 0); +} + +sub +FW_makeEdit($$$) +{ + my ($name, $n, $val) = @_; + + # Toggle Edit-Window visibility script. + my $psc = AttrVal("global", "perlSyntaxCheck", ($featurelevel>5.7) ? 1 : 0); + FW_pO ""; + FW_pO "$n"; + FW_pO ""; + + $val =~ s,\\\n,\n,g; + $val = FW_htmlEscape($val); + my $eval = $val; + $eval = "
$eval
" if($eval =~ m/\n/); + FW_pO ""; + FW_pO "
$eval
"; + FW_pO ""; + + FW_pO ""; + FW_pO "
"; + FW_pO "
"; + FW_pO FW_hidden("detail", $name); + my $cmd = "modify"; + my $ncols = $FW_ss ? 30 : 60; + FW_pO ""; + FW_pO "
" . FW_submit("cmd.${cmd}$name", "$cmd $name",($psc?"psc":"")); + FW_pO "
"; + FW_pO ""; +} + + +sub +FW_longpollInfo($@) +{ + my $fmt = shift; + if($fmt && $fmt eq "JSON") { + my @a; + map { my $x = $_; #Forum 57377, ASCII 0-19 \ " + $x=~ s/([\x00-\x1f\x22\x5c\x7f])/sprintf '\u%04x', ord($1)/ge; + push @a,$x; } @_; + return '["'.join('","', @a).'"]'; + } else { + return join('<<', @_); + } +} + +sub +FW_roomStatesForInform($$) +{ + my ($me, $sinceTimestamp ) = @_; + return "" if($me->{inform}{type} !~ m/status/); + + my %extPage = (); + my @data; + foreach my $dn (keys %{$me->{inform}{devices}}) { + next if(!defined($defs{$dn})); + my $t = $defs{$dn}{TYPE}; + next if(!$t || $modules{$t}{FW_atPageEnd}); + + my $lastChanged = OldTimestamp( $dn ); + next if(!defined($lastChanged) || $lastChanged lt $sinceTimestamp); + + my ($allSet, $cmdlist, $txt) = FW_devState($dn, "", \%extPage); + if($defs{$dn} && $defs{$dn}{STATE} && $defs{$dn}{TYPE} ne "weblink") { + push @data, + FW_longpollInfo($me->{inform}{fmt}, $dn, $defs{$dn}{STATE}, $txt); + } + } + my $data = join("\n", map { s/\n/ /gm; $_ } @data)."\n"; + return $data; +} + +sub +FW_logInform($$) +{ + my ($me, $msg) = @_; # _NO_ Log3 here! + + my $ntfy = $defs{$me}; + if(!$ntfy) { + delete $logInform{$me}; + return; + } + $msg = FW_htmlEscape($msg); + if(!FW_addToWritebuffer($ntfy, "
$msg
") ){ + TcpServer_Close($ntfy, 1); + delete $logInform{$me}; + } +} + +sub +FW_Notify($$) +{ + my ($ntfy, $dev) = @_; + my $h = $ntfy->{inform}; + return undef if(!$h); + my $isStatus = ($h->{type} =~ m/status/); + my $events; + + my $dn = $dev->{NAME}; + if($dn eq "global" && $isStatus) { + my $vs = int(@structChangeHist) ? 'visible' : 'hidden'; + my $data = FW_longpollInfo($h->{fmt}, + "#FHEMWEB:$ntfy->{NAME}","\$('#saveCheck').css('visibility','$vs')",""); + FW_addToWritebuffer($ntfy, $data."\n"); + + if($dev->{CHANGED}) { + $dn = $1 if($dev->{CHANGED}->[0] =~ m/^MODIFIED (.*)$/); + if($dev->{CHANGED}->[0] =~ m/^ATTR ([^ ]+) ([^ ]+) (.*)$/s) { + $dn = $1; + my @a = ("a-$2: $3"); + $events = \@a; + } + } + } + + if($dn eq $ntfy->{SNAME} && + $dev->{CHANGED} && + $dev->{CHANGED}->[0] =~ m/^JS(#([^:]*))?:(.*)$/) { + my $data = $3; + return if( $2 && $ntfy->{PEER} !~ m/$2/ ); + $data = FW_longpollInfo($h->{fmt}, "#FHEMWEB:$ntfy->{NAME}",$data,""); + FW_addToWritebuffer($ntfy, $data."\n"); + return; + } + + return undef if($isStatus && !$h->{devices}{$dn}); + + my @data; + my %extPage; + my $isRaw = ($h->{type} =~ m/raw/); + $events = deviceEvents($dev, AttrVal($FW_wname, "addStateEvent",!$isRaw)) + if(!$events); + + if($isStatus) { + # Why is saving this stuff needed? FLOORPLAN? + my @old = ($FW_wname, $FW_ME, $FW_ss, $FW_tp, $FW_subdir); + $FW_wname = $ntfy->{SNAME}; + $FW_ME = "/" . AttrVal($FW_wname, "webname", "fhem"); + $FW_subdir = ($h->{iconPath} ? "/floorplan/$h->{iconPath}" : ""); # 47864 + $FW_sp = AttrVal($FW_wname, "stylesheetPrefix", "f18"); + $FW_sp = "" if($FW_sp eq "default"); + $FW_ss = ($FW_sp =~ m/smallscreen/); + $FW_tp = ($FW_sp =~ m/smallscreen|touchpad/); + my $spDir = ($FW_sp eq "default" ? "" : "$FW_sp:"); + @FW_iconDirs = grep { $_ } split(":", AttrVal($FW_wname, "iconPath", + "${spDir}fhemSVG:openautomation:default")); + if($h->{iconPath}) { + unshift @FW_iconDirs, $h->{iconPath}; + FW_readIcons($h->{iconPath}); + } + + if( !$modules{$defs{$dn}{TYPE}}{FW_atPageEnd} ) { + my ($allSet, $cmdlist, $txt) = FW_devState($dn, "", \%extPage); + ($FW_wname, $FW_ME, $FW_ss, $FW_tp, $FW_subdir) = @old; + push @data, FW_longpollInfo($h->{fmt}, $dn, $dev->{STATE}, $txt); + } + + #Add READINGS + if($events) { # It gets deleted sometimes (?) + my $tn = TimeNow(); + my $max = int(@{$events}); + for(my $i = 0; $i < $max; $i++) { + if($events->[$i] !~ /: /) { + if($dev->{NAME} eq 'global') { # Forum #47634 + my($type,$args) = split(' ', $events->[$i], 2); + $args = "" if(!defined($args)); # global SAVE + push @data, FW_longpollInfo($h->{fmt}, "$dn-$type", $args, $args); + } + + next; #ignore 'set' commands + } + my ($readingName,$readingVal) = split(": ",$events->[$i],2); + next if($readingName !~ m/^[A-Za-z\d_\.\-\/:]+$/); # Forum #70608,70844 + push @data, FW_longpollInfo($h->{fmt}, + "$dn-$readingName", $readingVal,$readingVal); + push @data, FW_longpollInfo($h->{fmt}, "$dn-$readingName-ts", $tn, $tn); + } + } + } + + if($isRaw) { + if($events) { # It gets deleted sometimes (?) + my $tn = TimeNow(); + if($attr{global}{mseclog}) { + my ($seconds, $microseconds) = gettimeofday(); + $tn .= sprintf(".%03d", $microseconds/1000); + } + my $max = int(@{$events}); + my $dt = $dev->{TYPE}; + for(my $i = 0; $i < $max; $i++) { + my $line = "$tn $dt $dn ".$events->[$i]."
"; + eval { + my $ok; + if($h->{filterType} && $h->{filterType} eq "notify") { + $ok = ($dn =~ m/^$h->{filter}$/ || + "$dn:$events->[$i]" =~ m/^$h->{filter}$/) ; + } else { + $ok = ($line =~ m/$h->{filter}/) ; + } + push @data,$line if($ok); + } + } + } + } + + if(@data){ + if(!FW_addToWritebuffer($ntfy, + join("\n", map { s/\n/ /gm; $_ } @data)."\n") ){ + my $name = $ntfy->{NAME}; + Log3 $name, 4, "Closing connection $name due to full buffer in FW_Notify"; + TcpServer_Close($ntfy, 1); + } + } + + return undef; +} + +sub +FW_directNotify($@) # Notify without the event overhead (Forum #31293) +{ + my $filter; + if($_[0] =~ m/^FILTER=(.*)/) { + $filter = "^$1\$"; + shift; + } + my $dev = $_[0]; + foreach my $ntfy (values(%defs)) { + next if(!$ntfy->{TYPE} || + $ntfy->{TYPE} ne "FHEMWEB" || + !$ntfy->{inform} || + !$ntfy->{inform}{devices}{$dev} || + $ntfy->{inform}{type} ne "status"); + next if($filter && $ntfy->{inform}{filter} !~ m/$filter/); + if(!FW_addToWritebuffer($ntfy, + FW_longpollInfo($ntfy->{inform}{fmt}, @_)."\n")) { + my $name = $ntfy->{NAME}; + Log3 $name, 4, "Closing connection $name due to full buffer in FW_Notify"; + TcpServer_Close($ntfy, 1); + } + } +} + +################### +# Compute the state (==second) column +sub +FW_devState($$@) +{ + my ($d, $rf, $extPage) = @_; + + my ($hasOnOff, $link); + + my $cmdList = AttrVal($d, "webCmd", ""); + my $allSets = FW_widgetOverride($d, getAllSets($d, $FW_chash)); + my $state = $defs{$d}{STATE}; + $state = "" if(!defined($state)); + + my $txt = $state; + my $dsi = ($attr{$d} && ($attr{$d}{stateFormat} || $attr{$d}{devStateIcon})); + + $hasOnOff = ($allSets =~ m/(^| )on(:[^ ]*)?( |$)/i && + $allSets =~ m/(^| )off(:[^ ]*)?( |$)/i); + if(AttrVal($d, "showtime", undef)) { + my $v = $defs{$d}{READINGS}{state}{TIME}; + $txt = $v if(defined($v)); + + } elsif(!$dsi && $allSets =~ m/\bdesired-temp:/) { + $txt = "$1 °C" if($txt =~ m/^measured-temp: (.*)/); # FHT fix + $cmdList = "desired-temp" if(!$cmdList); + + } elsif(!$dsi && $allSets =~ m/\bdesiredTemperature:/) { + $txt = ReadingsVal($d, "temperature", ""); # ignores stateFormat!!! + $txt =~ s/ .*//; + $txt .= "°C"; + $cmdList = "desiredTemperature" if(!$cmdList); + + } else { + my ($icon, $isHtml); + ($icon, $link, $isHtml) = FW_dev2image($d); + $txt = ($isHtml ? $icon : FW_makeImage($icon, $state)) if($icon); + + my $cmdlist = (defined($link) ? $link : ""); + my $h = ""; + foreach my $cmd (split(":", $cmdlist)) { + my $htmlTxt; + my @c = split(' ', $cmd); # @c==0 if $cmd==" "; + if(int(@c) && $allSets && $allSets =~ m/\b$c[0]:([^ ]*)/) { + my $values = $1; + foreach my $fn (sort keys %{$data{webCmdFn}}) { + no strict "refs"; + $htmlTxt = &{$data{webCmdFn}{$fn}}($FW_wname, + $d, $FW_room, $cmd, $values); + use strict "refs"; + last if(defined($htmlTxt)); + } + } + + if( $htmlTxt ) { + $h .= "

$htmlTxt

"; + } + } + + if( $h ) { + $link = undef; + $h =~ s/'/\\"/g; + $txt = "$txt"; + } else { + $link = "cmd.$d=set $d $link" if(defined($link)); + } + + } + + + if($hasOnOff) { + my $isUpperCase = ($allSets =~ m/(^| )ON(:[^ ]*)?( |$)/ && + $allSets =~ m/(^| )OFF(:[^ ]*)?( |$)/); + # Have to cover: "on:An off:Aus", "A0:Aus AI:An Aus:off An:on" + my $on = ReplaceEventMap($d, $isUpperCase ? "ON" :"on" , 1); + my $off = ReplaceEventMap($d, $isUpperCase ? "OFF":"off", 1); + $link = "cmd.$d=set $d " . ($state eq $on ? $off : $on) if(!defined($link)); + $cmdList = "$on:$off" if(!$cmdList); + + } + + if(defined($link)) { # Have command to execute + my $room = AttrVal($d, "room", undef); + if($room) { + if($FW_room && $room =~ m/\b$FW_room\b/) { + $room = $FW_room; + } else { + $room =~ s/,.*//; + } + $link .= "&room=".urlEncode($room); + } + $txt = "$txt" + if($link !~ m/ noFhemwebLink\b/); + } + + my $style = AttrVal($d, "devStateStyle", ""); + + $state =~ s/"//g; + $state =~ s/<.*?>/ /g; # remove HTML tags for the title + $txt = "
$txt
"; + + my $type = $defs{$d}{TYPE}; + my $sfn = $modules{$type}{FW_summaryFn}; + if($sfn) { + if(!defined($extPage)) { + my %hash; + $extPage = \%hash; + } + no strict "refs"; + my $newtxt = &{$sfn}($FW_wname, $d, $FW_room, $extPage); + use strict "refs"; + $txt = $newtxt if(defined($newtxt)); # As specified + } + + return ($allSets, $cmdList, $txt); +} + + +sub +FW_Get($@) +{ + my ($hash, @a) = @_; + + my $arg = (defined($a[1]) ? $a[1] : ""); + if($arg eq "icon") { + return "need one icon as argument" if(int(@a) != 3); + my $ofn = $FW_wname; + $FW_wname = $hash->{NAME}; + my $icon = FW_iconPath($a[2]); + $FW_wname = $ofn; + return defined($icon) ? "$FW_icondir/$icon" : "no such icon"; + + } elsif($arg eq "pathlist") { + return "web server root: $FW_dir\n". + "icon directory: $FW_icondir\n". + "css directory: $FW_cssdir\n". + "gplot directory: $FW_gplotdir"; + + } else { + return "Unknown argument $arg choose one of icon pathlist:noArg"; + + } +} + + +##################################### +sub +FW_Set($@) +{ + my ($hash, @a) = @_; + my %cmd = ("rereadicons" => 1, "clearSvgCache" => 1); + + return "no set value specified" if(@a < 2); + return ("Unknown argument $a[1], choose one of ". + join(" ", map { "$_:noArg" } sort keys %cmd)) + if(!$cmd{$a[1]}); + + if($a[1] eq "rereadicons") { + my @dirs = keys %FW_icons; + %FW_icons = (); + foreach my $d (@dirs) { + FW_readIcons($d); + } + } + if($a[1] eq "clearSvgCache") { + my $cDir = "$FW_dir/SVGcache"; + if(opendir(DH, $cDir)) { + map { my $n="$cDir/$_"; unlink($n) if(-f $n); } readdir(DH); + closedir(DH); + } else { + return "Can't open $cDir: $!"; + } + } + return undef; +} + +##################################### +sub +FW_closeInactiveClients() +{ + my $now = time(); + foreach my $dev (keys %defs) { + next if(!$defs{$dev}{TYPE} || $defs{$dev}{TYPE} ne "FHEMWEB" || + !$defs{$dev}{LASTACCESS} || $defs{$dev}{inform} || + ($now - $defs{$dev}{LASTACCESS}) < 60); + Log3 $FW_wname, 4, "Closing inactive connection $dev"; + FW_Undef($defs{$dev}, undef); + delete $defs{$dev}; + delete $attr{$dev}; + } + InternalTimer($now+60, "FW_closeInactiveClients", 0, 0); +} + +sub +FW_htmlEscape($) +{ + my ($txt) = @_; + $txt =~ s/&/&/g; + $txt =~ s//>/g; + $txt =~ s/'/'/g; +# $txt =~ s/\n/
/g; + return $txt; +} + +########################### +# Widgets START +sub +FW_widgetFallbackFn() +{ + my ($FW_wname, $d, $FW_room, $cmd, $values) = @_; + + # webCmd "temp 30" should remain text + # noArg is needed for fhem.cfg.demo / Cinema + return "" if(!$values || $values eq "noArg"); + + my($reading) = split( ' ', $cmd, 2 ); + my $current; + if($cmd eq "desired-temp" || $cmd eq "desiredTemperature") { + $current = ReadingsVal($d, $cmd, 20); + $current =~ s/ .*//; # Cut off Celsius + $current = sprintf("%2.1f", int(2*$current)/2) if($current =~ m/[0-9.-]/); + } else { + $current = ReadingsVal($d, $reading, undef); + if( !defined($current) ) { + $reading = 'state'; + $current = Value($d); + } + $current =~ s/$cmd //; + $current = ReplaceEventMap($d, $current, 1); + } + return "
"; +} +# Widgets END +########################### + +sub +FW_visibleDevices(;$) +{ + my($FW_wname) = @_; + + my %devices = (); + foreach my $d (sort keys %defs) { + next if(!defined($defs{$d})); + my $h = $defs{$d}; + next if(!$h->{TEMPORARY}); + next if($h->{TYPE} ne "FHEMWEB"); + next if(defined($FW_wname) && $h->{SNAME} ne $FW_wname); + + next if(!defined($h->{inform})); + + @devices{ keys %{$h->{inform}->{devices}} } = + values %{$h->{inform}->{devices}}; + } + return %devices; +} + +sub +FW_ActivateInform($;$) +{ + my ($cl, $arg) = @_; + $FW_activateInform = ($arg ? $arg : 1); +} + +sub +FW_widgetOverride($$) +{ + my ($d, $str) = @_; + + return $str if(!$str); + + my $da = AttrVal($d, "widgetOverride", ""); + my $fa = AttrVal($FW_wname, "widgetOverride", ""); + return $str if(!$da && !$fa); + + my @list; + push @list, split(" ", $fa) if($fa); + push @list, split(" ", $da) if($da); + foreach my $na (@list) { + my ($n,$a) = split(":", $na, 2); + $str =~ s/\b($n)\b(:[^ ]*)?/$1:$a/g; + } + return $str; +} + + +1; + +=pod +=item helper +=item summary HTTP Server and FHEM Frontend +=item summary_DE HTTP Server und FHEM Frontend +=begin html + + +

FHEMWEB

+
    + FHEMWEB is the builtin web-frontend, it also implements a simple web + server (optionally with Basic-Auth and HTTPS). +

    + + + Define +
      + define <name> FHEMWEB <tcp-portnr> [global|IP] +

      + Enable the webfrontend on port <tcp-portnr>. If global is specified, + then requests from all interfaces (not only localhost / 127.0.0.1) are + serviced. If IP is specified, then FHEMWEB will only listen on this IP.
      + To enable listening on IPV6 see the comments here. +
      +
    +
    + + + Set +
      +
    • rereadicons
      + reads the names of the icons from the icon path. Use after adding or + deleting icons. +
    • +
    • clearSvgCache
      + delete all files found in the www/SVGcache directory, which is used to + cache SVG data, if the SVGcache attribute is set. +
    • +
    +
    + + + Get +
      +
    • icon <logical icon>
      + returns the absolute path to the logical icon. Example: +
        + get myFHEMWEB icon FS20.on
        + /data/Homeautomation/fhem/FHEM/FS20.on.png +
        +
      +
    • +
    • pathlist
      + return FHEMWEB specific directories, where files for given types are + located +

      + +
    + + + Attributes +
      + +
    • addHtmlTitle
      + If set to 0, do not add a title Attribute to the set/get/attr detail + widgets. This might be necessary for some screenreaders. Default is 1. +

    • + + + +
    • addStateEvent

    • + +
    • alias_<RoomName>
      + If you define a userattr alias_<RoomName> and set this attribute + for a device assgined to <RoomName>, then this value will be used + when displaying <RoomName>.
      + Note: you can use the userattr alias_.* to allow all rooms, but in this + case the attribute dropdown in the device detail view won't work for the + alias_.* attributes. +

    • + +
    • allowfrom
    • +
      + +
    • allowedCommands, basicAuth, basicAuthMsg
      + Please create these attributes for the corresponding allowed device, they are deprecated for the FHEMWEB + instance from now on. +

    • + + +
    • allowedHttpMethods
      + FHEMWEB implements the GET, POST and OPTIONS HTTP methods. Some external + devices require the HEAD method, which is not implemented correctly in + FHEMWEB, as FHEMWEB always returns a body, which, according to the spec, + is wrong. As in some cases this not a problem, enabling GET may work. + To do this, set this attribute to GET|POST|HEAD, default ist GET|POST. + OPTIONS is always enabled. +

    • + + +
    • closeConn
      + If set, a TCP Connection will only serve one HTTP request. Seems to + solve problems on iOS9 for WebApp startup. +

    • + + + +
    • column
      + Allows to display more than one column per room overview, by specifying + the groups for the columns. Example:
      +
        + attr WEB column LivingRoom:FS20,notify|FHZ,notify DiningRoom:FS20|FHZ +
      + In this example in the LivingRoom the FS20 and the notify group is in + the first column, the FHZ and the notify in the second.
      + Notes: some elements like SVG plots and readingsGroup can only be part of + a column if they are part of a group. + This attribute can be used to sort the groups in a room, just specify + the groups in one column. + Space in the room and group name has to be written as %20 for this + attribute. Both the room name and the groups are regular expressions. +
    • +
      + + +
    • confirmDelete
      + confirm delete actions with a dialog. Default is 1, set it to 0 to + disable the feature. +
    • +
      + + +
    • confirmJSError
      + JavaScript errors are reported in a dialog as default. + Set this attribute to 0 to disable the reporting. +
    • +
      + + +
    • CORS
      + If set to 1, FHEMWEB will supply a "Cross origin resource sharing" + header, see the wikipedia for details. +
    • +
      + + +
    • csrfToken
      + If set, FHEMWEB requires the value of this attribute as fwcsrf Parameter + for each command. It is used as countermeasure for Cross Site Resource + Forgery attacks. If the value is random, then a random number will be + generated on each FHEMWEB start. If it is set to the literal string + none, no token is expected. Default is random for featurelevel 5.8 and + greater, and none for featurelevel below 5.8

    • + + +
    • csrfTokenHTTPHeader
      + If set (default), FHEMWEB sends the token with the X-FHEM-csrfToken HTTP + header, which is used by some clients. Set it to 0 to switch it off, as + a measurre against shodan.io like FHEM-detection.

    • + + +
    • CssFiles
      + Space separated list of .css files to be included. The filenames + are relative to the www directory. Example: +
        + attr WEB CssFiles pgm2/mystyle.css +
      +

    • + + +
    • Css
      + CSS included in the header after the CssFiles section. +

    • + + +
    • cmdIcon
      + Space separated list of cmd:iconName pairs. If set, the webCmd text is + replaced with the icon. An easy method to set this value is to use + "Extend devStateIcon" in the detail-view, and copy its value.
      + Example:
        + attr lamp cmdIcon on:control_centr_arrow_up off:control_centr_arrow_down +
      +

    • + + +
    • defaultRoom
      + show the specified room if no room selected, e.g. on execution of some + commands. If set hides the motd. Example:
      + attr WEB defaultRoom Zentrale +
    • +
      + + +
    • devStateIcon
      + First form:
      +
        + Space separated list of regexp:icon-name:cmd triples, icon-name and cmd + may be empty.
        + If the state of the device matches regexp, then icon-name will be + displayed as the status icon in the room, and (if specified) clicking + on the icon executes cmd. If fhem cannot find icon-name, then the + status text will be displayed. + Example:
        +
          + attr lamp devStateIcon on:closed off:open
          + attr lamp devStateIcon on::A0 off::AI
          + attr lamp devStateIcon .*:noIcon
          +
        + Note: if the image is referencing an SVG icon, then you can use the + @colorname suffix to color the image. E.g.:
        +
          + attr Fax devStateIcon on:control_building_empty@red + off:control_building_filled:278727 +
        + If the cmd is noFhemwebLink, then no HTML-link will be generated, i.e. + nothing will happen when clicking on the icon or text. + +
      + Second form:
      +
        + Perl code enclosed in {}. If the code returns undef, then the default + icon is used, if it retuns a string enclosed in <>, then it is + interpreted as an html string. Else the string is interpreted as a + devStateIcon of the first fom, see above. + Example:
        + {'<div + style="width:32px;height:32px;background-color:green"></div>'} +
      +
    • +
      + + +
    • devStateStyle
      + Specify an HTML style for the given device, e.g.:
      +
        + attr sensor devStateStyle style="text-align:left;;font-weight:bold;;"
        +
      +
    • +
      + +
    • deviceOverview
      + Configures if the device line from the room view (device icon, state + icon and webCmds/cmdIcons) should also be shown in the device detail + view. Can be set to always, onClick, iconOnly or never. Default is + always. +

    • + + +
    • editConfig
      + If this FHEMWEB attribute is set to 1, then you will be able to edit + the FHEM configuration file (fhem.cfg) in the "Edit files" section. + After saving this file a rereadcfg is executed automatically, which has + a lot of side effects.
      +

    • + + +
    • editFileList
      + Specify the list of Files shown in "Edit Files" section. It is a + newline separated list of triples, the first is the Title, the next is + the directory to search for as a perl expression(!), the third the + regular expression. Default + is: +
        + + Own modules and helper files:$MW_dir:^(.*sh|[0-9][0-9].*Util.*pm|.*cfg|.*holiday|myUtilsTemplate.pm|.*layout)$
        + Gplot files:$FW_gplotdir:^.*gplot$
        + Styles:$FW_cssdir:^.*(css|svg)$
        +
        +
      + NOTE: The directory spec is not flexible: all .js/.css/_defs.svg files + come from www/pgm2 ($FW_cssdir), .gplot files from $FW_gplotdir + (www/gplot), everything else from $MW_dir (FHEM). +

    • + + +
    • endPlotNow
      + If this FHEMWEB attribute is set to 1, then day and hour plots will + end at current time. Else the whole day, the 6 hour period starting at + 0, 6, 12 or 18 hour or the whole hour will be shown. This attribute + is not used if the SVG has the attribute startDate defined.
      +

    • + + +
    • endPlotToday
      + If this FHEMWEB attribute is set to 1, then week and month plots will + end today. Else the current week or the current month will be shown. +
      +

    • + + +
    • fwcompress
      + Enable compressing the HTML data (default is 1, i.e. yes, use 0 to switch it off). +
    • +
      + + +
    • forbiddenroom
      + just like hiddenroom (see below), but accessing the room or the + detailed view via direct URL is prohibited. +
    • +
      + + +
    • hiddengroup
      + Comma separated list of groups to "hide", i.e. not to show in any room + of this FHEMWEB instance.
      + Example: attr WEBtablet hiddengroup FileLog,dummy,at,notify +
    • +
      + + +
    • hiddengroupRegexp
      + One regexp for the same purpose as hiddengroup. +
    • +
      + + +
    • hiddenroom
      + Comma separated list of rooms to "hide", i.e. not to show. Special + values are input, detail and save, in which case the input areas, link + to the detailed views or save button are hidden (although each aspect + still can be addressed through URL manipulation).
      + The list can also contain values from the additional "Howto/Wiki/FAQ" + block. +
    • +
      + + +
    • hiddenroomRegexp
      + One regexp for the same purpose as hiddenroom. Example: +
        + attr WEB hiddenroomRegexp .*config +
      + Note: the special values input, detail and save cannot be specified + with hiddenroomRegexp. +
    • +
      + + +
    • httpHeader
      + One or more HTTP header lines to be sent out with each answer. Example: +
        + attr WEB httpHeader X-Clacks-Overhead: GNU Terry Pratchett +
      +
    • +
      + + + +
    • HTTPS
      + Enable HTTPS connections. This feature requires the perl module + IO::Socket::SSL, to be installed with cpan -i IO::Socket::SSL or + apt-get install libio-socket-ssl-perl; OSX and the FritzBox-7390 + already have this module.
      + + A local certificate has to be generated into a directory called certs, + this directory must be in the modpath + directory, at the same level as the FHEM directory. +
        + mkdir certs
        + cd certs
        + openssl req -new -x509 -nodes -out server-cert.pem -days 3650 -keyout server-key.pem +
      +
      +
    • + + +
    • icon
      + Set the icon for a device in the room overview. There is an + icon-chooser in FHEMWEB to ease this task. Setting icons for the room + itself is indirect: there must exist an icon with the name + ico<ROOMNAME>.png in the iconPath. +
    • +
      + + +
    • iconPath
      + colon separated list of directories where the icons are read from. + The directories start in the fhem/www/images directory. The default is + $styleSheetPrefix:fhemSVG:openautomation:default
      + Set it to fhemSVG:openautomation to get only SVG images. +
    • +
      + + +
    • JavaScripts
      + Space separated list of JavaScript files to be included. The filenames + are relative to the www directory. For each file an additional + user-settable FHEMWEB attribute will be created, to pass parameters to + the script. The name of this additional attribute gets the Param + suffix, directory and the fhem_ prefix will be deleted. Example: +
        + attr WEB JavaScripts codemirror/fhem_codemirror.js
        + attr WEB codemirrorParam { "theme":"blackboard", "lineNumbers":true } +
      +

    • + + +
    • longpoll [0|1|websocket]
      + If activated, the browser is notifed when device states, readings or + attributes are changed, a reload of the page is not necessary. + Default is 1 (on), use 0 to deactivate it.
      + If websocket is specified, then this API is used to notify the browser, + else HTTP longpoll. Note: some older browser do not implement websocket. +
    • +
      + + +
    • longpollSVG
      + Reloads an SVG weblink, if an event should modify its content. Since + an exact determination of the affected events is too complicated, we + need some help from the definition in the .gplot file: the filter used + there (second parameter if the source is FileLog) must either contain + only the deviceName or have the form deviceName.event or deviceName.*. + This is always the case when using the Plot + editor. The SVG will be reloaded for any event triggered by + this deviceName. Default is off. Note: the plotEmbed attribute must be + set. +
    • +
      + + + +
    • mainInputLength
      + length of the maininput text widget in characters (decimal number). +
    • +
      + + +
    • menuEntries
      + Comma separated list of name,html-link pairs to display in the + left-side list. Example:
      + attr WEB menuEntries fhem.de,http://fhem.de,culfw.de,http://culfw.de
      + attr WEB menuEntries + AlarmOn,http://fhemhost:8083/fhem?cmd=set%20alarm%20on
      +
    • +
      + + + +
    • nameDisplay
      + The argument is perl code, which is executed for each single device in + the room to determine the name displayed. $DEVICE is the name of the + current device, and $ALIAS is the value of the alias attribute or the + name of the device, if no alias is set. E.g. you can add a a global + userattr named alias_hu for the Hungarian translation, and specify + nameDisplay for the hungarian FHEMWEB instance as +
        + AttrVal($DEVICE, "alias_hu", $ALIAS) +
      +
    • +
      + + +
    • nrAxis
      + the number of axis for which space should be reserved on the left and + right sides of a plot and optionaly how many axes should realy be used + on each side, separated by comma: left,right[,useLeft,useRight]. You + can set individual numbers by setting the nrAxis of the SVG. Default is + 1,1. +

    • + + +
    • ploteditor
      + Configures if the Plot editor should be shown + in the SVG detail view. + Can be set to always, onClick or never. Default is always. +

    • + + +
    • plotEmbed
      + If set (to 1), SVG plots will be rendered as part of <embed> + tags, as in the past this was the only way to display SVG. Setting + plotEmbed to 0 (the default) will render SVG in-place.
      +

    • + + +
    • plotfork
      + If set to a nonzero value, run part of the processing (e.g. SVG plot generation or RSS feeds) in + parallel processes, default is 0. Note: do not use it on systems with + small memory footprint. +

    • + + +
    • plotmode
      + Specifies how to generate the plots: +
        +
      • SVG
        + The plots are created with the SVG module. + This is the default.
      • +
      • gnuplot-scroll
        + The plots are created with the gnuplot program. The gnuplot + output terminal PNG is assumed. Scrolling to historical values + is also possible, just like with SVG.
      • +
      • gnuplot-scroll-svg
        + Like gnuplot-scroll, but the output terminal SVG is assumed.
      • +
      +

    • + + +
    • plotsize
      + the default size of the plot, in pixels, separated by comma: + width,height. You can set individual sizes by setting the plotsize of + the SVG. Default is 800,160 for desktop, and 480,160 for + smallscreen. +

    • + + +
    • plotWeekStartDay
      + Start the week-zoom of the SVG plots with this day. + 0 is Sunday, 1 is Monday, etc.
      +

    • + + +
    • redirectCmds
      + Clear the browser URL window after issuing the command by redirecting + the browser, as a reload for the same site might have unintended + side-effects. Default is 1 (enabled). Disable it by setting this + attribute to 0 if you want to study the command syntax, in order to + communicate with FHEMWEB. +
    • +
      + + +
    • refresh
      + If set, a http-equiv="refresh" entry will be genererated with the given + argument (i.e. the browser will reload the page after the given + seconds). +

    • + + +
    • reverseLogs
      + Display the lines from the logfile in a reversed order, newest on the + top, so that you dont have to scroll down to look at the latest entries. + Note: enabling this attribute will prevent FHEMWEB from streaming + logfiles, resulting in a considerably increased memory consumption + (about 6 times the size of the file on the disk). +
    • +
      + + +
    • roomIcons
      + Space separated list of room:icon pairs, to override the default + behaviour of showing an icon, if there is one with the name of + "icoRoomName". This is the correct way to remove the icon for the room + Everything, or to set one for rooms with / in the name (e.g. + Anlagen/EDV). The first part is treated as regexp, so space is + represented by a dot. Example:
      + attr WEB roomIcons Anlagen.EDV:icoEverything +
    • +
      + + +
    • smallscreenCommands
      + If set to 1, commands, slider and dropdown menues will appear in + smallscreen landscape mode. +

    • + + +
    • sortby
      + Take the value of this attribute when sorting the devices in the room + overview instead of the alias, or if that is missing the devicename + itself. If the sortby value is enclosed in {} than it is evaluated as a + perl expression. $NAME is set to the device name. +
    • +
      + + +
    • showUsedFiles
      + In the Edit files section, show only the used files. + Note: currently this is only working for the "Gplot files" section. +
    • +
      + + +
    • sortRooms
      + Space separated list of rooms to override the default sort order of the + room links. As the rooms in this attribute are actually regexps, space + in the roomname has to be specified as dot (.). + Example:
      + attr WEB sortRooms DG OG EG Keller +
    • +
      + +
    • sslVersion
      + See the global attribute sslVersion. +

    • + + +
    • sslCertPrefix
      + Set the prefix for the SSL certificate, default is certs/server-, see + also the HTTPS attribute. +

    • + + +
    • styleData
      + data-storage used by dynamic styles like f18 +

    • + + +
    • stylesheetPrefix
      + prefix for the files style.css, svg_style.css and svg_defs.svg. If the + file with the prefix is missing, the default file (without prefix) will + be used. These files have to be placed into the FHEM directory, and can + be selected directly from the "Select style" FHEMWEB menu entry. Example: +
        + attr WEB stylesheetPrefix dark
        +
        + Referenced files:
        +
          + darksvg_defs.svg
          + darksvg_style.css
          + darkstyle.css
          +
        +
        +
      + Note:if the argument contains the string smallscreen or touchpad, + then FHEMWEB will optimize the layout/access for small screen size (i.e. + smartphones) or touchpad devices (i.e. tablets)
      + + The default configuration installs 3 FHEMWEB instances: port 8083 for + desktop browsers, port 8084 for smallscreen, and 8085 for touchpad.
      + + If touchpad or smallscreen is specified, then WebApp support is + activated: After viewing the site on the iPhone or iPad in Safari, you + can add a link to the home-screen to get full-screen support. Links are + rendered differently in this mode to avoid switching back to the "normal" + browser. +
    • +
      + + +
    • SVGcache
      + if set, cache plots which won't change any more (the end-date is prior + to the current timestamp). The files are written to the www/SVGcache + directory. Default is off.
      + See also the clearSvgCache command for clearing the cache. +

    • + + +
    • title
      + Sets the title of the page. If enclosed in {} the content is evaluated. +

    • + + +
    • viewport
      + Sets the "viewport" attribute in the HTML header. This can for + example be used to force the width of the page or disable zooming.
      + Example: attr WEB viewport + width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no +

    • + + +
    • webCmd
      + Colon separated list of commands to be shown in the room overview for a + certain device. Has no effect on smallscreen devices, see the + devStateIcon command for an alternative.
      + Example: +
        + attr lamp webCmd on:off:on-for-timer 10
        +
      +
      + + The first specified command is looked up in the "set device ?" list + (see the setList attribute for dummy devices). + If there it contains some known modifiers (colon, followed + by a comma separated list), then a different widget will be displayed. + See also the widgetOverride attribute below. Examples: +
        + define d1 dummy
        + attr d1 webCmd state
        + attr d1 readingList state
        + attr d1 setList state:on,off

        + + define d2 dummy
        + attr d2 webCmd state
        + attr d2 readingList state
        + attr d2 setList state:slider,0,1,10

        + + define d3 dummy
        + attr d3 webCmd state
        + attr d3 readingList state
        + attr d3 setList state:time
        +
      + If the command is state, then the value will be used as a command.
      + Note: this is an attribute for the displayed device, not for the FHEMWEB + instance. +
    • +
      + + +
    • webCmdLabel
      + Colon separated list of labels, used to prefix each webCmd. The number + of labels must exactly match the number of webCmds. To implement + multiple rows, insert a return character after the text and before the + colon.

    • + + +
    • webname
      + Path after the http://hostname:port/ specification. Defaults to fhem, + i.e the default http address is http://localhost:8083/fhem +

    • + + +
    • widgetOverride
      + Space separated list of name:modifier pairs, to override the widget + for a set/get/attribute specified by the module author. + Following is the list of known modifiers: +
        + +
      +
    • +
      +
    +
+=end html + +=begin html_DE + + +

FHEMWEB

+
    + FHEMWEB ist das default WEB-Frontend, es implementiert auch einen einfachen + Webserver (optional mit Basic-Auth und HTTPS). +

    + + + Define +
      + define <name> FHEMWEB <tcp-portnr> [global|IP] +

      + Aktiviert das Webfrontend auf dem Port <tcp-portnr>. Mit dem + Parameter global werden Anfragen von allen Netzwerkschnittstellen + akzeptiert (nicht nur vom localhost / 127.0.0.1). Falls IP angegeben wurde, + dann werden nur Anfragen an diese IP Adresse akzeptiert.
      + + Informationen für den Betrieb mit IPv6 finden Sie hier.
      +
    +
    + + + Set +
      +
    • rereadicons
      + Damit wird die Liste der Icons neu eingelesen, für den Fall, dass + Sie Icons löschen oder hinzufügen. +
    • +
    • clearSvgCache
      + Im Verzeichnis www/SVGcache werden SVG Daten zwischengespeichert, wenn + das Attribut SVGcache gesetzt ist. Mit diesem Befehl leeren Sie diesen + Zwischenspeicher. +
    • +
    +
    + + + Get +
      +
    • icon <logical icon>
      + Liefert den absoluten Pfad des (logischen) Icons zurück. Beispiel: +
        + get myFHEMWEB icon FS20.on
        + /data/Homeautomation/fhem/FHEM/FS20.on.png +
        +
    • +
    • pathlist
      + Zeigt diejenigen Verzeichnisse an, in welchen die verschiedenen Dateien + für FHEMWEB liegen. +
    • +

      + +
    + + + Attribute +
      + +
    • addHtmlTitle
      + Falls der Wert 0 ist, wird bei den set/get/attr Parametern in der + DetailAnsicht der Geräte kein title Attribut gesetzt. Das is bei + manchen Screenreadern erforderlich. Die Voreinstellung ist 1. +

    • + +
    • addStateEvent

    • + +
    • alias_<RoomName>
      + Falls man das Attribut alias_<RoomName> definiert, und dieses + Attribut für ein Gerät setzt, dann wird dieser Wert bei + Anzeige von <RoomName> verwendet.
      + Achtung: man kann im userattr auch alias_.* verwenden um alle + möglichen Räume abzudecken, in diesem Fall wird aber die + Attributauswahl in der Detailansicht für alias_.* nicht + funktionieren. +

    • + +
    • allowfrom +

    • + +
    • allowedCommands, basicAuth, basicAuthMsg
      + Diese Attribute müssen ab sofort bei dem passenden allowed Gerät angelegt werden, und sind + für eine FHEMWEB Instanz unerwünscht. +

    • + + +
    • allowedHttpMethods
      + FHEMWEB implementiert die HTTP Methoden GET, POST und OPTIONS. Manche + externe Geräte benötigen HEAD, das ist aber in FHEMWEB nicht + korrekt implementiert, da FHEMWEB immer ein body zurückliefert, was + laut Spec falsch ist. Da ein body in manchen Fällen kein Problem + ist, kann man HEAD durch setzen dieses Attributes auf GET|POST|HEAD + aktivieren, die Voreinstellung ist GET|POST. OPTIONS ist immer + aktiviert. +

    • + + + +
    • closeConn
      + Falls gesetzt, wird pro TCP Verbindung nur ein HTTP Request + durchgeführt. Für iOS9 WebApp startups scheint es zu helfen. +

    • + + +
    • cmdIcon
      + Leerzeichen getrennte Auflistung von cmd:iconName Paaren. + Falls gesetzt, wird das webCmd text durch den icon gesetzt. + Am einfachsten setzt man cmdIcon indem man "Extend devStateIcon" im + Detail-Ansicht verwendet, und den Wert nach cmdIcon kopiert.
      + Beispiel:
        + attr lamp cmdIcon on:control_centr_arrow_up off:control_centr_arrow_down +
      +

    • + + +
    • column
      + Damit werden mehrere Spalten für einen Raum angezeigt, indem + sie verschiedene Gruppen Spalten zuordnen. Beispiel:
      +
        + attr WEB column LivingRoom:FS20,notify|FHZ,notify DiningRoom:FS20|FHZ +
      + + In diesem Beispiel werden im Raum LivingRoom die FS20 sowie die notify + Gruppe in der ersten Spalte, die FHZ und das notify in der zweiten + Spalte angezeigt.
      + + Anmerkungen: einige Elemente, wie SVG Plots und readingsGroup + können nur dann Teil einer Spalte sein wenn sie in group stehen. Dieses Attribut kann man zum sortieren + der Gruppen auch dann verwenden, wenn man nur eine Spalte hat. + Leerzeichen im Raum- und Gruppennamen sind für dieses Attribut als + %20 zu schreiben. Raum- und Gruppenspezifikation ist jeweils ein + %regulärer Ausdruck. +

    • + + +
    • confirmDelete
      + Löschaktionen weden mit einem Dialog bestätigt. + Falls dieses Attribut auf 0 gesetzt ist, entfällt das. +
    • +
      + + +
    • confirmJSError
      + JavaScript Fehler werden per Voreinstellung in einem Dialog gemeldet. + Durch setzen dieses Attributes auf 0 werden solche Fehler nicht + gemeldet. +
    • +
      + + +
    • CORS
      + Wenn auf 1 gestellt, wird FHEMWEB einen "Cross origin resource sharing" + Header bereitstellen, näheres siehe Wikipedia. +

    • + + +
    • csrfToken
      + Falls gesetzt, wird der Wert des Attributes als fwcsrf Parameter bei + jedem über FHEMWEB abgesetzten Kommando verlangt, es dient zum + Schutz von Cross Site Resource Forgery Angriffen. + Falls der Wert random ist, dann wird ein Zufallswert beim jeden FHEMWEB + Start neu generiert, falls er none ist, dann wird kein Parameter + verlangt. Default ist random für featurelevel 5.8 und + größer, und none für featurelevel kleiner 5.8 +

    • + + +
    • csrfTokenHTTPHeader
      + Falls gesetzt (Voreinstellung), FHEMWEB sendet im HTTP Header den + csrfToken als X-FHEM-csrfToken, das wird von manchen FHEM-Clients + benutzt. Mit 0 kann man das abstellen, um Sites wie shodan.io die + Erkennung von FHEM zu erschweren.

    • + + +
    • CssFiles
      + Leerzeichen getrennte Liste von .css Dateien, die geladen werden. + Die Dateinamen sind relativ zum www Verzeichnis anzugeben. Beispiel: +
        + attr WEB CssFiles pgm2/mystyle.css +
      +

    • + + +
    • Css
      + CSS, was nach dem CssFiles Abschnitt im Header eingefuegt wird. +

    • + + +
    • defaultRoom
      + Zeigt den angegebenen Raum an falls kein Raum explizit ausgewählt + wurde. Achtung: falls gesetzt, wird motd nicht mehr angezeigt. + Beispiel:
      + attr WEB defaultRoom Zentrale +

    • + + +
    • devStateIcon
      + Erste Variante:
      +
        + Leerzeichen getrennte Auflistung von regexp:icon-name:cmd + Dreierpärchen, icon-name und cmd dürfen leer sein.
        + + Wenn der Zustand des Gerätes mit der regexp übereinstimmt, + wird als icon-name das entsprechende Status Icon angezeigt, und (falls + definiert), löst ein Klick auf das Icon das entsprechende cmd aus. + Wenn fhem icon-name nicht finden kann, wird der Status als Text + angezeigt. + Beispiel:
        +
          + attr lamp devStateIcon on:closed off:open
          + attr lamp devStateIcon on::A0 off::AI
          + attr lamp devStateIcon .*:noIcon
          +
        + Anmerkung: Wenn das Icon ein SVG Bild ist, kann das @colorname Suffix + verwendet werden um das Icon einzufärben. Z.B.:
        +
          + attr Fax devStateIcon on:control_building_empty@red + off:control_building_filled:278727 +
        + Falls cmd noFhemwebLink ist, dann wird kein HTML-Link generiert, d.h. + es passiert nichts, wenn man auf das Icon/Text klickt. +
      + Zweite Variante:
      +
        + Perl regexp eingeschlossen in {}. Wenn der Code undef + zurückliefert, wird das Standard Icon verwendet; wird ein String + in <> zurück geliefert, wird dieser als HTML String interpretiert. + Andernfalls wird der String als devStateIcon gemäß der + ersten Variante interpretiert, siehe oben. Beispiel:
        + + {'<div style="width:32px;height:32px;background-color:green"></div>'} +
      +

    • + + +
    • devStateStyle
      + Für ein best. Gerät einen best. HTML-Style benutzen. + Beispiel:
      +
        + attr sensor devStateStyle style="text-align:left;;font-weight:bold;;"
        +
      +

    • + +
    • deviceOverview
      + Gibt an ob die Darstellung aus der Raum-Ansicht (Zeile mit + Gerüteicon, Stateicon und webCmds/cmdIcons) auch in der + Detail-Ansicht angezeigt werden soll. Kann auf always, onClick, + iconOnly oder never gesetzt werden. Der Default ist always. +

    • + + +
    • editConfig
      + Falls dieses FHEMWEB Attribut (auf 1) gesetzt ist, dann kann man die + FHEM Konfigurationsdatei in dem "Edit files" Abschnitt bearbeiten. Beim + Speichern dieser Datei wird automatisch rereadcfg ausgefuehrt, was + diverse Nebeneffekte hat.
      +

    • + + +
    • editFileList
      + Definiert die Liste der angezeigten Dateien in der "Edit Files" + Abschnitt. Es ist eine Newline getrennte Liste von Tripeln bestehend + aus Titel, Verzeichnis für die Suche als perl Ausdruck(!), und + Regexp. Die Voreinstellung ist: +
        + + Own modules and helper files:$MW_dir:^(.*sh|[0-9][0-9].*Util.*pm|.*cfg|.*holiday|myUtilsTemplate.pm|.*layout)$
        + Gplot files:$FW_gplotdir:^.*gplot$
        + Styles:$FW_cssdir:^.*(css|svg)$
        +
        +
      + Achtung: die Verzeichnis Angabe ist nicht flexibel: alle + .js/.css/_defs.svg Dateien sind in www/pgm2 ($FW_cssdir), .gplot + Dateien in $FW_gplotdir (www/gplot), alles andere in $MW_dir (FHEM). +

    • + + +
    • endPlotNow
      + Wenn Sie dieses FHEMWEB Attribut auf 1 setzen, werden Tages und + Stunden-Plots zur aktuellen Zeit beendet. (Ähnlich wie + endPlotToday, nur eben minütlich). + Ansonsten wird der gesamte Tag oder eine 6 Stunden Periode (0, 6, 12, + 18 Stunde) gezeigt. Dieses Attribut wird nicht verwendet, wenn das SVG + Attribut startDate benutzt wird.
      +

    • + + +
    • endPlotToday
      + Wird dieses FHEMWEB Attribut gesetzt, so enden Wochen- bzw. Monatsplots + am aktuellen Tag, sonst wird die aktuelle Woche/Monat angezeigt. +

    • + + +
    • forbiddenroom
      + Wie hiddenroom, aber der Zugriff auf die Raum- oder Detailansicht + über direkte URL-Eingabe wird unterbunden. +

    • + + +
    • fwcompress
      + Aktiviert die HTML Datenkompression (Standard ist 1, also ja, 0 stellt + die Kompression aus). +

    • + + +
    • hiddengroup
      + Wie hiddenroom (siehe unten), jedoch auf Gerätegruppen bezogen. +
      + Beispiel: attr WEBtablet hiddengroup FileLog,dummy,at,notify +

    • + + +
    • hiddengroupRegexp
      + Ein regulärer Ausdruck, um Gruppen zu verstecken. +
    • +
      + + +
    • hiddenroom
      + Eine Komma getrennte Liste, um Räume zu verstecken, d.h. nicht + anzuzeigen. Besondere Werte sind input, detail und save. In diesem + Fall werden diverse Eingabefelder ausgeblendent. Durch direktes Aufrufen + der URL sind diese Räume weiterhin erreichbar!
      + Ebenso können Einträge in den Logfile/Commandref/etc Block + versteckt werden.

    • + + +
    • hiddenroomRegexp
      + Ein regulärer Ausdruck, um Räume zu verstecken. Beispiel: +
        + attr WEB hiddenroomRegexp .*config +
      + Achtung: die besonderen Werte input, detail und save müssen mit + hiddenroom spezifiziert werden. +
    • +
      + + +
    • httpHeader
      + Eine oder mehrere HTTP-Header Zeile, die in jede Antwort eingebettet + wird. Beispiel: +
        + attr WEB httpHeader X-Clacks-Overhead: GNU Terry Pratchett +
      +
    • +
      + + +
    • HTTPS
      + Ermöglicht HTTPS Verbindungen. Es werden die Perl Module + IO::Socket::SSL benötigt, installierbar mit cpan -i + IO::Socket::SSL oder apt-get install libio-socket-ssl-perl; (OSX und + die FritzBox-7390 haben dieses Modul schon installiert.)
      + + Ein lokales Zertifikat muss im Verzeichis certs erzeugt werden. + Dieses Verzeichnis muss im modpath + angegeben werden, also auf der gleichen Ebene wie das FHEM Verzeichnis. + Beispiel: +
        + mkdir certs
        + cd certs
        + openssl req -new -x509 -nodes -out server-cert.pem -days 3650 -keyout + server-key.pem +
      + +
      +
    • + + +
    • icon
      + Damit definiert man ein Icon für die einzelnen Geräte in der + Raumübersicht. Es gibt einen passenden Link in der Detailansicht + um das zu vereinfachen. Um ein Bild für die Räume selbst zu + definieren muss ein Icon mit dem Namen ico<Raumname>.png im + iconPath existieren (oder man verwendet roomIcons, s.u.) +

    • + + +
    • iconPath
      + Durch Doppelpunkt getrennte Aufzählung der Verzeichnisse, in + welchen nach Icons gesucht wird. Die Verzeichnisse müssen unter + fhem/www/images angelegt sein. Standardeinstellung ist: + $styleSheetPrefix:fhemSVG:openautomation:default
      + Setzen Sie den Wert auf fhemSVG:openautomation um nur SVG Bilder zu + benutzen. +

    • + + +
    • JavaScripts
      + Leerzeichen getrennte Liste von JavaScript Dateien, die geladen werden. + Die Dateinamen sind relativ zum www Verzeichnis anzugeben. Für + jede Datei wird ein zusätzliches Attribut angelegt, damit der + Benutzer dem Skript Parameter weiterreichen kann. Bei diesem + Attributnamen werden Verzeichnisname und fhem_ Präfix entfernt + und Param als Suffix hinzugefügt. Beispiel: +
        + attr WEB JavaScripts codemirror/fhem_codemirror.js
        + attr WEB codemirrorParam { "theme":"blackboard", "lineNumbers":true } +
      +

    • + + +
    • longpoll [0|1|websocket]
      + Falls gesetzt, FHEMWEB benachrichtigt den Browser, wenn + Gerätestatuus, Readings or Attribute sich ändern, ein + Neuladen der Seite ist nicht notwendig. Zum deaktivieren 0 verwenden. +
      + Falls websocket spezifiziert ist, läuft die Benachrichtigung des + Browsers über dieses Verfahren sonst über HTTP longpoll. + Achtung: ältere Browser haben keine websocket Implementierung. +

    • + + + +
    • longpollSVG
      + Lädt SVG Instanzen erneut, falls ein Ereignis dessen Inhalt + ändert. Funktioniert nur, falls die dazugehörige Definition + der Quelle in der .gplot Datei folgenden Form hat: deviceName.Event + bzw. deviceName.*. Wenn man den Plot Editor + benutzt, ist das übrigens immer der Fall. Die SVG Datei wird bei + jedem auslösenden Event dieses Gerätes neu geladen. + Die Voreinstellung ist aus. Achtung: das plotEmbed Attribute muss + gesetzt sein. +

    • + + +
    • mainInputLength
      + Länge des maininput Eingabefeldes (Anzahl der Buchstaben, + Ganzzahl). +

    • + + +
    • menuEntries
      + Komma getrennte Liste; diese Links werden im linken Menü angezeigt. + Beispiel:
      + attr WEB menuEntries fhem.de,http://fhem.de,culfw.de,http://culfw.de
      + attr WEB menuEntries + AlarmOn,http://fhemhost:8083/fhem?cmd=set%20alarm%20on
      +

    • + + +
    • nameDisplay
      + Das Argument ist Perl-Code, was für jedes Gerät in der + Raum-Übersicht ausgeführt wird, um den angezeigten Namen zu + berechnen. Dabei kann man die Variable $DEVICE für den aktuellen + Gerätenamen, und $ALIAS für den aktuellen alias bzw. Name, + falls alias nicht gesetzt ist, verwenden. Z.Bsp. für eine FHEMWEB + Instanz mit ungarischer Anzeige fügt man ein global userattr + alias_hu hinzu, und man setzt nameDisplay für diese FHEMWEB + Instanz auf dem Wert: +
        + AttrVal($DEVICE, "alias_hu", $ALIAS) +
      +
    • +
      + + +
    • nrAxis
      + (bei mehrfach-Y-Achsen im SVG-Plot) Die Darstellung der Y Achsen + benötigt Platz. Hierdurch geben Sie an wie viele Achsen Sie + links,rechts [useLeft,useRight] benötigen. Default ist 1,1 (also 1 + Achse links, 1 Achse rechts). +

    • + + +
    • ploteditor
      + Gibt an ob der Plot Editor in der SVG detail + ansicht angezeigt werden soll. Kann auf always, onClick oder never + gesetzt werden. Der Default ist always. +

    • + + +
    • plotEmbed 0
      + Falls gesetzt (auf 1), dann werden SVG Grafiken mit <embed> Tags + gerendert, da auf älteren Browsern das die einzige + Möglichkeit war, SVG dastellen zu können. Falls 0 (die + Voreinstellung), dann werden die SVG Grafiken "in-place" gezeichnet. +

    • + + +
    • plotfork
      + Falls gesetzt, dann werden bestimmte Berechnungen (z.Bsp. SVG und RSS) + auf nebenläufige Prozesse verteilt. Voreinstellung ist 0. Achtung: + nicht auf Systemen mit wenig Hauptspeicher verwenden. +

    • + + +
    • plotmode
      + Spezifiziert, wie Plots erzeugt werden sollen: +
        +
      • SVG
        + Die Plots werden mit Hilfe des SVG Moduls als SVG + Grafik gerendert. Das ist die Standardeinstellung.
      • + +
      • gnuplot-scroll
        + Die plots werden mit dem Programm gnuplot erstellt. Das output + terminal ist PNG. Der einfache Zugriff auf historische Daten + ist möglich (analog SVG). +
      • + +
      • gnuplot-scroll-svg
        + Wie gnuplot-scroll, aber als output terminal wird SVG angenommen. +
      • +
      +

    • + + +
    • plotsize
      + gibt die Standardbildgröße aller erzeugten Plots an als + Breite,Höhe an. Um einem individuellen Plot die Größe zu + ändern muss dieses Attribut bei der entsprechenden SVG Instanz + gesetzt werden. Default sind 800,160 für Desktop und 480,160 + für Smallscreen +

    • + + +
    • plotWeekStartDay
      + Starte das Plot in der Wochen-Ansicht mit diesem Tag. + 0 ist Sonntag, 1 ist Montag, usw. +

    • + + +
    • redirectCmds
      + Damit wird das URL Eingabefeld des Browser nach einem Befehl geleert. + Standard ist eingeschaltet (1), ausschalten kann man es durch + setzen des Attributs auf 0, z.Bsp. um den Syntax der Kommunikation mit + FHEMWEB zu untersuchen. +

    • + + +
    • refresh
      + Damit erzeugen Sie auf den ausgegebenen Webseiten einen automatischen + Refresh, z.B. nach 5 Sekunden. +

    • + + +
    • reverseLogs
      + Damit wird das Logfile umsortiert, die neuesten Einträge stehen + oben. Der Vorteil ist, dass man nicht runterscrollen muss um den + neuesten Eintrag zu sehen, der Nachteil dass FHEM damit deutlich mehr + Hauptspeicher benötigt, etwa 6 mal so viel, wie das Logfile auf + dem Datenträger groß ist. Das kann auf Systemen mit wenig + Speicher (FRITZ!Box) zum Terminieren des FHEM Prozesses durch das + Betriebssystem führen. +

    • + + +
    • roomIcons
      + Leerzeichen getrennte Liste von room:icon Zuordnungen + Der erste Teil wird als regexp interpretiert, daher muss ein + Leerzeichen als Punkt geschrieben werden. Beispiel:
      + attr WEB roomIcons Anlagen.EDV:icoEverything +

    • + + +
    • sortby
      + Der Wert dieses Attributs wird zum sortieren von Geräten in + Räumen verwendet, sonst wäre es der Alias oder, wenn keiner + da ist, der Gerätename selbst. Falls der Wert des sortby + Attributes in {} eingeschlossen ist, dann wird er als ein perl Ausdruck + evaluiert. $NAME wird auf dem Gerätenamen gesetzt. +

    • + + +
    • showUsedFiles
      + Zeige nur die verwendeten Dateien in der "Edit files" Abschnitt. + Achtung: aktuell ist das nur für den "Gplot files" Abschnitt + implementiert. +
    • +
      + + +
    • sortRooms
      + Durch Leerzeichen getrennte Liste von Räumen, um deren Reihenfolge + zu definieren. + Da die Räume in diesem Attribut als Regexp interpretiert werden, + sind Leerzeichen im Raumnamen als Punkt (.) zu hinterlegen. + Beispiel:
      + attr WEB sortRooms DG OG EG Keller +

    • + + +
    • smallscreenCommands
      + Falls auf 1 gesetzt werden Kommandos, Slider und Dropdown Menüs im + Smallscreen Landscape Modus angezeigt. +

    • + +
    • sslVersion
      + Siehe das global Attribut sslVersion. +

    • + + +
    • sslCertPrefix
      + Setzt das Präfix der SSL-Zertifikate, die Voreinstellung ist + certs/server-, siehe auch das HTTP Attribut. +

    • + + +
    • styleData
      + wird von dynamischen styles wie f18 werwendet +

    • + + +
    • stylesheetPrefix
      + Präfix für die Dateien style.css, svg_style.css und + svg_defs.svg. Wenn die Datei mit dem Präfix fehlt, wird die Default + Datei (ohne Präfix) verwendet. Diese Dateien müssen im FHEM + Ordner liegen und können direkt mit "Select style" im FHEMWEB + Menüeintrag ausgewählt werden. Beispiel: +
        + attr WEB stylesheetPrefix dark
        +
        + Referenzdateien:
        +
          + darksvg_defs.svg
          + darksvg_style.css
          + darkstyle.css
          +
        +
        +
      + Anmerkung:Wenn der Parametername smallscreen oder touchpad + enthält, wird FHEMWEB das Layout/den Zugriff für entsprechende + Geräte (Smartphones oder Touchpads) optimieren
      + + Standardmäßig werden 3 FHEMWEB Instanzen aktiviert: Port 8083 + für Desktop Browser, Port 8084 für Smallscreen, und 8085 + für Touchpad.
      + + Wenn touchpad oder smallscreen benutzt werden, wird WebApp support + aktiviert: Nachdem Sie eine Seite am iPhone oder iPad mit Safari + angesehen haben, können Sie einen Link auf den Homescreen anlegen um + die Seite im Fullscreen Modus zu sehen. Links werden in diesem Modus + anders gerendert, um ein "Zurückfallen" in den "normalen" Browser zu + verhindern. +

    • + + +
    • SVGcache
      + Plots die sich nicht mehr ändern, werden im SVGCache Verzeichnis + (www/SVGcache) gespeichert, um die erneute, rechenintensive + Berechnung der Grafiken zu vermeiden. Default ist 0, d.h. aus.
      + Siehe den clearSvgCache Befehl um diese Daten zu löschen. +

    • + + +
    • title
      + Setzt den Titel der Seite. Falls in {} eingeschlossen, dann wird es + als Perl Ausdruck evaluiert. +

    • + + +
    • viewport
      + Setzt das "viewport" Attribut im HTML Header. Das kann benutzt + werden um z.B. die Breite fest vorzugeben oder Zoomen zu verhindern.
      + Beispiel: attr WEB viewport + width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no +

    • + + +
    • webCmd
      + Durch Doppelpunkte getrennte Auflistung von Befehlen, die für ein + bestimmtes Gerät gelten sollen. Funktioniert nicht mit + smallscreen, ein Ersatz dafür ist der devStateIcon Befehl.
      + Beispiel: +
        + attr lamp webCmd on:off:on-for-timer 10
        +
      +
      + + Der erste angegebene Befehl wird in der "set device ?" list + nachgeschlagen (Siehe das setList Attrib + für Dummy Geräte). Wenn dort bekannte Modifier sind, + wird ein anderes Widget angezeigt. Siehe auch widgetOverride.
      + Wenn der Befehl state ist, wird der Wert als Kommando interpretiert.
      + Beispiele: +
        + define d1 dummy
        + attr d1 webCmd state
        + attr d1 setList state:on,off
        + define d2 dummy
        + attr d2 webCmd state
        + attr d2 setList state:slider,0,1,10
        + define d3 dummy
        + attr d3 webCmd state
        + attr d3 setList state:time
        +
      + Anmerkung: dies ist ein Attribut für das anzuzeigende Gerät, + nicht für die FHEMWEBInstanz. +

    • + + +
    • webCmdLabel
      + Durch Doppelpunkte getrennte Auflistung von Texten, die vor dem + jeweiligen webCmd angezeigt werden. Der Anzahl der Texte muss exakt den + Anzahl der webCmds entsprechen. Um mehrzeilige Anzeige zu realisieren, + kann ein Return nach dem Text und vor dem Doppelpunkt eingefuehrt + werden.

    • + + +
    • webname
      + Der Pfad nach http://hostname:port/ . Standard ist fhem, + so ist die Standard HTTP Adresse http://localhost:8083/fhem +

    • + + +
    • widgetOverride
      + Leerzeichen separierte Liste von Name/Modifier Paaren, mit dem man den + vom Modulautor für einen bestimmten Parameter (Set/Get/Attribut) + vorgesehene Widgets ändern kann. Folgendes ist die Liste der + bekannten Modifier: +
        + +
    • + +
    +
+ +=end html_DE + +=cut