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 = ();
46 our $loop_count; # how many LOOPs we have done, used as start indicator
49 0x0, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
50 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
51 0x1231, 0x210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
52 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
53 0x2462, 0x3443, 0x420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
54 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
55 0x3653, 0x2672, 0x1611, 0x630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
56 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
57 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x840, 0x1861, 0x2802, 0x3823,
58 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
59 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0xa50, 0x3a33, 0x2a12,
60 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
61 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0xc60, 0x1c41,
62 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
63 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0xe70,
64 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
65 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
66 0x1080, 0xa1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
67 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
68 0x2b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
69 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
70 0x34e2, 0x24c3, 0x14a0, 0x481, 0x7466, 0x6447, 0x5424, 0x4405,
71 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
72 0x26d3, 0x36f2, 0x691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
73 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
74 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x8e1, 0x3882, 0x28a3,
75 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
76 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0xaf1, 0x1ad0, 0x2ab3, 0x3a92,
77 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
78 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0xcc1,
79 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
80 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0xed1, 0x1ef0
85 $bar_trend{-60} = "Falling Rapidly";
86 $bar_trend{196} = "Falling Rapidly";
87 $bar_trend{-20} = "Falling Slowly";
88 $bar_trend{236} = "Falling Slowly";
89 $bar_trend{0} = "Steady";
90 $bar_trend{20} = "Rising Slowly";
91 $bar_trend{60} = "Rising Rapidly";
95 $SIG{TERM} = $SIG{INT} = sub {++$ending; Mojo::IOLoop->stop;};
99 # WebSocket weather service
100 websocket '/weather' => sub {
106 app->log->debug('WebSocket opened.');
107 dbg 'WebSocket opened' if isdbg 'chan';
110 # send historical data
111 $c->send($ld->{lasthour_h}) if exists $ld->{lasthour_h};
112 $c->send($ld->{lastmin_h}) if exists $ld->{lastmin_h};
114 # send the 5 days worth of data to the graph
115 say "last10min = " . scalar @last10minsr . " last5day = " . scalar @last5daysh;
116 $c->send($_) for @last10minsr;
117 $c->send($_) for @last5daysh;
120 $c->inactivity_timeout(3615);
126 dbg "websocket: text $msg" if isdbg 'chan';
130 dbg "websocket: json $msg" if isdbg 'chan';
135 $c->on(finish => sub {
136 my ($c, $code, $reason) = @_;
137 app->log->debug("WebSocket closed with status $code.");
138 dbg "websocket closed with status $code" if isdbg 'chan';
143 get '/' => {template => 'index'};
153 dbg "*** starting $0";
159 my $dayno = int ($tnow/86400);
160 @last5daysh = grab_history(SMGLog->new("day"), "h", $tnow-(86400*5), $_) for ($dayno-4, $dayno-3, $dayno-2, $dayno-1, $dayno);
161 @last10minsr = map {my ($t, $js) = split(/\s/, $_, 2); $js} grab_history(SMGLog->new("debug"), "r", $tnow-(60*3), $dayno);
163 our $dlog = SMGLog->new("day");
164 dbg "before next tick";
165 Mojo::IOLoop->next_tick(sub { loop() });
166 dbg "before app start";
168 dbg "after app start";
171 $dataf->close if $dataf;
175 # move all the files along one
176 cycle_loop_data_files();
184 ##################################################################################
188 dbg "last_min: " . scalar gmtime($ld->{last_min});
189 dbg "last_hour: " . scalar gmtime($ld->{last_hour});
191 $did = Mojo::IOLoop->recurring(1 => sub {$dlog->flushall});
202 $d =~ s/([\%\x00-\x1f\x7f-\xff])/sprintf("%%%02X", ord($1))/eg;
203 dbg "read added '$d' buf lth=" . length $buf if isdbg 'raw';
204 if ($state eq 'waitnl' && $buf =~ /[\cJ\cM]+/) {
205 dbg "Got \\n" if isdbg 'state';
206 Mojo::IOLoop->remove($tid) if $tid;
210 $ser->write("LPS 1 1\n");
211 chgstate("waitloop");
212 } elsif ($state eq "waitloop") {
213 if ($buf =~ /\x06/) {
214 dbg "Got ACK 0x06" if isdbg 'state';
215 chgstate('waitlooprec');
218 } elsif ($state eq 'waitlooprec') {
219 if (length $buf >= 99) {
220 dbg "got loop record" if isdbg 'chan';
231 dbg "start_loop writing $nlcount \\n" if isdbg 'state';
233 Mojo::IOLoop->remove($tid) if $tid;
235 $tid = Mojo::IOLoop->recurring(0.6 => sub {
236 if (++$nlcount > 10) {
237 dbg "\\n count > 10, closing connection" if isdbg 'chan';
241 dbg "writing $nlcount \\n" if isdbg 'state';
249 dbg "state '$state' -> '$_[0]'" if isdbg 'state';
256 dbg "do reopen on '$name' ending $ending";
258 $ser = do_open($name);
262 Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
273 my $ob = Serial->new($name, 19200) || die "$name $!\n";
274 dbg "streaming $name fileno(" . fileno($ob) . ")" if isdbg 'chan';
276 my $ser = Mojo::IOLoop::Stream->new($ob);
277 $ser->on(error=>sub {dbg "serial $_[1]"; do_reopen($name) unless $ending});
278 $ser->on(close=>sub {dbg "serial closing"; do_reopen($name) unless $ending});
279 $ser->on(timeout=>sub {dbg "serial timeout";});
280 $ser->on(read=>sub {on_read(@_)});
283 Mojo::IOLoop->remove($tid) if $tid;
285 Mojo::IOLoop->remove($rid) if $rid;
287 $rid = Mojo::IOLoop->recurring($poll_interval => sub {
288 start_loop() if !$state;
302 my $loo = substr $blk,0,3;
303 unless ( $loo eq 'LOO') {
304 dbg "Block invalid loo -> $loo" if isdbg 'chan'; return;
312 my $crc_calc = CRC_CCITT($blk);
317 $tmp = unpack("s", substr $blk,7,2) / 1000;
318 $h{Pressure} = nearest(1, in2mb($tmp));
320 $tmp = unpack("s", substr $blk,9,2) / 10;
321 $h{Temp_In} = nearest(0.1, f2c($tmp));
323 $temp = nearest(0.1, f2c(unpack("s", substr $blk,12,2) / 10));
324 $h{Temp_Out} = $temp;
325 if ($temp > 75 || $temp < -75) {
326 dbg "LOOP Temperature out of range ($temp), record ignored";
330 $tmp = unpack("C", substr $blk,14,1);
331 $h{Wind} = nearest(0.1, mph2mps($tmp));
332 $h{Dir} = unpack("s", substr $blk,16,2)+0;
334 my $wind = {w => $h{Wind}, d => $h{Dir}};
335 $wind = 0 if $wind == 255;
336 push @{$ld->{wind_min}}, $wind;
338 $tmp = int(unpack("C", substr $blk,33,1)+0);
340 dbg "LOOP Outside Humidity out of range ($tmp), record ignored";
343 $h{Humidity_Out} = $tmp;
344 $tmp = int(unpack("C", substr $blk,11,1)+0);
346 dbg "LOOP Inside Humidity out of range ($tmp), record ignored";
349 $h{Humidity_In} = $tmp;
352 $tmp = unpack("C", substr $blk,43,1)+0;
353 $h{UV} = $tmp unless $tmp >= 255;
354 $tmp = unpack("s", substr $blk,44,2)+0; # watt/m**2
355 $h{Solar} = $tmp unless $tmp >= 32767;
357 # $h{Rain_Rate} = nearest(0.1,unpack("s", substr $blk,41,2) * $rain_mult);
358 $rain = $h{Rain_Day} = nearest(0.1, unpack("s", substr $blk,50,2) * $rain_mult);
359 my $delta_rain = $h{Rain} = nearest(0.1, ($rain >= $ld->{last_rain} ? $rain - $ld->{last_rain} : $rain)) if $loop_count;
360 $ld->{last_rain} = $rain;
362 # what sort of packet is it?
363 my $sort = unpack("C", substr $blk,4,1);
367 $tmp = unpack("C", substr $blk,18,2);
368 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp/10));
369 $tmp = unpack("C", substr $blk,20,2);
370 # $h{Wind_Avg_2} = nearest(0.1,mph2mps($tmp/10));
371 $tmp = unpack("C", substr $blk,22,2);
372 # $h{Wind_Gust_10} = nearest(0.1,mph2mps($tmp/10));
374 # $h{Dir_Avg_10} = unpack("C", substr $blk,24,2)+0;
375 $tmp = unpack("C", substr $blk,30,2);
376 $h{Dew_Point} = nearest(0.1, f2c($tmp));
381 $tmp = unpack("C", substr $blk,15,1);
382 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp));
383 $h{Dew_Point} = nearest(0.1, dew_point($h{Temp_Out}, $h{Humidity_Out}));
384 $h{Rain_Month} = nearest(0.1, unpack("s", substr $blk,52,2) * $rain_mult);
385 $h{Rain_Year} = nearest(0.1, unpack("s", substr $blk,54,2) * $rain_mult);
390 my $dayno = int($ts/86400);
391 if ($dayno > $ld->{last_day}) {
392 $ld->{Temp_Out_Max} = $ld->{Temp_Out_Min} = $temp;
393 $ld->{Temp_Out_Max_T} = $ld->{Temp_Out_Min_T} = clocktime($ts, 0);
394 $ld->{last_day} = $dayno;
396 cycle_loop_data_files();
398 if ($temp > $ld->{Temp_Out_Max}) {
399 $ld->{Temp_Out_Max} = $temp;
400 $ld->{Temp_Out_Max_T} = clocktime($ts, 0);
403 if ($temp < $ld->{Temp_Out_Min}) {
404 $ld->{Temp_Out_Min} = $temp;
405 $ld->{Temp_Out_Min_T} = clocktime($ts, 0);
409 if ($ts >= $ld->{last_hour} + 1800) {
410 $h{Pressure_Trend} = unpack("C", substr $blk,3,1);
411 $h{Pressure_Trend_txt} = $bar_trend{$h{Pressure_Trend}};
412 $h{Batt_TX_OK} = (unpack("C", substr $blk,86,1)+0) ^ 1;
413 $h{Batt_Console} = nearest(0.01, unpack("s", substr $blk,87,2) * 0.005859375);
414 $h{Forecast_Icon} = unpack("C", substr $blk,89,1);
415 $h{Forecast_Rule} = unpack("C", substr $blk,90,1);
416 $h{Sunrise} = sprintf( "%04d", unpack("S", substr $blk,91,2) );
417 $h{Sunrise} =~ s/(\d{2})(\d{2})/$1:$2/;
418 $h{Sunset} = sprintf( "%04d", unpack("S", substr $blk,93,2) );
419 $h{Sunset} =~ s/(\d{2})(\d{2})/$1:$2/;
420 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
421 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
422 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
423 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
426 if ($loop_count) { # i.e not the first
427 my $a = wind_average(scalar @{$ld->{wind_hour}} ? @{$ld->{wind_hour}} : {w => $h{Wind}, d => $h{Dir}});
429 $h{Wind_1h} = nearest(0.1, $a->{w});
430 $h{Dir_1h} = nearest(0.1, $a->{d});
432 $a = wind_average(@{$ld->{wind_min}});
433 $h{Wind_1m} = nearest(0.1, $a->{w});
434 $h{Dir_1m} = nearest(1, $a->{d});
436 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
438 $ld->{last_rain_min} = $ld->{last_rain_hour} = $rain;
441 $s = genstr($ts, 'h', \%h);
442 $ld->{lasthour_h} = $s;
444 $ld->{last_hour} = int($ts/1800)*1800;
445 $ld->{last_min} = int($ts/60)*60;
446 @{$ld->{wind_hour}} = ();
447 @{$ld->{wind_min}} = ();
451 push @last5daysh, $s;
452 shift @last5daysh if @last5daysh > 5*24;
456 } elsif ($ts >= $ld->{last_min} + 60) {
457 my $a = wind_average(@{$ld->{wind_min}});
460 push @{$ld->{wind_hour}}, $a;
462 if ($loop_count) { # i.e not the first
465 $h{Wind_1m} = nearest(0.1, $a->{w});
466 $h{Dir_1m} = nearest(1, $a->{d});
467 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
469 $ld->{last_rain_min} = $rain;
471 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
472 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
473 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
474 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
477 $s = genstr($ts, 'm', \%h);
478 $ld->{lastmin_h} = $s;
480 $ld->{last_min} = int($ts/60)*60;
481 @{$ld->{wind_min}} = ();
483 output_str($s, 1) if $s;
487 my $o = gen_hash_diff($ld->{last_h}, \%h);
489 $s = genstr($ts, 'r', $o);
490 push @last10minsr, $s;
491 shift @last10minsr if @last10minsr > 240;
494 dbg "loop rec not changed" if isdbg 'chan';
496 output_str($s, 0) if $s;
501 dbg "CRC check failed for LOOP data!";
512 my $j = $json->encode($h);
513 my $tm = clocktime($ts, 1);
514 return qq|{"tm":"$tm","t":$ts,"$let":$j}|;
521 my ($sec,$min,$hr) = (gmtime $ts)[0,1,2];
524 $s = sprintf "%02d:%02d:%02d", $hr, $min, $sec;
526 $s = sprintf "%02d:%02d", $hr, $min;
538 $dlog->writenow($s) if $logit;
539 foreach my $ws (keys $WS) {
556 while (my ($k, $v) = each %$now) {
557 if (!exists $last->{$k} || $last->{$k} ne $now->{$k}) {
562 return $count ? \%o : undef;
570 # Using the simplified approximation for dew point
571 # Accurate to 1 degree C for humidities > 50 %
572 # http://en.wikipedia.org/wiki/Dew_point
574 my $dewpoint = $temp - ((100 - $rh) / 5);
576 # this is the more complete one (which doesn't work)
580 #my $ytrh = log(($rh/100) + ($b * $temp) / ($c + $temp));
581 #my $dewpoint = ($c * $ytrh) / ($b - $ytrh);
588 # Expects packed data...
589 my $data_str = shift @_;
592 my @lst = split //, $data_str;
593 foreach my $data (@lst) {
594 my $data = unpack("c",$data);
597 my $index = $crc >> 8 ^ $data;
598 my $lhs = $crc_table[$index];
599 #print "lhs=$lhs, crc=$crc\n";
600 my $rhs = ($crc << 8) & 0xFFFF;
611 return ($_[0] - 32) * 5/9;
616 return $_[0] * 0.44704;
621 return $_[0] * 33.8637526;
626 my ($sindir, $cosdir, $wind);
631 $sindir += sin(d2r($r->{d})) * $r->{w};
632 $cosdir += cos(d2r($r->{d})) * $r->{w};
636 my $avhdg = r2d(atan2($sindir, $cosdir));
637 $avhdg += 360 if $avhdg < 0;
638 return {w => nearest(0.1,$wind / $count), d => nearest(0.1,$avhdg)};
645 return ($n / pi) * 180;
652 return ($n / 180) * pi;
659 $ld->{rain24} ||= [];
661 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
662 my $rm = nearest(0.1, $rain >= $ld->{last_rain_min} ? $rain - $ld->{last_rain_min} : $rain);
663 my $Rain_1m = nearest(0.1, $rm);
664 push @{$ld->{rain24}}, $Rain_1m;
665 $ld->{rain_24} += $rm;
666 while (@{$ld->{rain24}} > 24*60) {
667 $ld->{rain_24} -= shift @{$ld->{rain24}};
669 my $Rain_24h = nearest(0.1, $ld->{rain_24});
670 return ($Rain_1m, $Rain_1h, $Rain_24h);
676 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
677 $dataf->autoflush(1);
683 dbg "read loop data: $s" if isdbg 'json';
684 $ld = $json->decode($s) if length $s;
686 # sort out rain stats
688 if ($ld->{rain24} && ($c = @{$ld->{rain24}}) < 24*60) {
689 my $diff = 24*60 - $c;
690 unshift @{$ld->{rain24}}, 0 for 0 .. $diff;
695 $rain += $_ for @{$ld->{rain24}};
698 $ld->{rain_24} = nearest(0.1, $rain);
706 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
707 $dataf->autoflush(1);
713 my $s = $json->encode($ld);
714 dbg "write loop data: $s" if isdbg 'json';
718 sub cycle_loop_data_files
720 $dataf->close if $dataf;
723 rename "$datafn.oooo", "$datafn.ooooo";
724 rename "$datafn.ooo", "$datafn.oooo";
725 rename "$datafn.oo", "$datafn.ooo";
726 rename "$datafn.o", "$datafn.oo";
727 copy $datafn, "$datafn.o";
734 my $start = shift || time - 86400;
738 if ($lg->open($dayno, 'r+')) {
739 while (my $l = $lg->read) {
740 next unless $l =~ /,"$let":/;
741 my ($t) = $l =~ /"t":(\d+)/;
742 if ($t && $t >= $start) {