############################################## # $Id$ 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_confFiles($); 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 $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_extraRooms; # hash of extra 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 extraRooms:textField-long 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" ); $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; } } $cmds{show} = { Fn=>"FW_show", ClientFilter=>"FHEMWEB", Hlp=>", show temporary room with devices from " }; } ##################################### 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 RemoveInternalTimer(0, "FW_closeInactiveClients"); InternalTimer(time()+60, "FW_closeInactiveClients", 0, 0); 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 # https://tools.ietf.org/html/rfc6455 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; # $op: 0=>Continuation, 1=>Text, 2=>Binary, 8=>Close, 9=>Ping, 10=>Pong if($op == 8) { TcpServer_Close($hash, 1); return; } elsif($op == 9) { return addToWritebuffer($hash, chr(0x8A).chr(0)); # Pong } 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; } my @m; if($mask) { @m = unpack("C*", substr($hash->{BUF},$i,4)); $i += 4; } return if(length($hash->{BUF}) < $i+$len); my $data = substr($hash->{BUF}, $i, $len); if($mask) { my $idx = 0; $data = pack("C*", map { $_ ^ $m[$idx++ % 4] } unpack("C*", $data)); } $hash->{BUF} = ""; my $ret = FW_fC($data); FW_addToWritebuffer($hash, FW_longpollInfo("JSON", defined($ret) ? $ret : "")."\n"); 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) = @_; # Forum #41125, 88470 if(!$hash->{inform} && !$hash->{BUF} && !defined($hash->{".WRITEBUFFER"})) { 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 (devspec2array(".*", $FW_chash)) { 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_extraRooms = (); if(my $extra = AttrVal($FW_wname, "extraRooms", undef)) { foreach my $room (split(/ |\n/, $extra)) { next if(!$room || $room =~ /^#/); $room =~ m/name=([^:]+):devspec=([^\s]+)/; my $r = $1; my $d = "#devspec=$2"; $FW_rooms{$r}{$d} = 1; $FW_extraRooms{$r} = $d; } } $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; if( $FW_room =~ m/^#devspec=(.*)$/ ) { @devs = devspec2array($1) if( $1 ); @devs = () if( int(@devs) == 1 && !defined($defs{$devs[0]}) ); } else { @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)= @_; return if(!@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 = FW_confFiles(2); 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($) { my ($param) = @_; # create and return regexp for editFileList return "(".join ( "|" , sort keys %{$data{confFiles}} ).")" if $param == 1; # create and return array with filenames return sort keys %{$data{confFiles}} if $param == 2; } ################## # 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 for external programs:\$FW_confdir:^".FW_confFiles(1)."\$\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" && $param[0]) { 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 "extraRooms") { foreach my $room (split(/ |\n/, $param[0])) { next if(!$room || $room =~ /^#/); return "Bad extraRooms entry $room, not name=:devspec=" if($room !~ m/name=([^:]+):devspec=([^\s]+)/); } } 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 # return ($allSets, $cmdList, $txt); sub FW_devState($$@) { my ($d, $rf, $extPage) = @_; my ($hasOnOff, $link); return ("","","") if(!$FW_wname); 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 $html; foreach my $state (split("\n", $state)) { $txt = $state; my ($icon, $isHtml); ($icon, $link, $isHtml) = FW_dev2image($d,$state); $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/); } $html .= ' ' if( $html ); $html .= $txt; } $txt = $html; } 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; } sub FW_show($$) { my ($hash, $param) = @_; return "usage: show " if( !$param); $FW_room = "#devspec=$param"; return undef; } 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>'}
    • Note: The above is valid for each line of STATE. If STATE (through stateFormat) is multilined, multiple icons (one per line) will be created.

    • 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).

    • extraRooms
      Space or newline separated list of dynamic rooms to add to the room list.
      Example:
      attr WEB extraRooms name=open:devspec=contact=open.* name=closed:devspec=contact=closed.*

    • 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>'}
      Anmerkung: Obiges gilt pro STATE Zeile. Wenn STATE (durch stateFormat) mehrzeilig ist, wird pro Zeile ein Icon erzeugt.

    • 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.

    • extraRooms
      Durch Leerzeichen oder Zeilenumbruch getrennte Liste von dynamischen Räumen, die zusätzlich angezeigt werden sollen. Beispiel:
      attr WEB extraRooms name=Offen:devspec=contact=open.* name=Geschlossen:devspec=contact=closed.*

    • 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