A more efficient way to get JSON records is by connecting to any of the several undocumented, unsupported websock sockets. Here's a little Perl script that does exactly that. You run it like this:
$ ./websocket.pl ws://hubitat.local/eventsocket >>$LOGFILE &
Since Perl does output buffering when writing to a non-terminal, you may have to wait a while to see any output in the output file. You can send the script a SIGUSR1 to flush output, or you can run it to the terminal and watch the events roll by.
This script understands the following undocumented HE websock socket record formats:
- ws://hubitat.local/eventsocket
- ws://hubitat.local/logsocket
- ws://hubitat.local/zwaveLogsocket
- ws://hubitat.local/zigbeeLogsocket
- ...most anything else...
It prepends the current date (GMT seconds since the epoch) to all records read. For eventsocket records beginning with source:
, it also wraps the JSON with a content:{} outer layer to match what Maker API produces.
#!/usr/bin/perl -w
# $0 ws://path...
# Listen on a websocket URL, e.g. ws://hubitat.local/eventsocket
# Touch up and then dump the records read to STDOUT.
# - Add the current time in UTC seconds since epoch (for gnuplot)
# - Wrap the JSON record with ("content": ... } to match Maker API JSON
# Traps SIGHUP SIGINT SIGTERM and flushes buffers before exit.
# Traps SIGUSR1 and flushes buffers and does NOT exit.
#
# Modeled after: Simple WebSocket test client using blocking I/O
# Greg Kennedy 2019
# Originally from https://metacpan.org/pod/AnyEvent::WebSocket::Client
# Removed all reading of stdin; this version is listen only.
# -Ian! D. Allen - idallen@idallen.ca - www.idallen.com
use v5.014;
use warnings;
use JSON;
# Core IO::Socket::INET for creating sockets to the Internet
# Protocol handler for WebSocket HTTP protocol
# Time conversion to create UTC seconds since epoch
#
use IO::Socket::INET;
use Protocol::WebSocket::Client;
use Time::Local qw( timegm_nocheck );
die "Usage: $0 ws://URL\n" unless @ARGV == 1;
my $url = $ARGV[0];
# Protocol::WebSocket takes a full URL, but IO::Socket::* uses only a host
# and port. This regex section retrieves host/port from URL.
my ($proto, $host, $port, $path);
if ($url =~ m/^(?:(?<proto>ws|wss):\/\/)?(?<host>[^\/:]+)(?::(?<port>\d+))?(?<path>\/.*)?$/)
{
$host = $+{host};
$path = $+{path};
if (defined $+{proto} && defined $+{port}) {
$proto = $+{proto};
$port = $+{port};
} elsif (defined $+{port}) {
$port = $+{port};
if ($port == 443) { $proto = 'wss' } else { $proto = 'ws' }
} elsif (defined $+{proto}) {
$proto = $+{proto};
if ($proto eq 'wss') { $port = 443 } else { $port = 80 }
} else {
$proto = 'ws';
$port = 80;
}
} else {
die "$0: Failed to parse Host/Port from URL: $url\n";
}
warn "$0: Opening blocking INET socket to $proto://$host:$port\n"
if -t STDERR;
# Create a basic TCP socket connected to the remote server.
my $tcp_socket = IO::Socket::INET->new(
PeerAddr => $host,
PeerPort => "$proto($port)",
Proto => 'tcp',
Blocking => 1
) or die "$0: Failed to connect to socket for host '$host' port '$port':\n$@\n";
# Create a websocket protocol handler.
# This doesn't actually "do" anything with the socket:
# It just encodes / decode WebSocket messages. We have to send them ourselves.
warn "$0: Creating Protocol::WebSocket::Client handler\n"
if -t STDERR;
my $client = Protocol::WebSocket::Client->new(url => $url);
# Set up the various methods for the WS Protocol handler
# On Write: take the buffer (WebSocket packet) and send it on the socket.
$client->on(
write => sub {
my $client = shift;
my ($buf) = @_;
syswrite $tcp_socket, $buf;
}
);
# On Connect: this is what happens after the handshake succeeds, and we
# are "connected" to the service.
$client->on(
connect => sub {
my $client = shift;
warn "$0: Successfully connected to $url\n"
if -t STDERR;
}
);
# On Error, print to console. This can happen if the handshake
# fails for whatever reason.
$client->on(
error => sub {
my $client = shift;
my ($buf) = @_;
warn "$0: Error on websocket: $buf\n";
$tcp_socket->close;
exit(1);
}
);
# On Read: Called whenever a complete WebSocket "frame" successfully parsed.
$client->on(
read => sub {
my $client = shift;
local ($_) = @_;
# Something is wrong with the charset of these records. Fix them:
if ( s/([^\xc2])\xb0/$1\xc2\xb0/g ) {
# warn "DEBUG Updated Latin-1 in $_\n"
# if -t STDERR;
}
if ( s/\\u00B0/\xc2\xb0/g ) {
# warn "DEBUG Updated odd escape in $_\n"
# if -t STDERR;
}
# Add the current epoch "time" to the Hubitat JSON record.
# Convert the time to UTC for gnuplot.
# We accept /eventsocket, /logsocket, /zwaveLogsocket, /zigbeeLogsocket
my $time = timegm_nocheck(localtime());
# Wrap an /eventsocket record with ("content": ... } to match Maker API
if ( s/^\s*\{\s*"source"\s*:/{"content":{"date":"$time","source":/ ) {
# This is /eventsocket format, make same as Maker API JSON format
print $_, "}\n";
return;
}
# Get here for all other JSON formats
if ( s/^\s*\{\s*/{"date":"$time",/ ) {
print $_, "\n";
return;
}
warn "$0: Unable to parse: $_\n";
}
);
# Signals caught to allow exit with buffers flushing
$SIG{'HUP'} = $SIG{'INT'} = $SIG{'TERM'} = sub {
# Sends correct close header
$client->disconnect;
die "$0: Caught signal @_ $!\n";
};
# SIGUSR1 used to flush buffers without exiting
$SIG{'USR1'} = sub {
$| = 1; # flush the output
$| = 0;
};
# Now that we've set all that up, call connect on $client.
# This causes the Protocol object to create a handshake and write it
# (using the on_write method we specified - which includes sysread $tcp_socket)
warn "$0: Connecting to client\n"
if -t STDERR;
$client->connect;
# Now, we go into a loop, calling sysread and passing results to client->read.
# The client Protocol object knows what to do with the data, and will
# call our hooks (on_connect, on_read, on_read, on_read...) accordingly.
while ($tcp_socket->connected) {
# await response
my $recv_data;
my $bytes_read = sysread $tcp_socket, $recv_data, 16384;
if (!defined $bytes_read) {
next if $! =~ /interrupted/i; # because of SIGUSR1 handler
warn "$0: sysread on tcp_socket failed: $!\n";
last;
}
if ($bytes_read == 0) {
warn "$0: Connection terminated (EOF).\n";
last;
}
# unpack response - this triggers any handler if a complete packet is read.
$client->read($recv_data);
}
# Sends correct close header
$client->disconnect;