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};
115 $c->inactivity_timeout(3615);
121 dbg "websocket: text $msg" if isdbg 'chan';
125 dbg "websocket: json $msg" if isdbg 'chan';
130 $c->on(finish => sub {
131 my ($c, $code, $reason) = @_;
132 app->log->debug("WebSocket closed with status $code.");
133 dbg "websocket closed with status $code" if isdbg 'chan';
138 get '/' => {template => 'index'};
148 dbg "*** starting $0";
154 my $dayno = int ($tnow/86400);
155 @last5daysh = grab_history(SMGLog->new("day"), "h", $tnow-(86400*5), $_) for ($dayno-4, $dayno-3, $dayno-2, $dayno-1, $dayno);
156 @last10minsr = map {my ($t, $js) = split(/\s/, $_, 2); $js} grab_history(SMGLog->new("debug"), "r", $tnow-(60*3), $dayno);
158 our $dlog = SMGLog->new("day");
159 dbg "before next tick";
160 Mojo::IOLoop->next_tick(sub { loop() });
161 dbg "before app start";
163 dbg "after app start";
166 $dataf->close if $dataf;
170 # move all the files along one
171 cycle_loop_data_files();
179 ##################################################################################
183 dbg "last_min: " . scalar gmtime($ld->{last_min});
184 dbg "last_hour: " . scalar gmtime($ld->{last_hour});
186 $did = Mojo::IOLoop->recurring(1 => sub {$dlog->flushall});
197 $d =~ s/([\%\x00-\x1f\x7f-\xff])/sprintf("%%%02X", ord($1))/eg;
198 dbg "read added '$d' buf lth=" . length $buf if isdbg 'raw';
199 if ($state eq 'waitnl' && $buf =~ /[\cJ\cM]+/) {
200 dbg "Got \\n" if isdbg 'state';
201 Mojo::IOLoop->remove($tid) if $tid;
205 $ser->write("LPS 1 1\n");
206 chgstate("waitloop");
207 } elsif ($state eq "waitloop") {
208 if ($buf =~ /\x06/) {
209 dbg "Got ACK 0x06" if isdbg 'state';
210 chgstate('waitlooprec');
213 } elsif ($state eq 'waitlooprec') {
214 if (length $buf >= 99) {
215 dbg "got loop record" if isdbg 'chan';
226 dbg "start_loop writing $nlcount \\n" if isdbg 'state';
228 Mojo::IOLoop->remove($tid) if $tid;
230 $tid = Mojo::IOLoop->recurring(0.6 => sub {
231 if (++$nlcount > 10) {
232 dbg "\\n count > 10, closing connection" if isdbg 'chan';
236 dbg "writing $nlcount \\n" if isdbg 'state';
244 dbg "state '$state' -> '$_[0]'" if isdbg 'state';
251 dbg "do reopen on '$name' ending $ending";
253 $ser = do_open($name);
257 Mojo::IOLoop->start unless Mojo::IOLoop->is_running;
268 my $ob = Serial->new($name, 19200) || die "$name $!\n";
269 dbg "streaming $name fileno(" . fileno($ob) . ")" if isdbg 'chan';
271 my $ser = Mojo::IOLoop::Stream->new($ob);
272 $ser->on(error=>sub {dbg "serial $_[1]"; do_reopen($name) unless $ending});
273 $ser->on(close=>sub {dbg "serial closing"; do_reopen($name) unless $ending});
274 $ser->on(timeout=>sub {dbg "serial timeout";});
275 $ser->on(read=>sub {on_read(@_)});
278 Mojo::IOLoop->remove($tid) if $tid;
280 Mojo::IOLoop->remove($rid) if $rid;
282 $rid = Mojo::IOLoop->recurring($poll_interval => sub {
283 start_loop() if !$state;
297 my $loo = substr $blk,0,3;
298 unless ( $loo eq 'LOO') {
299 dbg "Block invalid loo -> $loo" if isdbg 'chan'; return;
307 my $crc_calc = CRC_CCITT($blk);
312 $tmp = unpack("s", substr $blk,7,2) / 1000;
313 $h{Pressure} = nearest(1, in2mb($tmp));
315 $tmp = unpack("s", substr $blk,9,2) / 10;
316 $h{Temp_In} = nearest(0.1, f2c($tmp));
318 $temp = nearest(0.1, f2c(unpack("s", substr $blk,12,2) / 10));
319 $h{Temp_Out} = $temp;
320 if ($temp > 75 || $temp < -75) {
321 dbg "LOOP Temperature out of range ($temp), record ignored";
325 $tmp = unpack("C", substr $blk,14,1);
326 $h{Wind} = nearest(0.1, mph2mps($tmp));
327 $h{Dir} = unpack("s", substr $blk,16,2)+0;
329 my $wind = {w => $h{Wind}, d => $h{Dir}};
330 $wind = 0 if $wind == 255;
331 push @{$ld->{wind_min}}, $wind;
333 $tmp = int(unpack("C", substr $blk,33,1)+0);
335 dbg "LOOP Outside Humidity out of range ($tmp), record ignored";
338 $h{Humidity_Out} = $tmp;
339 $tmp = int(unpack("C", substr $blk,11,1)+0);
341 dbg "LOOP Inside Humidity out of range ($tmp), record ignored";
344 $h{Humidity_In} = $tmp;
347 $tmp = unpack("C", substr $blk,43,1)+0;
348 $h{UV} = $tmp unless $tmp >= 255;
349 $tmp = unpack("s", substr $blk,44,2)+0; # watt/m**2
350 $h{Solar} = $tmp unless $tmp >= 32767;
352 # $h{Rain_Rate} = nearest(0.1,unpack("s", substr $blk,41,2) * $rain_mult);
353 $rain = $h{Rain_Day} = nearest(0.1, unpack("s", substr $blk,50,2) * $rain_mult);
354 my $delta_rain = $h{Rain} = nearest(0.1, ($rain >= $ld->{last_rain} ? $rain - $ld->{last_rain} : $rain)) if $loop_count;
355 $ld->{last_rain} = $rain;
357 # what sort of packet is it?
358 my $sort = unpack("C", substr $blk,4,1);
362 $tmp = unpack("C", substr $blk,18,2);
363 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp/10));
364 $tmp = unpack("C", substr $blk,20,2);
365 # $h{Wind_Avg_2} = nearest(0.1,mph2mps($tmp/10));
366 $tmp = unpack("C", substr $blk,22,2);
367 # $h{Wind_Gust_10} = nearest(0.1,mph2mps($tmp/10));
369 # $h{Dir_Avg_10} = unpack("C", substr $blk,24,2)+0;
370 $tmp = unpack("C", substr $blk,30,2);
371 $h{Dew_Point} = nearest(0.1, f2c($tmp));
376 $tmp = unpack("C", substr $blk,15,1);
377 # $h{Wind_Avg_10} = nearest(0.1,mph2mps($tmp));
378 $h{Dew_Point} = nearest(0.1, dew_point($h{Temp_Out}, $h{Humidity_Out}));
379 $h{Rain_Month} = nearest(0.1, unpack("s", substr $blk,52,2) * $rain_mult);
380 $h{Rain_Year} = nearest(0.1, unpack("s", substr $blk,54,2) * $rain_mult);
385 my $dayno = int($ts/86400);
386 if ($dayno > $ld->{last_day}) {
387 $ld->{Temp_Out_Max} = $ld->{Temp_Out_Min} = $temp;
388 $ld->{Temp_Out_Max_T} = $ld->{Temp_Out_Min_T} = clocktime($ts, 0);
389 $ld->{last_day} = $dayno;
391 cycle_loop_data_files();
393 if ($temp > $ld->{Temp_Out_Max}) {
394 $ld->{Temp_Out_Max} = $temp;
395 $ld->{Temp_Out_Max_T} = clocktime($ts, 0);
398 if ($temp < $ld->{Temp_Out_Min}) {
399 $ld->{Temp_Out_Min} = $temp;
400 $ld->{Temp_Out_Min_T} = clocktime($ts, 0);
404 if ($ts >= $ld->{last_hour} + 1800) {
405 $h{Pressure_Trend} = unpack("C", substr $blk,3,1);
406 $h{Pressure_Trend_txt} = $bar_trend{$h{Pressure_Trend}};
407 $h{Batt_TX_OK} = (unpack("C", substr $blk,86,1)+0) ^ 1;
408 $h{Batt_Console} = nearest(0.01, unpack("s", substr $blk,87,2) * 0.005859375);
409 $h{Forecast_Icon} = unpack("C", substr $blk,89,1);
410 $h{Forecast_Rule} = unpack("C", substr $blk,90,1);
411 $h{Sunrise} = sprintf( "%04d", unpack("S", substr $blk,91,2) );
412 $h{Sunrise} =~ s/(\d{2})(\d{2})/$1:$2/;
413 $h{Sunset} = sprintf( "%04d", unpack("S", substr $blk,93,2) );
414 $h{Sunset} =~ s/(\d{2})(\d{2})/$1:$2/;
415 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
416 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
417 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
418 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
421 if ($loop_count) { # i.e not the first
422 my $a = wind_average(scalar @{$ld->{wind_hour}} ? @{$ld->{wind_hour}} : {w => $h{Wind}, d => $h{Dir}});
424 $h{Wind_1h} = nearest(0.1, $a->{w});
425 $h{Dir_1h} = nearest(0.1, $a->{d});
427 $a = wind_average(@{$ld->{wind_min}});
428 $h{Wind_1m} = nearest(0.1, $a->{w});
429 $h{Dir_1m} = nearest(1, $a->{d});
431 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
433 $ld->{last_rain_min} = $ld->{last_rain_hour} = $rain;
436 $s = genstr($ts, 'h', \%h);
437 $ld->{lasthour_h} = $s;
439 $ld->{last_hour} = int($ts/1800)*1800;
440 $ld->{last_min} = int($ts/60)*60;
441 @{$ld->{wind_hour}} = ();
442 @{$ld->{wind_min}} = ();
446 push @last5daysh, $s;
447 shift @last5daysh if @last5daysh > 5*24;
451 } elsif ($ts >= $ld->{last_min} + 60) {
452 my $a = wind_average(@{$ld->{wind_min}});
455 push @{$ld->{wind_hour}}, $a;
457 if ($loop_count) { # i.e not the first
460 $h{Wind_1m} = nearest(0.1, $a->{w});
461 $h{Dir_1m} = nearest(1, $a->{d});
462 ($h{Rain_1m}, $h{Rain_1h}, $h{Rain_24h}) = calc_rain($rain);
464 $ld->{last_rain_min} = $rain;
466 $h{Temp_Out_Max} = $ld->{Temp_Out_Max};
467 $h{Temp_Out_Min} = $ld->{Temp_Out_Min};
468 $h{Temp_Out_Max_T} = $ld->{Temp_Out_Max_T};
469 $h{Temp_Out_Min_T} = $ld->{Temp_Out_Min_T};
472 $s = genstr($ts, 'm', \%h);
473 $ld->{lastmin_h} = $s;
475 $ld->{last_min} = int($ts/60)*60;
476 @{$ld->{wind_min}} = ();
478 output_str($s, 1) if $s;
482 my $o = gen_hash_diff($ld->{last_h}, \%h);
484 $s = genstr($ts, 'r', $o);
485 push @last10minsr, $s;
486 shift @last10minsr if @last10minsr > 240;
489 dbg "loop rec not changed" if isdbg 'chan';
491 output_str($s, 0) if $s;
496 dbg "CRC check failed for LOOP data!";
507 my $j = $json->encode($h);
508 my $tm = clocktime($ts, 1);
509 return qq|{"tm":"$tm","t":$ts,"$let":$j}|;
516 my ($sec,$min,$hr) = (gmtime $ts)[0,1,2];
519 $s = sprintf "%02d:%02d:%02d", $hr, $min, $sec;
521 $s = sprintf "%02d:%02d", $hr, $min;
533 $dlog->writenow($s) if $logit;
534 foreach my $ws (keys $WS) {
551 while (my ($k, $v) = each %$now) {
552 if (!exists $last->{$k} || $last->{$k} ne $now->{$k}) {
557 return $count ? \%o : undef;
565 # Using the simplified approximation for dew point
566 # Accurate to 1 degree C for humidities > 50 %
567 # http://en.wikipedia.org/wiki/Dew_point
569 my $dewpoint = $temp - ((100 - $rh) / 5);
571 # this is the more complete one (which doesn't work)
575 #my $ytrh = log(($rh/100) + ($b * $temp) / ($c + $temp));
576 #my $dewpoint = ($c * $ytrh) / ($b - $ytrh);
583 # Expects packed data...
584 my $data_str = shift @_;
587 my @lst = split //, $data_str;
588 foreach my $data (@lst) {
589 my $data = unpack("c",$data);
592 my $index = $crc >> 8 ^ $data;
593 my $lhs = $crc_table[$index];
594 #print "lhs=$lhs, crc=$crc\n";
595 my $rhs = ($crc << 8) & 0xFFFF;
606 return ($_[0] - 32) * 5/9;
611 return $_[0] * 0.44704;
616 return $_[0] * 33.8637526;
621 my ($sindir, $cosdir, $wind);
626 $sindir += sin(d2r($r->{d})) * $r->{w};
627 $cosdir += cos(d2r($r->{d})) * $r->{w};
631 my $avhdg = r2d(atan2($sindir, $cosdir));
632 $avhdg += 360 if $avhdg < 0;
633 return {w => nearest(0.1,$wind / $count), d => nearest(0.1,$avhdg)};
640 return ($n / pi) * 180;
647 return ($n / 180) * pi;
654 $ld->{rain24} ||= [];
656 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
657 my $rm = nearest(0.1, $rain >= $ld->{last_rain_min} ? $rain - $ld->{last_rain_min} : $rain);
658 my $Rain_1m = nearest(0.1, $rm);
659 push @{$ld->{rain24}}, $Rain_1m;
660 $ld->{rain_24} += $rm;
661 while (@{$ld->{rain24}} > 24*60) {
662 $ld->{rain_24} -= shift @{$ld->{rain24}};
664 my $Rain_24h = nearest(0.1, $ld->{rain_24});
665 return ($Rain_1m, $Rain_1h, $Rain_24h);
671 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
672 $dataf->autoflush(1);
678 dbg "read loop data: $s" if isdbg 'json';
679 $ld = $json->decode($s) if length $s;
681 # sort out rain stats
683 if ($ld->{rain24} && ($c = @{$ld->{rain24}}) < 24*60) {
684 my $diff = 24*60 - $c;
685 unshift @{$ld->{rain24}}, 0 for 0 .. $diff;
690 $rain += $_ for @{$ld->{rain24}};
693 $ld->{rain_24} = nearest(0.1, $rain);
701 $dataf = IO::File->new("+>> $datafn") or die "cannot open $datafn $!";
702 $dataf->autoflush(1);
708 my $s = $json->encode($ld);
709 dbg "write loop data: $s" if isdbg 'json';
713 sub cycle_loop_data_files
715 $dataf->close if $dataf;
718 rename "$datafn.oooo", "$datafn.ooooo";
719 rename "$datafn.ooo", "$datafn.oooo";
720 rename "$datafn.oo", "$datafn.ooo";
721 rename "$datafn.o", "$datafn.oo";
722 copy $datafn, "$datafn.o";
729 my $start = shift || time - 86400;
733 if ($lg->open($dayno, 'r+')) {
734 while (my $l = $lg->read) {
735 next unless $l =~ /,"$let":/;
736 my ($t) = $l =~ /"t":(\d+)/;
737 if ($t && $t >= $start) {