I'm slowly migrating off of a virtual server at a service provider. Most things have been moving to docker. In this case, it's going onto the docker server, but not going to run in docker itself.
It has to be on the same network as the Weatherflow device to be able to pick up data, and me keeping it on the unrouteable network meant this docker platform needed to be on two separate networks. So, I fired off the command to create a new connection on the same interface.
Unfortunately, this resulted in the system not being able to boot onto the network. So, it was backed out, and I moved the weather hub off of the unrouteable to the regular network. The code below, I simply could NOT get to arrive in any docker image, or running directly on the docker server (outside of a container). However, after a lot of troubleshooting to verify that the broadcast packets were getting there using :
sudo tcpdump -i enp1s0 udp port 50222 -Xvvv
I finally realized that RedHat had firewall-cmd running by default. Now, I'm trying to make all this work with defaults, which means that instead of disabling it, I'm going to open the port :
firewall-cmd --permanent --add-port=50222/udp
firewall-cmd --reload
firewall-cmd --list-all
You should see the ports: line now containing "50222/udp". And, if you run the server, it should show it's receiving packets. Okay, back to building this "listener". I went down this whole path of making it run an exec() to curl, which made it really easy to send the received JSON to a perl script and customize whatever you needed from there. Yes, at the very bottom of the code I have commented out a "sendJSON" function, and instead I'm using a handleJSON function that will do the parsing, so if you need to flip that switch because you are using a non-docker version of HTTPd and Perl, you can install the package and make it happen.
Since the Perl instances in the Apache/HTTPd 2.4 instances ("httpd 2.4") do NOT include either the Perl JSON module, or the DBD::MySQL module, it ultimately means you can't send this to Apache and deal with it nicely there if you are also using native docker. That leaves one choice - write a UDP "listener" (duh, UDP packages don't "listen" on UDP ports, because there is no connection/handshake).
So, I wrote the dumb thing in perl. I'd already had the UDP side, but since it might as well do the local database insertion, whelp, that's what I did.
Fancy thing is that I can specify which datagram types to accept from the Tempest system. I really only care about lightning strikes and obs_st (standard metrics). But, I slapped SQL for all, and did a fancy ".disabled" extension to hide the ones I wanted. If I don't have a disabled, when it sees a packet, it will dump the hash_ref so that I can easily write one if I need it. I just wrote what I saw, then disabled most of them.
Here's the code for the listener :
#!/usr/bin/perl -w
# written by Joe Lewis <joe@joe-lewis.com> on November 29, 2022
# A UDP listener for the tempest weather system. The API for weatherflow (maker
# of the tempest system) can be found at :
# https://weatherflow.github.io/Tempest/api/udp.html
# if on RHEL, install :
# perl-libwww-perl
# perl-Sys-Syslog.x86_64
# for RHEL, this uses the template tool kit (available in the EPEL repository)
# sudo subscription-manager repos --enable codeready-builder-for-rhel-9-x86_64-rpms
# sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
# sudo yum install perl-Template-Toolkit.x86_64
use strict;
use warnings;
use Data::Dumper;
use DBI;
use Getopt::Long;
use HTTP::Request;
use HTTP::Response;
use IO::Socket;
use JSON;
use LWP::UserAgent;
use POSIX;
use Sys::Syslog qw(:DEFAULT :standard :macros);
use Template;
my $template_directory = 'sql';
my $mysql_hostname = '127.0.0.1'; # if you use "localhost", it will try the unix socket
my $mysql_username = 'user_only_having_insert_or_update';
my $mysql_password = 'users_password';
my $mysql_database = 'weather';
my $verbose = undef;
my $daemonize = undef;
GetOptions(
'sql-directory=s' => \$template_directory,
'daemonize' => \$daemonize,
'verbose' => \$verbose,
);
my($sock, $newmsg, $MAXLEN, $PORTNO);
my $json = JSON->new->allow_nonref;
my $userAgent = LWP::UserAgent->new(timeout => 10);
my $postURL = 'https://protected.silverhawk.net/cgi-bin/weatherflow.pl';
my $postContentType = 'application/json';
my $name = 'WeatherFlow-UDP-conv';
my $nagios_command_file = '/var/lib/nagios3/rw/nagios.cmd';
my $error_log_qfn = '/var/log/'.$name.'.log';
my $pid_file_qfn = '/var/run/'.$name.'.pid';
$MAXLEN = 1024;
$PORTNO = 50222;
sub sighup_handler {
exit;
};
sub sigusr1_handler {
exit;
};
sub logMessage {
my $msg = shift;
$msg .= "\n" unless $msg =~ /\n/;
syslog('info',$msg);
};
sub handleUDPPacket {
my $weatherstationname = shift;
my $weatherstationclient = shift;
my $data = shift;
my $href = $json->decode($data);
my $weatherstationip = inet_ntoa($weatherstationclient);
# just for my sanity, insert the hostname
$href->{'client_hostname'} = $weatherstationname;
$href->{'client_ip'} = $weatherstationip;
my $sqlTemplate = $template_directory.'/'.$href->{type}.'.sql';
if (-f $sqlTemplate) {
my $tt = Template->new({INCLUDE_PATH=>$template_directory,INTERPOLATE => 1}) or die "Error launching the template toolkit : $Template::ERROR\n";
# establish a MySQL connection
my $dbh = DBI->connect('dbi:mysql:host='.$mysql_hostname.':database='.$mysql_database.':mysql_ssl=1',$mysql_username,$mysql_password);
$href->{dbh} = $dbh;
my $SQL;
$tt->process($href->{type}.'.sql',$href,\$SQL) or warn 'Template Processing Failure: '.$tt->error()."\n";
if ($SQL) {
$dbh->do($SQL) or warn "Error running SQL on type $href->{type}: $DBI::errstr, SQL :\n\t$SQL\n";
}
$dbh->disconnect();
} else {
# if we have a disabled SQL file, do nothing because we're intentionally swallowing it.
if (!-f $sqlTemplate.'.disabled') {
# no template defined, warn it
warn "non-existant SQL template $sqlTemplate containing:\n".Dumper($href);
}
};
};
openlog("",'ndelay','daemon');
if (defined($daemonize)) {
my $pid = fork();
exit if ($pid > 0);
setsid();
$pid = fork();
exit if ($pid > 0);
};
$SIG{HUP} = \&sighup_handler;
$SIG{USR1} = \&sigusr1_handler;
$sock = IO::Socket::INET->new(
LocalPort => $PORTNO,
Proto => 'udp') or die "socket: $@";
logMessage("Awaiting UDP messages on port $PORTNO");
while ($sock->recv($newmsg, $MAXLEN)) {
my($clientport, $clientaddr) = sockaddr_in($sock->peername);
my $clienthost = gethostbyaddr($clientaddr, AF_INET);
handleUDPPacket($clienthost,$clientaddr,$newmsg);
}
my $status = $!;
logMessage("Loop completed, exiting with $status");
die "recv: $status";
Now, that's relatively simple. I created the separate directory for the SQL's specific to the Tempest functionality, and then piled a bunch of files in there :
*** sql/device_status.sql.disabled
INSERT INTO device_status (timestamp,serial_number,client_ip,client_hostname,hub_sn,voltage,rssi,hub_rssi,sensor_status,firmware_revision) VALUES (from_unixtime([% timestamp %]),[% dbh.quote(serial_number) %],[% dbh.quote(client_ip) %],[% dbh.quote(client_hostname) %],[% dbh.quote(hub_sn) %],[% voltage %],[% rssi %],[% hub_rssi %],[% sensor_status %],[% firmware_revision %])
*** sql/evt_precip.sql.disabled
INSERT INTO evt_precip (timestamp,station_id,hub_sn) VALUES (from_unixtime([% evt.0 %]),[% dbh.quote(serial_number) %],[% dbh.quote(hub_sn) %)
*** sql/evt_strike.sql
INSERT INTO lightning_events (epoch,station_id,energy,distance_km,distance_miles) VALUES (from_unixtime([% evt.0 %]),[% dbh.quote(serial_number) %],[% evt.2 %],[% evt.1 %],[% evt.1/1.6093445 %])
*** sql/hub_status.sql.disabled
INSERT INTO hub_status (timestamp,serial_number,frequency,rssi,client_ip,uptime,firmware_revision,hw_version,client_hostname,radio_version,reboot_count,i2c_bus_error_count,radio_status,radio_network_id) VALUES (from_unixtime([% timestamp %]),[% dbh.quote(serial_number) %],[% freq %],[% rssi %],[% dbh.quote(client_ip) %],[% uptime %],[% dbh.quote(firmare_revision) %],[% hw_version %],[% dbh.quote(client_hostname) %],[% radio_stats.0 %],[% radio_stats.1 %],[% radio_stats.2 %],[% radio_stats.3 %],[% radio_stats.4 %])
*** sql/obs_air.sql.disabled
INSERT INTO obs_air (timestamp,serial_number,firmware_revision,hub_sn,station_pressure_mb,station_pressure_inhg,temperature_celcius,temperature_farenheit,rel_humidity,lightning_strike_count,lightning_strike_avg_distance_km,lightning_strike_avg_distance_miles,battery_voltage,report_interval) VALUES (from_unixtime([% obs.0.0 %]),[% dbh.quote(serial_number) %],[% firmware_revision %],[% dbh.quote(hub_sn) %],[% obs.0.1 %],([% obs.0.1 %]*0.029530)+4.7634625,[% obs.0.2 %],([% obs.0.2 %]*1.8)+32,[% obs.0.3 %],[% obs.0.4 %],[% obs.0.5 %],[% obs.0.5 %]/0.621371,[% obs.0.6],[% obs.0.7 %])
*** sql/obs_sky.sql.disabled
INSERT INTO obs_sky (timestamp,serial_number,firmware_revision,hub_sn,illuminance_lux,uv_index,rain_previous_minute_mm,rain_previous_minute_in,wind_lull_mps,wind_lull_mph,wind_avg_mps,wind_avg_mph,wind_gust_mps,wind_gust_mph,wind_direction,battery_voltage,report_interval,solar_radiation,day_rain_accumulation_mm,precipitation_type,wind_sample_interval) VALUES (from_unixtime([% obs.0.0 %]),[% dbh.quote(serial_number) %],[% firmware_revision %],[% dbh.quote(hub_sn) %],[% obs.0.1 %],[% obs.0.2 %],[% obs.0.3 %],[% obs.0.3 %]/25.4,[% obs.0.4 %],[% obs.0.4 %]*2.23694,[% obs.0.5 %],[% obs.0.5 %]*2.23694,[% obs.0.6 %],[% obs.0.6 %]*2.23694,[% obs.0.7 %],[% obs.0.8 %],[% obs.0.9 %],[% obs.0.10 %],[% obs.0.11 %],[% obs.0.12 %],[% obs.0.13 %])
*** sql/obs_st.sql
INSERT INTO twenty_four_hour (epoch,station_id,wind_lull_metric,wind_lull_standard,wind_avg_metric,wind_avg_standard,wind_gust_metric,wind_gust_standard,wind_direction,wind_sample_interval,barometric_pressure_milibars,barometric_pressure_inHg,temperature_celcius,temperature_farenheit,relative_humidity,illuminance,ultraviolet_index,solar_radiation,rain_amount_metric,rain_amount_standard,precipitation_type,lightning_avg_distance_metric,lightning_avg_distance_standard,lightning_strike_count,battery_voltage,report_interval) VALUES (from_unixtime([% obs.0.0 %]),[% dbh.quote(serial_number) %],[% obs.0.1 %],[% obs.0.1 %]*2.23694,[% obs.0.2 %],[% obs.0.2 %]*2.23694,[% obs.0.3 %],[% obs.0.3 %]*2.23694,[% obs.0.4 %],[% obs.0.5 %],[% obs.0.6 %],([% obs.0.6 %]*0.029530)+4.7634625,[% obs.0.7 %],([% obs.0.7 %]*1.8)+32,[% obs.0.8 %],[% obs.0.9 %],[% obs.0.10 %],[% obs.0.11 %],[% obs.0.12 %],[% obs.0.12 %]/25.4,[% obs.0.13 %],[% obs.0.14 %],[% obs.0.14 %]/0.621371,[% obs.0.15 %],[% obs.0.16 %],[% obs.0.17 %])
*** sql/obs_st.sql.original
INSERT INTO obs_st (timestamp,serial_number,firmware_revision,hub_sn,wind_lull_mps,wind_lull_mph,wind_avg_mps,wind_avg_mph,wind_gust_mps,wind_gust_mph,wind_direction,wind_sample_interval,station_pressure_mb,station_pressure_inhg,temperature_c,temperature_f,relative_humidity,illuminance_lux,uv,solar_radiation,rain_previous_minute_mm,rain_previous_minute_in,precip_type,lightning_strike_avg_distance_km,lightning_avg_distance_miles,lightning_strike_count,battery,report_interval) VALUES (from_unixtime([% obs.0.0 %]),[% dbh.quote(serial_number) %],[% firmware_revision %],[% dbh.quote(hub_sn) %],[% obs.0.1 %],[% obs.0.1 %]*2.23694,[% obs.0.2 %],[% obs.0.2 %]*2.23694,[% obs.0.3 %],[% obs.0.3 %]*2.23694,[% obs.0.4 %],[% obs.0.5 %],[% obs.0.6 %],([% obs.0.6 %]*0.029530)+4.7634625,[% obs.0.7 %],([% obs.0.7 %]*1.8)+32,[% obs.0.8 %],[% obs.0.9 %],[% obs.0.10 %],[% obs.0.11 %],[% obs.0.12 %],[% obs.0.12 %]/25.4,[% obs.0.13 %],[% obs.0.14 %],[% obs.0.14 %]/0.621371,[% obs.0.15 %],[% obs.0.16 %],[% obs.0.17 %])
*** sql/rapid_wind.sql.disabled
INSERT INTO rapid_wind (epoch,station_id,speed_mps,speed_mph,direction) VALUES (from_unixtime([% ob.0 %]),[% dbh.quote(serial_number) %],[% ob.1 %],[% ob.1 %]*2.236936,[% ob.2 %])
And to ensure it starts on boot (this is outside of docker - meh, I was so frustrated that I'm fine with it), I created an /etc/systemd/system/weatherflow_udp.service file containing :
[Unit]
Description=UDP Listener for Tempest Weatherflow
After=default.target
[Service]
Type=oneshot
ExecStart=/where/ever/you/stored/it/tempest/perl/udp_listener.pl --daemonize --sql-directory /where/ever/you/stored/it/tempest/perl/sql/
[Install]
WantedBy=default.target
And then enabled the service :
systemctl daemon-reload
systemctl enable weatherflow_udp
Viola (or Cello, I don't care), I have data!
One step closer!