diff --git a/fhem/FHEM/01_FHEMWEB.pm b/fhem/FHEM/01_FHEMWEB.pm
index af64b92a4..44647e6bc 100755
--- a/fhem/FHEM/01_FHEMWEB.pm
+++ b/fhem/FHEM/01_FHEMWEB.pm
@@ -11,6 +11,7 @@ use Time::HiRes qw(gettimeofday);
#########################
# Forward declaration
sub FW_IconURL($);
+sub FW_addToWritebuffer($$@);
sub FW_answerCall($);
sub FW_dev2image($;$);
sub FW_devState($$@);
@@ -156,7 +157,7 @@ FHEMWEB_Initialize($)
hiddengroup
hiddenroom
iconPath
- longpoll:0,1
+ longpoll:0,1,websocket
longpollSVG:1,0
menuEntries
mainInputLength
@@ -376,7 +377,6 @@ FW_Read($$)
} @FW_httpheader;
delete($hash->{HDR});
- $FW_userAgent = $FW_httpheader{"User-Agent"};
my @origin = grep /Origin/, @FW_httpheader;
$FW_headerlines = (AttrVal($FW_wname, "CORS", 0) ?
(($#origin<0) ? "": "Access-Control-Allow-".$origin[0]."\r\n").
@@ -422,6 +422,25 @@ FW_Read($$)
delete $hash->{CONTENT_LENGTH};
$hash->{LASTACCESS} = $now;
+ if( $method eq 'GET' && $FW_httpheader{Connection} =~ /Upgrade/i ) {
+ use Digest::SHA1 qw(sha1_base64);
+ TcpServer_WriteBlocking($FW_chash,
+ "HTTP/1.1 101 Switching Protocols\r\n" .
+ "Upgrade: websocket\r\n" .
+ "Connection: Upgrade\r\n" .
+ "Sec-WebSocket-Accept:".
+ sha1_base64($FW_httpheader{'Sec-WebSocket-Key'}.
+ "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")."=\r\n" .
+ "\r\n" );
+ $FW_chash->{websocket} = 1;
+
+ my $me = $FW_chash;
+ my ($cmd, $cmddev) = FW_digestCgi($arg);
+ FW_initInform($me, 0) if($FW_inform);
+ return -1;
+ }
+
+ $FW_userAgent = $FW_httpheader{"User-Agent"};
$arg = "" if(!defined($arg));
Log3 $FW_wname, 4, "$name $method $arg; BUFLEN:".length($hash->{BUF});
$FW_ME = "/" . AttrVal($FW_wname, "webname", "fhem");
@@ -482,7 +501,7 @@ FW_Read($$)
("Expires: ".FmtDateTimeRFC1123($now+900)."\r\n") : "");
Log3 $FW_wname, 4,
"name: $arg / RL:$length / $FW_RETTYPE / $compressed / $expires";
- if( ! addToWritebuffer($hash,
+ if( ! FW_addToWritebuffer($hash,
"HTTP/1.1 200 OK\r\n" .
"Content-Length: $length\r\n" .
$expires . $compressed . $FW_headerlines .
@@ -496,6 +515,77 @@ FW_Read($$)
}
}
+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 : ".*");
+ }
+ my $filter = $me->{inform}{filter};
+ $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+$/);
+
+ if($longpoll) {
+ my $sinceTimestamp = FmtDateTime($me->{inform}{since});
+ 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));
+ }
+
+ 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(0xFF) . chr($len) . $txt;
+ } else {
+ $txt = chr(0x81) . chr(0x7E) . pack( 'n', $len ) . $txt;
+ }
+ }
+ return addToWritebuffer($hash, $txt, $callback, $nolimit);
+}
+
sub
FW_AsyncOutput($$)
{
@@ -524,7 +614,7 @@ FW_AsyncOutput($$)
next if( $chash->{TYPE} ne 'FHEMWEB' );
next if( !$chash->{inform} );
next if( !$chash->{FW_ID} || $chash->{FW_ID} ne $hash->{FW_ID} );
- addToWritebuffer($chash, $data."\n");
+ FW_addToWritebuffer($chash, $data."\n");
$found = 1;
last;
}
@@ -670,51 +760,7 @@ FW_answerCall($)
}
if($FW_inform) { # Longpoll header
- 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 : ".*");
- }
- my $filter = $me->{inform}{filter};
- $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});
- 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));
-
- if($FW_id && $defs{$FW_wname}{asyncOutput}) {
- my $data = $defs{$FW_wname}{asyncOutput}{$FW_id};
- if($data) {
- 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}});
- }
-
+ FW_initInform($me, 1);
return -1;
}
@@ -871,7 +917,7 @@ FW_answerCall($)
my $csrf= ($FW_CSRF ? "fwcsrf='$defs{$FW_wname}{CSRFTOKEN}'" : "");
my $gen = 'generated="'.(time()-1).'"';
- my $lp = 'longpoll="'.AttrVal($FW_wname,"longpoll",1).'"';
+ my $lp = 'longpoll="'.AttrVal($FW_wname,"longpoll",1).'"';
$FW_id = $FW_chash->{NR} if( !$FW_id );
FW_pO "\n
";
@@ -2575,7 +2621,7 @@ FW_logInform($$)
return;
}
$msg = FW_htmlEscape($msg);
- if(!addToWritebuffer($ntfy, "$msg
") ){
+ if(!FW_addToWritebuffer($ntfy, "$msg
") ){
TcpServer_Close($ntfy);
delete $logInform{$me};
delete $defs{$me};
@@ -2596,7 +2642,7 @@ FW_Notify($$)
my $vs = int(@structChangeHist) ? 'visible' : 'hidden';
my $data = FW_longpollInfo($h->{fmt},
"#FHEMWEB:$ntfy->{NAME}","\$('#saveCheck').css('visibility','$vs')","");
- addToWritebuffer($ntfy, $data."\n");
+ FW_addToWritebuffer($ntfy, $data."\n");
if($dev->{CHANGED}) {
$dn = $1 if($dev->{CHANGED}->[0] =~ m/^MODIFIED (.*)$/);
@@ -2614,7 +2660,7 @@ FW_Notify($$)
my $data = $3;
return if( $2 && $ntfy->{PEER} !~ m/$2/ );
$data = FW_longpollInfo($h->{fmt}, "#FHEMWEB:$ntfy->{NAME}",$data,"");
- addToWritebuffer($ntfy, $data."\n");
+ FW_addToWritebuffer($ntfy, $data."\n");
return;
}
@@ -2687,7 +2733,8 @@ FW_Notify($$)
}
if(@data){
- if(!addToWritebuffer($ntfy, join("\n", map { s/\n/ /gm; $_ } @data)."\n") ){
+ 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);
@@ -2714,7 +2761,7 @@ FW_directNotify($@) # Notify without the event overhead (Forum #31293)
!$ntfy->{inform}{devices}{$dev} ||
$ntfy->{inform}{type} ne "status");
next if($filter && $ntfy->{inform}{filter} !~ m/$filter/);
- if(!addToWritebuffer($ntfy,
+ 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";
diff --git a/fhem/www/pgm2/console.js b/fhem/www/pgm2/console.js
index 8c450a14f..86f40ebde 100644
--- a/fhem/www/pgm2/console.js
+++ b/fhem/www/pgm2/console.js
@@ -8,24 +8,39 @@ var mustScroll = 1;
log("Console is opening");
function
-consUpdate()
+consUpdate(evt)
{
- if(consConn.readyState == 4) {
- FW_errmsg("Connection lost, trying a reconnect every 5 seconds.");
- setTimeout(consFill, 5000);
- return; // some problem connecting
- }
+ var errstr = "Connection lost, trying a reconnect every 5 seconds.";
+ var new_content = "";
- if(consConn.readyState != 3)
- return;
- var len = consConn.responseText.length;
-
- if (consLastIndex == len) // No new data
- return;
-
- var new_content = consConn.responseText.substring(consLastIndex, len);
- consLastIndex = len;
-
+ if(evt.target instanceof WebSocket) {
+ if(evt.type == 'close') {
+ FW_errmsg(errstr, 4900);
+ consConn.close();
+ consConn = undefined;
+ setTimeout(consFill, 5000);
+ return;
+ }
+ new_content = evt.data;
+ consLastIndex = 0;
+
+ } else {
+ if(consConn.readyState == 4) {
+ FW_errmsg(errstr, 4900);
+ setTimeout(consFill, 5000);
+ return;
+ }
+
+ if(consConn.readyState != 3)
+ return;
+
+ var len = consConn.responseText.length;
+ if (consLastIndex == len) // No new data
+ return;
+
+ new_content = consConn.responseText.substring(consLastIndex, len);
+ consLastIndex = len;
+ }
log("Console Rcvd: "+new_content);
if(new_content.indexOf('<') != 0)
new_content = new_content.replace(/ /g, " ");
@@ -41,18 +56,41 @@ consFill()
{
FW_errmsg("");
- if(consConn) {
- consConn.onreadystatechange = undefined;
- consConn.abort();
+ if(FW_pollConn) {
+ if($("body").attr("longpoll") == "websocket") {
+ FW_pollConn.close();
+ } else {
+ FW_pollConn.abort();
+ }
+ FW_pollConn = undefined;
}
- consConn = new XMLHttpRequest();
- var query = document.location.pathname+"?XHR=1"+
+
+ var query = "?XHR=1"+
"&inform=type=raw;withLog="+withLog+";filter="+consFilter+
"×tamp="+new Date().getTime();
query = addcsrf(query);
- consConn.open("GET", query, true);
- consConn.onreadystatechange = consUpdate;
- consConn.send(null);
+
+ if($("body").attr("longpoll") == "websocket") {
+ if(consConn) {
+ consConn.close();
+ }
+ consConn = new WebSocket((location+query).replace(/^http/i, "ws"));
+ consConn.onclose =
+ consConn.onerror =
+ consConn.onmessage = consUpdate;
+
+ } else {
+ if(consConn) {
+ consConn.onreadystatechange = undefined;
+ consConn.abort();
+ }
+ consConn = new XMLHttpRequest();
+ consConn.open("GET", location.pathname+query, true);
+ consConn.onreadystatechange = consUpdate;
+ consConn.send(null);
+
+ }
+
consLastIndex = 0;
if(oldFilter != consFilter) // only clear, when filter changes
$("#console").html("");
diff --git a/fhem/www/pgm2/fhemweb.js b/fhem/www/pgm2/fhemweb.js
index 13dbba405..f215498fd 100644
--- a/fhem/www/pgm2/fhemweb.js
+++ b/fhem/www/pgm2/fhemweb.js
@@ -670,24 +670,41 @@ var FW_longpollOffset = 0;
var FW_leaving;
function
-FW_doUpdate()
+FW_doUpdate(evt)
{
- if(FW_pollConn.readyState == 4 && !FW_leaving) {
- if(FW_pollConn.status == "401") {
- location.reload();
+ var errstr = "Connection lost, trying a reconnect every 5 seconds.";
+ var input="";
+
+ if(evt.target instanceof WebSocket) {
+ if(evt.type == 'close') {
+ FW_errmsg(errstr, 4900);
+ FW_pollConn.close();
+ FW_pollConn = undefined;
+ setTimeout(FW_longpoll, 5000);
return;
}
- FW_errmsg("Connection lost, trying a reconnect every 5 seconds.", 4900);
- setTimeout(FW_longpoll, 5000);
- return; // some problem connecting
+ input = evt.data;
+ FW_longpollOffset = 0;
+
+ } else {
+ if(FW_pollConn.readyState == 4 && !FW_leaving) {
+ if(FW_pollConn.status == "401") {
+ location.reload();
+ return;
+ }
+ FW_errmsg(errstr, 4900);
+ setTimeout(FW_longpoll, 5000);
+ return;
+ }
+
+ if(FW_pollConn.readyState != 3)
+ return;
+
+ input = FW_pollConn.responseText;
}
- if(FW_pollConn.readyState != 3)
- return;
-
- var input = FW_pollConn.responseText;
var devs = new Array();
- if(input.length <= FW_longpollOffset)
+ if(!input || input.length <= FW_longpollOffset)
return;
FW_serverLastMsg = (new Date()).getTime()/1000;
@@ -757,7 +774,6 @@ FW_longpoll()
FW_pollConn.abort();
}
- FW_pollConn = new XMLHttpRequest();
FW_leaving = 0;
// Build the notify filter for the backend
@@ -809,14 +825,25 @@ FW_longpoll()
if(FW_serverGenerated)
since = FW_serverLastMsg + (FW_serverGenerated-FW_serverFirstMsg);
- var query = location.pathname+"?XHR=1"+
+ var query = "?XHR=1"+
"&inform=type=status;filter="+filter+";since="+since+";fmt=JSON"+
'&fw_id='+$("body").attr('fw_id')+
"×tamp="+new Date().getTime();
query = addcsrf(query);
- FW_pollConn.open("GET", query, true);
- FW_pollConn.onreadystatechange = FW_doUpdate;
- FW_pollConn.send(null);
+
+ if($("body").attr("longpoll") == "websocket") {
+ FW_pollConn = new WebSocket((location+query).replace(/^http/i, "ws"));
+ FW_pollConn.onclose =
+ FW_pollConn.onerror =
+ FW_pollConn.onmessage = FW_doUpdate;
+
+ } else {
+ FW_pollConn = new XMLHttpRequest();
+ FW_pollConn.open("GET", location.pathname+query, true);
+ FW_pollConn.onreadystatechange = FW_doUpdate;
+ FW_pollConn.send(null);
+
+ }
log("Longpoll with filter "+filter);
}