9 use Mojo::IOLoop::Stream;
10 use Mojo::Transaction::WebSocket;
11 #use Mojo::JSON qw(decode_json encode_json);
15 use Math::Round qw(nearest);
17 use Data::Random qw(rand_chars);
20 use constant pi => 3.14159265358979;
22 my $devname = "/dev/davis";
23 my $datafn = ".loop_data";
26 my $poll_interval = 2.5;
27 my $rain_mult = 0.2; # 0.1 or 0.2 mm or 0.01 inches
35 my $ser; # the serial port Mojo::IOLoop::Stream
39 our $json = JSON->new->canonical(1);
40 our $WS = {}; # websocket connections
43 our @last10minsr = ();
45 our $windmins = 2; # no of minutes of wind data for the windrose
46 our $histdays = 5; # no of days of (half)hour data to search for main graph
47 our $updatepermin = 60 / 2.5; # no of updates per minute
49 our $loop_count; # how many LOOPs we have done, used as start indicator
52 0x0, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
53 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
54 0x1231, 0x210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
55 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
56 0x2462, 0x3443, 0x420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
57 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
58 0x3653, 0x2672, 0x1611, 0x630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
59 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
60 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x840, 0x1861, 0x2802, 0x3823,
61 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
62 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0xa50, 0x3a33, 0x2a12,
63 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
64 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0xc60, 0x1c41,
65 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
66 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0xe70,
67 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
68 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
69 0x1080, 0xa1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
70 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
71 0x2b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
72 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
73 0x34e2, 0x24c3, 0x14a0, 0x481, 0x7466, 0x6447, 0x5424, 0x4405,
74 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
75 0x26d3, 0x36f2, 0x691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
76 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
77 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x8e1, 0x3882, 0x28a3,
78 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
79 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0xaf1, 0x1ad0, 0x2ab3, 0x3a92,
80 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
81 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0xcc1,
82 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
83 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0xed1, 0x1ef0
88 $bar_trend{-60} = "Falling Rapidly";
89 $bar_trend{196} = "Falling Rapidly";
90 $bar_trend{-20} = "Falling Slowly";
91 $bar_trend{236} = "Falling Slowly";
92 $bar_trend{0} = "Steady";
93 $bar_trend{20} = "Rising Slowly";
94 $bar_trend{60} = "Rising Rapidly";
98 $SIG{TERM} = $SIG{INT} = sub {++$ending; Mojo::IOLoop->stop;};
102 # WebSocket weather service
103 websocket '/weather' => sub {
109 app->log->debug('WebSocket opened.');
110 dbg 'WebSocket opened' if isdbg 'chan';
113 # send historical data
114 $c->send($ld->{lasthour_h}) if exists $ld->{lasthour_h};
115 $c->send($ld->{lastmin_h}) if exists $ld->{lastmin_h};
118 $c->inactivity_timeout(3615);
124 dbg "websocket: text $msg" if isdbg 'chan';
128 dbg "websocket: json $msg" if isdbg 'chan';
133 $c->on(finish => sub {
134 my ($c, $code, $reason) = @_;
135 app->log->debug("WebSocket closed with status $code.");
136 dbg "websocket closed with status $code" if isdbg 'chan';
141 get '/' => {template => 'index'};
151 dbg "*** starting $0";
157 my $dayno = int ($tnow/86400);
158 for (my $i = 0-$histdays; $i < 0; ++$i ) {
159 push @last5daysh, grab_history(SMGLog->new("day"), "h", $tnow-(86400*$histdays), $dayno+$i+1);
161 @last10minsr = map {my ($t, $js) = split(/\s/, $_, 2); $js} grab_history(SMGLog->new("debug"), "r", $tnow-(60*$windmins), $dayno);
162 dbg sprintf("last5days = %d last10mins = %d", scalar @last5daysh, scalar @last10minsr);
164 our $dlog = SMGLog->new("day");
165 dbg "before next tick";
166 Mojo::IOLoop->next_tick(sub { loop() });
167 dbg "before app start";
168 app->secrets([qw(Here's something that's really seakrett)]);
170 dbg "after app start";
173 $dataf->close if $dataf;
177 # move all the files along one
178 cycle_loop_data_files();
186 ##################################################################################
190 dbg "last_min: " . scalar gmtime($ld->{last_min});
191 dbg "last_hour: " . scalar gmtime($ld->{last_hour});
193 $did = Mojo::IOLoop->recurring(1 => sub {$dlog->flushall});
204 $d =~ s/([\%\x00-\x1f\x7f-\xff])/sprintf("%%%02X", ord($1))/eg;
205 dbg "read added '$d' buf lth=" . length $buf if isdbg 'raw';
206 if ($state eq 'waitnl' && $buf =~ /[\cJ\cM]+/) {
207 dbg "Got \\n" if isdbg 'state';
208 Mojo::IOLoop->remove($tid) if $tid;
212 $ser->write("LPS 1 1\n");
213 chgstate("waitloop");
214 } elsif ($state eq "waitloop") {
215 if ($buf =~ /\x06/) {
216 dbg "Got ACK 0x06" if isdbg 'state';
217 chgstate('waitlooprec');
220 } elsif ($state eq 'waitlooprec') {
221 if (length $buf >= 99) {
222 dbg "got loop record" if isdbg 'chan';
233 dbg "start_loop writing $nlcount \\n" if isdbg 'state';
235 Mojo::IOLoop->remove($tid) if $tid;
237 $tid = Mojo::IOLoop->recurring(0.6 => sub {
238 if (++$nlcount > 10) {
239 dbg "\\n count > 10, closing connection" if isdbg 'chan';
243 dbg "writing $nlcount \\n" if isdbg 'state';
251 dbg "state '$state' -> '$_[0]'" if isdbg 'state';
258 dbg "do reopen on '$name' ending $ending";
260 $ser = do_open($name);
264 Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
275 my $ob = Serial->new($name, 19200) || die "$name $!\n";
276 dbg "streaming $name fileno(" . fileno($ob) . ")" if isdbg 'chan';
278 my $ser = Mojo::IOLoop::Stream->new($ob);
279 $ser->on(error=>sub {dbg "serial $_[1]"; do_reopen($name) unless $ending});
280 $ser->on(close=>sub {dbg "serial closing"; do_reopen($name) unless $ending});
281 $ser->on(timeout=>sub {dbg "serial timeout";});
282 $ser->on(read=>sub {on_read(@_)});
285 Mojo::IOLoop->remove($tid) if $tid;
287 Mojo::IOLoop->remove($rid) if $rid;
289 $rid = Mojo::IOLoop->recurring($poll_interval => sub {
290 start_loop() if !$state;
304 my $loo = substr $blk,0,3;
305 unless ( $loo eq 'LOO') {
306 dbg "Block invalid loo -> $loo" if isdbg 'chan'; return;
314 my $crc_calc = CRC_CCITT($blk);
319 $tmp = unpack("s", substr $blk,7,2) / 1000;
320 $h{Pressure} = nearest(1, in2mb($tmp));
322 $tmp = unpack("s", substr $blk,9,2) / 10;
323 $h{Temp_In} = nearest(0.1, f2c($tmp));
325 $temp = nearest(0.1, f2c(unpack("s", substr $blk,12,2) / 10));
326 $h{Temp_Out} = $temp;
327 if ($temp > 75 || $temp < -75) {
328 dbg "LOOP Temperature out of range ($temp), record ignored";
332 $tmp = unpack("C", substr $blk,14,1);
333 $h{Wind} = nearest(0.1, mph2mps($tmp));
334 $h{Dir} = unpack("s", substr $blk,16,2)+0;
336 my $wind = {w => $h{Wind}, d => $h{Dir}};
337 $wind = 0 if $wind == 255;
338 push @{$ld->{wind_min}}, $wind;
340 $tmp = int(unpack("C", substr $blk,33,1)+0);
342 dbg "LOOP Outside Humidity out of range ($tmp), record ignored";
345 $h{Humidity_Out} = $tmp;
346 $tmp = int(unpack("C", substr $blk,11,1)+0);
348 dbg "LOOP Inside Humidity out of range ($tmp), record ignored";
351 $h{Humidity_In} = $tmp;
354 $tmp = unpack("C", substr $blk,43,1)+0;
355 $h{UV} = $tmp unless $tmp >= 255;
356 $tmp = unpack("s", substr $blk,44,2)+0; # watt/m**2
357 $h{Solar} = $tmp unless $tmp >= 32767;
359 # $h{Rain_Rate} = nearest(0.1,unpack("s", substr $blk,41,2) * $rain_mult);
360 $rain = $h{Rain_Day} = nearest(0.1, unpack("s", substr $blk,50,2) * $rain_mult);
361 my $delta_rain = $h{Rain} = nearest(0.1, ($rain >= $ld->{last_rain} ? $rain - $ld->{last_rain} : $rain)) if $loop_count;
362 $ld->{last_rain} = $rain;
364 # what sort of packet is it?
365 my $sort = unpack("C", substr $blk,4,1);
369 $tmp = unpack("C", substr $blk,18,2);
370 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp/10));
371 $tmp = unpack("C", substr $blk,20,2);
372 # $h{Wind_Avg_2} = nearest(0.1,mph2mps($tmp/10));
373 $tmp = unpack("C", substr $blk,22,2);
374 # $h{Wind_Gust_10} = nearest(0.1,mph2mps($tmp/10));
376 # $h{Dir_Avg_10} = unpack("C", substr $blk,24,2)+0;
377 $tmp = unpack("C", substr $blk,30,2);
378 $h{Dew_Point} = nearest(0.1, f2c($tmp));
383 $tmp = unpack("C", substr $blk,15,1);
384 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp));
385 $h{Dew_Point} = nearest(0.1, dew_point($h{Temp_Out}, $h{Humidity_Out}));
386 $h{Rain_Month} = nearest(0.1, unpack("s", substr $blk,52,2) * $rain_mult);
387 $h{Rain_Year} = nearest(0.1, unpack("s", substr $blk,54,2) * $rain_mult);
392 my $dayno = int($ts/86400);
393 if ($dayno > $ld->{last_day}) {
394 $ld->{Temp_Out_Max} = $ld->{Temp_Out_Min} = $temp;
395 $ld->{Temp_Out_Max_T} = $ld->{Temp_Out_Min_T} = clocktime($ts, 0);
396 $ld->{last_day} = $dayno;
398 cycle_loop_data_files();
400 if ($temp > $ld->{Temp_Out_Max}) {
401 $ld->{Temp_Out_Max} = $temp;
402 $ld->{Temp_Out_Max_T} = clocktime($ts, 0);
405 if ($temp < $ld->{Temp_Out_Min}) {
406 $ld->{Temp_Out_Min} = $temp;
407 $ld->{Temp_Out_Min_T} = clocktime($ts, 0);
411 if ($ts >= $ld->{last_hour} + 1800) {
412 $h{Pressure_Trend} = unpack("C", substr $blk,3,1);
413 $h{Pressure_Trend_txt} = $bar_trend{$h{Pressure_Trend}};
414 $h{Batt_TX_OK} = (unpack("C", substr $blk,86,1)+0) ^ 1;
415 $h{Batt_Console} = nearest(0.01, unpack("s", substr $blk,87,2) * 0.005859375);
416 $h{Forecast_Icon} = unpack("C", substr $blk,89,1);
417 $h{Forecast_Rule} = unpack("C", substr $blk,90,1);
418 $h{Sunrise} = sprintf( "%04d", unpack("S", substr $blk,91,2) );
419 $h{Sunrise} =~ s/(\d{2})(\d{2})/$1:$2/;
420 $h{Sunset} = sprintf( "%04d", unpack("S", substr $blk,93,2) );
421 $h{Sunset} =~ s/(\d{2})(\d{2})/$1:$2/;
422 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
423 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
424 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
425 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
428 if ($loop_count) { # i.e not the first
429 my $a = wind_average(scalar @{$ld->{wind_hour}} ? @{$ld->{wind_hour}} : {w => $h{Wind}, d => $h{Dir}});
431 $h{Wind_1h} = nearest(0.1, $a->{w});
432 $h{Dir_1h} = nearest(0.1, $a->{d});
434 $a = wind_average(@{$ld->{wind_min}});
435 $h{Wind_1m} = nearest(0.1, $a->{w});
436 $h{Dir_1m} = nearest(1, $a->{d});
438 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
440 $ld->{last_rain_min} = $ld->{last_rain_hour} = $rain;
443 $s = genstr($ts, 'h', \%h);
444 $ld->{lasthour_h} = $s;
446 $ld->{last_hour} = int($ts/1800)*1800;
447 $ld->{last_min} = int($ts/60)*60;
448 @{$ld->{wind_hour}} = ();
449 @{$ld->{wind_min}} = ();
453 push @last5daysh, $s;
454 shift @last5daysh if @last5daysh > 5*24;
458 } elsif ($ts >= $ld->{last_min} + 60) {
459 my $a = wind_average(@{$ld->{wind_min}});
462 push @{$ld->{wind_hour}}, $a;
464 if ($loop_count) { # i.e not the first
467 $h{Wind_1m} = nearest(0.1, $a->{w});
468 $h{Dir_1m} = nearest(1, $a->{d});
469 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
471 $ld->{last_rain_min} = $rain;
473 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
474 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
475 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
476 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
479 $s = genstr($ts, 'm', \%h);
480 $ld->{lastmin_h} = $s;
482 $ld->{last_min} = int($ts/60)*60;
483 @{$ld->{wind_min}} = ();
485 output_str($s, 1) if $s;
489 my $o = gen_hash_diff($ld->{last_h}, \%h);
491 $o->{Dir} ||= $h{Dir};
492 $o->{Wind} ||= $h{Wind};
495 $s = genstr($ts, 'r', $o);
496 push @last10minsr, $s;
497 shift @last10minsr while @last10minsr > ($windmins * $updatepermin);
500 dbg "loop rec not changed" if isdbg 'chan';
502 output_str($s, 0) if $s;
507 dbg "CRC check failed for LOOP data!";
518 my $j = $json->encode($h);
519 my $tm = clocktime($ts, 1);
520 return qq|{"tm":"$tm","t":$ts,"$let":$j}|;
527 my ($sec,$min,$hr) = (gmtime $ts)[0,1,2];
530 $s = sprintf "%02d:%02d:%02d", $hr, $min, $sec;
532 $s = sprintf "%02d:%02d", $hr, $min;
544 $dlog->writenow($s) if $logit;
545 foreach my $ws (keys $WS) {
562 while (my ($k, $v) = each %$now) {
563 if (!exists $last->{$k} || $last->{$k} ne $now->{$k}) {
568 return $count ? \%o : undef;
576 # Using the simplified approximation for dew point
577 # Accurate to 1 degree C for humidities > 50 %
578 # http://en.wikipedia.org/wiki/Dew_point
580 my $dewpoint = $temp - ((100 - $rh) / 5);
582 # this is the more complete one (which doesn't work)
586 #my $ytrh = log(($rh/100) + ($b * $temp) / ($c + $temp));
587 #my $dewpoint = ($c * $ytrh) / ($b - $ytrh);
594 # Expects packed data...
595 my $data_str = shift @_;
598 my @lst = split //, $data_str;
599 foreach my $data (@lst) {
600 my $data = unpack("c",$data);
603 my $index = $crc >> 8 ^ $data;
604 my $lhs = $crc_table[$index];
605 #print "lhs=$lhs, crc=$crc\n";
606 my $rhs = ($crc << 8) & 0xFFFF;
617 return ($_[0] - 32) * 5/9;
622 return $_[0] * 0.44704;
627 return $_[0] * 33.8637526;
632 my ($sindir, $cosdir, $wind);
637 $sindir += sin(d2r($r->{d})) * $r->{w};
638 $cosdir += cos(d2r($r->{d})) * $r->{w};
642 my $avhdg = r2d(atan2($sindir, $cosdir));
643 $avhdg += 360 if $avhdg < 0;
644 return {w => nearest(0.1,$wind / $count), d => nearest(0.1,$avhdg)};
651 return ($n / pi) * 180;
658 return ($n / 180) * pi;
665 $ld->{rain24} ||= [];
667 my $Rain_1h = nearest(0.1, $rain >= $ld->{last_rain_hour} ? $rain - $ld->{last_rain_hour} : $rain); # this is the rate for this hour, so far
668 my $rm = nearest(0.1, $rain >= $ld->{last_rain_min} ? $rain - $ld->{last_rain_min} : $rain);
669 my $Rain_1m = nearest(0.1, $rm);
670 push @{$ld->{rain24}}, $Rain_1m;
671 $ld->{rain_24} += $rm;
672 while (@{$ld->{rain24}} > 24*60) {
673 $ld->{rain_24} -= shift @{$ld->{rain24}};
675 my $Rain_24h = nearest(0.1, $ld->{rain_24});
676 return ($Rain_1m, $Rain_1h, $Rain_24h);
682 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
683 $dataf->autoflush(1);
689 dbg "read loop data: $s" if isdbg 'json';
690 $ld = $json->decode($s) if length $s;
692 # sort out rain stats
694 if ($ld->{rain24} && ($c = @{$ld->{rain24}}) < 24*60) {
695 my $diff = 24*60 - $c;
696 unshift @{$ld->{rain24}}, 0 for 0 .. $diff;
701 $rain += $_ for @{$ld->{rain24}};
704 $ld->{rain_24} = nearest(0.1, $rain);
712 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
713 $dataf->autoflush(1);
719 my $s = $json->encode($ld);
720 dbg "write loop data: $s" if isdbg 'json';
724 sub cycle_loop_data_files
726 $dataf->close if $dataf;
729 rename "$datafn.oooo", "$datafn.ooooo";
730 rename "$datafn.ooo", "$datafn.oooo";
731 rename "$datafn.oo", "$datafn.ooo";
732 rename "$datafn.o", "$datafn.oo";
733 copy $datafn, "$datafn.o";
740 my $start = shift || time - 86400;
744 if ($lg->open($dayno, 'r+')) {
745 while (my $l = $lg->read) {
746 next unless $l =~ /,"$let":/;
747 my ($t) = $l =~ /"t":(\d+)/;
748 if ($t && $t >= $start) {