[Perl] DAQ system for the FAPG.
1
use Mojo::Base -strict;
2
3
use Test2::V0;
4
5
use FindBin;
6
use lib "${FindBin::Bin}/../lib/";
7
use lib "${FindBin::Bin}/lib/";
8
use Dashboard::Test qw(test_app test_empty_app);
9
10
my $t = test_app();
11
12
subtest 'weather model stores and retrieves data' => sub{
13
my $weather = $t->app->weather;
14
ok $weather, 'weather helper is available';
15
16
my $now = time;
17
my @rows = map {
18
{ epoch => ( int( $now / 3600 ) - $_ ) * 3600,
19
temperature => 20 + $_ * 0.5,
20
humidity => 60 + $_,
21
rain => $_ % 3 == 0 ? 0.2 : 0,
22
}
23
} 1 .. 12;
24
25
my $stored = $weather->store_batch( \@rows );
26
is $stored, 12, 'stored 12 weather points';
27
28
my $range = $weather->get_range( $rows[-1]{}, $rows[0]{} );
29
is scalar @$range, 12, 'get_range returns all 12 points';
30
ok $range->[0]{}, 'points have temperature';
31
ok $range->[0]{}, 'points have humidity';
32
33
my $latest = $weather->latest;
34
ok $latest, 'latest returns a row';
35
is $latest->{}, $rows[0]{}, 'latest is the most recent epoch';
36
};
37
38
subtest 'weather model deduplicates on epoch' => sub{
39
my $weather = $t->app->weather;
40
my $epoch = int( time / 3600 ) * 3600;
41
42
$weather->store_batch(
43
[ { epoch => $epoch, temperature => 25, humidity => 70, rain => 0 } ]
44
);
45
$weather->store_batch(
46
[ { epoch => $epoch,
47
temperature => 26,
48
humidity => 71,
49
rain => 0.1
50
}
51
]
52
);
53
54
my $range = $weather->get_range( $epoch, $epoch );
55
is scalar @$range, 1, 'only one row for the same epoch';
56
is $range->[0]{}, 26, 'second insert replaces first';
57
};
58
59
subtest 'weather API hourly endpoint' => sub{
60
$t->get_ok('/api/weather/hourly?hours=24')
61
->status_is(200)
62
->json_has('/points')
63
->json_is( '/hours' => 24 );
64
65
my $points = $t->tx->res->json->{};
66
ok @$points > 0, 'hourly returns seeded weather points';
67
ok exists $points->[0]{}, 'points have temperature';
68
ok exists $points->[0]{}, 'points have humidity';
69
ok exists $points->[0]{}, 'points have rain';
70
ok exists $points->[0]{}, 'points have timestamp';
71
};
72
73
subtest 'weather API forecast endpoint' => sub{
74
75
# Seed some future data
76
my $future = ( int( time / 3600 ) + 2 ) * 3600;
77
$t->app->weather->store_batch(
78
[ { epoch => $future,
79
temperature => 28,
80
humidity => 55,
81
rain => 0
82
},
83
{ epoch => $future + 3600,
84
temperature => 27,
85
humidity => 58,
86
rain => 0.5
87
},
88
]
89
);
90
91
$t->get_ok('/api/weather/forecast')
92
->status_is(200)
93
->json_has('/points')
94
->json_has('/from');
95
96
my $points = $t->tx->res->json->{points};
97
ok @$points > 0, 'forecast returns points';
98
};
99
100
subtest 'weather API current endpoint excludes future forecasts' => sub {
101
my $current_hour = int( time / 3600 ) * 3600;
102
my $future = $current_hour + 3600;
103
104
$t->app->weather->store_batch(
105
[ { epoch => $current_hour,
106
temperature => 22,
107
humidity => 65,
108
rain => 0,
109
},
110
{ epoch => $future,
111
temperature => 99,
112
humidity => 1,
113
rain => 10,
114
},
115
]
116
);
117
118
my $latest = $t->app->weather->latest_at(time);
119
is $latest->{epoch}, $current_hour,
120
'model selects the latest point at or before now';
121
122
$t->get_ok('/api/weather/current')
123
->status_is(200)
124
->json_is( '/temperature' => 22 )
125
->json_is( '/timestamp' => utc_timestamp($current_hour) );
126
};
127
128
subtest 'weather API refresh endpoint is POST-only' => sub {
129
local $t->app->config->{weather}{enabled} = 0;
130
131
$t->get_ok('/api/weather/refresh')->status_is(404);
132
$t->post_ok('/api/weather/refresh')
133
->status_is(200)
134
->json_is( '/status' => 'disabled' );
135
};
136
137
subtest 'weather page renders' => sub {
138
$t->get_ok('/weather')
139
->status_is(200)
140
->text_is( 'title', 'FAPG DAQ Weather' )
141
->element_exists('canvas#chart-weather-combined')
142
->element_exists('canvas#chart-weather-solar')
143
->element_exists('canvas#chart-weather-ground')
144
->element_exists(
145
'main section[data-controls-panel][aria-labelledby="weather-controls-title"]'
146
)
147
->text_is( '#weather-controls-title', 'Weather controls' )
148
->element_exists(
149
'#weather-controls .graph-control[data-default-timeframe="day"]')
150
->element_exists('[data-default-timeframe="day"]');
151
};
152
153
subtest 'weather page has nav link' => sub {
154
$t->get_ok('/weather')
155
->status_is(200)
156
->element_exists('.navbar-nav a.nav-link.active[href="/weather"]');
157
};
158
159
subtest 'index page has weather forecast card' => sub {
160
$t->get_ok('/')
161
->status_is(200)
162
->element_exists('[data-weather-forecast]')
163
->element_exists('canvas#chart-weather-forecast');
164
};
165
166
subtest 'empty database weather endpoints' => sub {
167
my $empty = test_empty_app();
168
$empty->get_ok('/api/weather/hourly')
169
->status_is(200)
170
->json_is( '/points' => [] );
171
$empty->get_ok('/api/weather/current')
172
->status_is(200)
173
->json_is( '/temperature' => undef );
174
$empty->get_ok('/weather')->status_is(200);
175
};
176
177
done_testing;
178
179
sub utc_timestamp {
180
my ($epoch) = @_;
181
require POSIX;
182
return POSIX::strftime( '%Y-%m-%dT%H:%M:%SZ', gmtime $epoch );
183
}
184