line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Weather::NOAA::Alert; |
2
|
|
|
|
|
|
|
|
3
|
1
|
|
|
1
|
|
24542
|
use strict; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
42
|
|
4
|
1
|
|
|
1
|
|
6
|
use warnings; |
|
1
|
|
|
|
|
3
|
|
|
1
|
|
|
|
|
30
|
|
5
|
|
|
|
|
|
|
|
6
|
1
|
|
|
1
|
|
914
|
use LWP::Simple; |
|
1
|
|
|
|
|
94217
|
|
|
1
|
|
|
|
|
10
|
|
7
|
1
|
|
|
1
|
|
916
|
use XML::Twig; |
|
0
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
8
|
|
|
|
|
|
|
use Data::Dumper; |
9
|
|
|
|
|
|
|
#use Carp; |
10
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
#use Exporter; |
12
|
|
|
|
|
|
|
#our @ISA = qw(Exporter); |
13
|
|
|
|
|
|
|
#@EXPORT = qw//; |
14
|
|
|
|
|
|
|
#@EXPORT_OK = qw//; |
15
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
=head1 NAME |
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
Weather::NOAA::Alert - Polling and parsing of NOAA CAP 1.1 Alerts |
19
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
=head1 VERSION |
21
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
Version 0.90 |
23
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
=cut |
25
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
our $VERSION = '0.90'; |
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
=head1 SYNOPSIS |
29
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
my $alert = Weather::NOAA::Alert->new(['TXZ104', 'TXC082', 'TXZ097']); |
31
|
|
|
|
|
|
|
my $events = $alert->get_events(); |
32
|
|
|
|
|
|
|
my ($errorCount, $eventCount, $addCount, $deleteCount) = $alert->poll_events(); |
33
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
=head1 DESCRIPTION |
35
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
Weather::NOAA::Alert will retrieve and parse the NOAA National Weather Service |
37
|
|
|
|
|
|
|
ATOM and CAP 1.1 feeds for the specified forecast zones. It is designed to |
38
|
|
|
|
|
|
|
cache previously polled CAP items. The overall process is to get the |
39
|
|
|
|
|
|
|
requested ATOM feed, get any CAP entries that are not in the cache, and store |
40
|
|
|
|
|
|
|
the alerts. |
41
|
|
|
|
|
|
|
|
42
|
|
|
|
|
|
|
You can find the zone list and more information about NOAA watches, warnings, |
43
|
|
|
|
|
|
|
and advisories at the following sites: |
44
|
|
|
|
|
|
|
|
45
|
|
|
|
|
|
|
=over 4 |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
=item * Zone List: L |
48
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
=item * Zone Maps: L |
50
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
=back |
52
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
=head1 EXAMPLE |
54
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
use Weather::NOAA::Alert; |
56
|
|
|
|
|
|
|
use Data::Dumper; |
57
|
|
|
|
|
|
|
|
58
|
|
|
|
|
|
|
my $SLEEP_SECS = 60; #Poll every 1 minute |
59
|
|
|
|
|
|
|
|
60
|
|
|
|
|
|
|
#my $alert = Weather::NOAA::Alert->new(['US']); |
61
|
|
|
|
|
|
|
my $alert = Weather::NOAA::Alert->new(['TXC085', 'TXZ097']); |
62
|
|
|
|
|
|
|
$alert->printLog(1); |
63
|
|
|
|
|
|
|
$alert->errorLog(1); |
64
|
|
|
|
|
|
|
|
65
|
|
|
|
|
|
|
my $events = $alert->get_events(); |
66
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
while (1) { |
68
|
|
|
|
|
|
|
my ($errorCount, $eventCount, $addCount, $deleteCount) |
69
|
|
|
|
|
|
|
= $alert->poll_events(); |
70
|
|
|
|
|
|
|
|
71
|
|
|
|
|
|
|
print Dumper( $events) . "\n"; |
72
|
|
|
|
|
|
|
|
73
|
|
|
|
|
|
|
print "Tracking $eventCount " . ($eventCount ==1 ? "event" : "events"); |
74
|
|
|
|
|
|
|
print " $addCount added, $deleteCount deleted"; |
75
|
|
|
|
|
|
|
print ", $errorCount " . ($errorCount ==1 ? "error" : "errors"); |
76
|
|
|
|
|
|
|
print "\n"; |
77
|
|
|
|
|
|
|
|
78
|
|
|
|
|
|
|
print "Sleeping $SLEEP_SECS seconds\n-----------------\n\n"; |
79
|
|
|
|
|
|
|
sleep($SLEEP_SECS); |
80
|
|
|
|
|
|
|
} |
81
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
|
83
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
=head1 METHODS |
85
|
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
=head2 B - Create a new Weather::NOAA::Alert object |
87
|
|
|
|
|
|
|
|
88
|
|
|
|
|
|
|
$alert = Weather::NOAA:Alert->new(@zones); |
89
|
|
|
|
|
|
|
$alert = Weather::NOAA:Alert->new(); |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
An array reference may be provided with the list of NOAA forecast zones |
92
|
|
|
|
|
|
|
that should be polled. If the list is not supplied then you must call |
93
|
|
|
|
|
|
|
C<$object-Ezones(@zones)> to set the zone list prior to calling |
94
|
|
|
|
|
|
|
C<$object-Epoll_events()>. |
95
|
|
|
|
|
|
|
|
96
|
|
|
|
|
|
|
=cut |
97
|
|
|
|
|
|
|
|
98
|
|
|
|
|
|
|
sub new { |
99
|
|
|
|
|
|
|
my $class = shift; |
100
|
|
|
|
|
|
|
my $self = {}; |
101
|
|
|
|
|
|
|
|
102
|
|
|
|
|
|
|
$self->{events} = {}; |
103
|
|
|
|
|
|
|
$self->{formatTime} = 0; |
104
|
|
|
|
|
|
|
$self->{formatAsterisk} = 0; |
105
|
|
|
|
|
|
|
$self->{printLog} = 0; |
106
|
|
|
|
|
|
|
$self->{printActions} = 0; |
107
|
|
|
|
|
|
|
$self->{errorLog} = 0; |
108
|
|
|
|
|
|
|
$self->{diagDump} = 0; |
109
|
|
|
|
|
|
|
$self->{diagFile} = undef; |
110
|
|
|
|
|
|
|
$self->{atomURLZone} = "http://alerts.weather.gov/cap/wwaatmget.php?x="; |
111
|
|
|
|
|
|
|
$self->{atomURLUS} = "http://alerts.weather.gov/cap/us.atom"; |
112
|
|
|
|
|
|
|
|
113
|
|
|
|
|
|
|
#http://alerts.weather.gov/cap/us.atom |
114
|
|
|
|
|
|
|
#http://alerts.weather.gov/cap/wwaatmget.php?x=TXZ104 |
115
|
|
|
|
|
|
|
#http://alerts.weather.gov/cap/wwacapget.php?x=CO20110424120100WinterStormWarning20110425120000CO.GJTWSWGJT.790bacf3c14f2dc0e2d5149cf668f95c |
116
|
|
|
|
|
|
|
|
117
|
|
|
|
|
|
|
bless( $self, $class); |
118
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
my $zoneList = shift; |
120
|
|
|
|
|
|
|
$self->zones($zoneList) if defined $zoneList; |
121
|
|
|
|
|
|
|
|
122
|
|
|
|
|
|
|
return $self; |
123
|
|
|
|
|
|
|
} |
124
|
|
|
|
|
|
|
|
125
|
|
|
|
|
|
|
=head2 B - Set the monitored zone list |
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
$object->zones([zone1, zone2, ...]); |
128
|
|
|
|
|
|
|
@zones = $object->zones(); |
129
|
|
|
|
|
|
|
|
130
|
|
|
|
|
|
|
Setting the zone list will overwrite the existing list with the |
131
|
|
|
|
|
|
|
supplied list. If called with no arguments, returns a reference |
132
|
|
|
|
|
|
|
to the current zone array. To return data for all zones use "US". |
133
|
|
|
|
|
|
|
|
134
|
|
|
|
|
|
|
=cut |
135
|
|
|
|
|
|
|
|
136
|
|
|
|
|
|
|
sub zones { $_[0]->{zones}=$_[1] if defined $_[1]; return $_[0]->{zones}; } |
137
|
|
|
|
|
|
|
|
138
|
|
|
|
|
|
|
=head2 B - get a reference to the alert object events hash |
139
|
|
|
|
|
|
|
|
140
|
|
|
|
|
|
|
%events = $object->get_events(); |
141
|
|
|
|
|
|
|
|
142
|
|
|
|
|
|
|
This is the primary output of the Weather::NOAA::Alert module. The data |
143
|
|
|
|
|
|
|
structure is the result of parsing the NOAA CAP 1.1 objects retrieved for |
144
|
|
|
|
|
|
|
the specified zones. The events hash is indexed first by zone, then by |
145
|
|
|
|
|
|
|
the CAP ID. |
146
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
Remembers past events in a data structure hash consisting of: |
148
|
|
|
|
|
|
|
C<{zone}-E{CAP id}-E{'delete'}> |
149
|
|
|
|
|
|
|
C< -E{'actionTime'}> |
150
|
|
|
|
|
|
|
C< -E{'event'}> |
151
|
|
|
|
|
|
|
C< -E{'certainty'}> |
152
|
|
|
|
|
|
|
C< -E{'senderName'}> |
153
|
|
|
|
|
|
|
C< -E{'urgency'}> |
154
|
|
|
|
|
|
|
C< -E{'instruction'}> |
155
|
|
|
|
|
|
|
C< -E{'description'}> |
156
|
|
|
|
|
|
|
C< -E{'category'}> |
157
|
|
|
|
|
|
|
C< -E{'severity'}> |
158
|
|
|
|
|
|
|
C< -E{'effective'}> |
159
|
|
|
|
|
|
|
C< -E{'headline'}> |
160
|
|
|
|
|
|
|
C< -E{'expires'}> |
161
|
|
|
|
|
|
|
|
162
|
|
|
|
|
|
|
Note that the hash keys are dynamically created from the section of the |
163
|
|
|
|
|
|
|
event. If NOAA adds, renames, or removes an XML parameter it will also |
164
|
|
|
|
|
|
|
change in the structure. In addition, nothing is currently parsed from the |
165
|
|
|
|
|
|
|
event header. There are XML parameters in the header that might be |
166
|
|
|
|
|
|
|
interesting to collect but I didn't have a use for any of that data. Future |
167
|
|
|
|
|
|
|
module revisions might include more of the NOAA data. |
168
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
=cut |
170
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
sub get_events { return $_[0]->{events}; } |
172
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
=head1 SETTINGS |
174
|
|
|
|
|
|
|
|
175
|
|
|
|
|
|
|
=head2 B - Add colons to strings that look like time stamps |
176
|
|
|
|
|
|
|
|
177
|
|
|
|
|
|
|
$object->formatTime( [1 | 0] ); |
178
|
|
|
|
|
|
|
$curr_setting = $object->formatTime(); |
179
|
|
|
|
|
|
|
|
180
|
|
|
|
|
|
|
When called without parameters will return the current setting. |
181
|
|
|
|
|
|
|
|
182
|
|
|
|
|
|
|
=cut |
183
|
|
|
|
|
|
|
|
184
|
|
|
|
|
|
|
sub formatTime { $_[0]->{formatTime}=$_[1] if defined $_[1]; return $_[0]->{formatTime}; } |
185
|
|
|
|
|
|
|
|
186
|
|
|
|
|
|
|
=head2 B - Strip asterisks from description and information tags |
187
|
|
|
|
|
|
|
|
188
|
|
|
|
|
|
|
$object->formatAsterisk( [1 | 0] ); |
189
|
|
|
|
|
|
|
$curr_setting = $object->formatAsterisk(); |
190
|
|
|
|
|
|
|
|
191
|
|
|
|
|
|
|
When called without parameters will return the current setting. |
192
|
|
|
|
|
|
|
|
193
|
|
|
|
|
|
|
=cut |
194
|
|
|
|
|
|
|
|
195
|
|
|
|
|
|
|
sub formatAsterisk { $_[0]->{formatAsterisk}=$_[1] if defined $_[1]; return $_[0]->{formatAsterisk}; } |
196
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
=head2 B - Print basic status information while retrieving cap entities |
198
|
|
|
|
|
|
|
|
199
|
|
|
|
|
|
|
$object->printLog( [1 | 0] ); |
200
|
|
|
|
|
|
|
$curr_setting = $object->printLog(); |
201
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
When called without parameters will return the current setting. |
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
=cut |
205
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
sub printLog { $_[0]->{printLog}=$_[1] if defined $_[1]; return $_[0]->{printLog}; } |
207
|
|
|
|
|
|
|
|
208
|
|
|
|
|
|
|
=head2 B - Print the cap ID for every entry added or deleted |
209
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
$object->printActions( [1 | 0] ); |
211
|
|
|
|
|
|
|
$curr_setting = $object->printActions(); |
212
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
When called without parameters will return the current setting. |
214
|
|
|
|
|
|
|
|
215
|
|
|
|
|
|
|
=cut |
216
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
sub printActions { $_[0]->{printActions}=$_[1] if defined $_[1]; return $_[0]->{printActions}; } |
218
|
|
|
|
|
|
|
|
219
|
|
|
|
|
|
|
=head2 B - Print error descriptions |
220
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
$object->errorLog( [1 | 0] ); |
222
|
|
|
|
|
|
|
$curr_setting = $object->errorLog(); |
223
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
When called without parameters will return the current setting. |
225
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
=cut |
227
|
|
|
|
|
|
|
|
228
|
|
|
|
|
|
|
sub errorLog { $_[0]->{errorLog}=$_[1] if defined $_[1]; return $_[0]->{errorLog}; } |
229
|
|
|
|
|
|
|
|
230
|
|
|
|
|
|
|
=head2 B - Save all atom content to file |
231
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
$object->diagDump( [1 | 0] ); |
233
|
|
|
|
|
|
|
$curr_setting = $object->diagDump(); |
234
|
|
|
|
|
|
|
|
235
|
|
|
|
|
|
|
Dumps all atom files received to the file diagnostics.txt in the |
236
|
|
|
|
|
|
|
current directory. When called without parameters will return |
237
|
|
|
|
|
|
|
the current setting. |
238
|
|
|
|
|
|
|
|
239
|
|
|
|
|
|
|
=cut |
240
|
|
|
|
|
|
|
|
241
|
|
|
|
|
|
|
sub diagDump { $_[0]->{diagDump}=$_[1] if defined $_[1]; return $_[0]->{diagDump}; } |
242
|
|
|
|
|
|
|
|
243
|
|
|
|
|
|
|
=head2 B - Sets the Atom URL for zones |
244
|
|
|
|
|
|
|
|
245
|
|
|
|
|
|
|
$object->atomURLZone( $url ); |
246
|
|
|
|
|
|
|
$curr_setting = $object->atomURLZone(); |
247
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
When called without parameters will return the current setting. The |
249
|
|
|
|
|
|
|
default setting is: |
250
|
|
|
|
|
|
|
|
251
|
|
|
|
|
|
|
C |
252
|
|
|
|
|
|
|
|
253
|
|
|
|
|
|
|
=cut |
254
|
|
|
|
|
|
|
|
255
|
|
|
|
|
|
|
sub atomURLZone { $_[0]->{atomURLZone}=$_[1] if defined $_[1]; return $_[0]->{atomURLZone}; } |
256
|
|
|
|
|
|
|
|
257
|
|
|
|
|
|
|
=head2 B - Sets the Atom URL when specifying zone "US" |
258
|
|
|
|
|
|
|
|
259
|
|
|
|
|
|
|
$object->atomURLUS( $url ); |
260
|
|
|
|
|
|
|
$curr_setting = $object->atomURLUS(); |
261
|
|
|
|
|
|
|
|
262
|
|
|
|
|
|
|
When called without parameters will return the current setting. The |
263
|
|
|
|
|
|
|
default setting is: |
264
|
|
|
|
|
|
|
|
265
|
|
|
|
|
|
|
C |
266
|
|
|
|
|
|
|
|
267
|
|
|
|
|
|
|
=cut |
268
|
|
|
|
|
|
|
|
269
|
|
|
|
|
|
|
sub atomURLUS { $_[0]->{atomURLUS}=$_[1] if defined $_[1]; return $_[0]->{atomURLUS}; } |
270
|
|
|
|
|
|
|
|
271
|
|
|
|
|
|
|
=head2 B - Returns the current module version |
272
|
|
|
|
|
|
|
|
273
|
|
|
|
|
|
|
my $version = Weather::NOAA::Alert->VERSION; |
274
|
|
|
|
|
|
|
|
275
|
|
|
|
|
|
|
=cut |
276
|
|
|
|
|
|
|
|
277
|
|
|
|
|
|
|
sub VERSION { return $VERSION; } |
278
|
|
|
|
|
|
|
|
279
|
|
|
|
|
|
|
=head2 B - Poll NWS Public Alerts |
280
|
|
|
|
|
|
|
|
281
|
|
|
|
|
|
|
my ($errorCount, $eventCount, $addCount, $deleteCount) |
282
|
|
|
|
|
|
|
= $object->poll_events(); |
283
|
|
|
|
|
|
|
|
284
|
|
|
|
|
|
|
Polls the National Weather Service Public Alerts and updates the events hash |
285
|
|
|
|
|
|
|
for each event. Returns an array of counts: |
286
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
=over 4 |
288
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
=item * $errorCount - The number of errors encountered while pulling the Atom files |
290
|
|
|
|
|
|
|
|
291
|
|
|
|
|
|
|
=item * $eventCount - The number of actively tracked events |
292
|
|
|
|
|
|
|
|
293
|
|
|
|
|
|
|
=item * $addCount - The number of events added on this poll |
294
|
|
|
|
|
|
|
|
295
|
|
|
|
|
|
|
=item * $deleteCount - The number of events deleted on this poll |
296
|
|
|
|
|
|
|
|
297
|
|
|
|
|
|
|
=back |
298
|
|
|
|
|
|
|
|
299
|
|
|
|
|
|
|
=cut |
300
|
|
|
|
|
|
|
|
301
|
|
|
|
|
|
|
sub poll_events { |
302
|
|
|
|
|
|
|
my ($self) = @_; |
303
|
|
|
|
|
|
|
my ($errorCount, $eventCount, $addCount, $deleteCount) = (0, 0, 0, 0); |
304
|
|
|
|
|
|
|
|
305
|
|
|
|
|
|
|
if( !defined( $self->{diagFile}) and $self->{diagDump}) { |
306
|
|
|
|
|
|
|
open($self->{diagFile}, ">>diagnostics.txt"); |
307
|
|
|
|
|
|
|
} |
308
|
|
|
|
|
|
|
|
309
|
|
|
|
|
|
|
foreach my $zone (@{$_[0]->{zones}}) { |
310
|
|
|
|
|
|
|
print "Pulling ATOM feed for zone $zone\n" if($self->{printLog}); |
311
|
|
|
|
|
|
|
my $atomTwig= new XML::Twig( |
312
|
|
|
|
|
|
|
TwigRoots => {'entry' => 1}, |
313
|
|
|
|
|
|
|
TwigHandlers => {'entry' => \&atomInfoHandler}, |
314
|
|
|
|
|
|
|
pretty_print => 'indented', |
315
|
|
|
|
|
|
|
); |
316
|
|
|
|
|
|
|
|
317
|
|
|
|
|
|
|
my $atomURL; |
318
|
|
|
|
|
|
|
if( $zone ne 'US') { |
319
|
|
|
|
|
|
|
$atomURL = $self->{atomURLZone} . $zone; |
320
|
|
|
|
|
|
|
} else { |
321
|
|
|
|
|
|
|
$atomURL = $self->{atomURLUS}; |
322
|
|
|
|
|
|
|
} |
323
|
|
|
|
|
|
|
my $atomContent; |
324
|
|
|
|
|
|
|
my $firstCAP = 1; |
325
|
|
|
|
|
|
|
if ($atomContent = get($atomURL)) { |
326
|
|
|
|
|
|
|
if( defined $self->{diagFile} and $self->{diagDump}) { |
327
|
|
|
|
|
|
|
print( {$self->{diagFile}} "Time: " . scalar(localtime())); |
328
|
|
|
|
|
|
|
print( {$self->{diagFile}} "\natomURL:: $atomURL\n"); |
329
|
|
|
|
|
|
|
print( {$self->{diagFile}} Dumper( \$atomContent) . "\n"); |
330
|
|
|
|
|
|
|
} |
331
|
|
|
|
|
|
|
|
332
|
|
|
|
|
|
|
if ($atomTwig->safe_parse($atomContent)) { |
333
|
|
|
|
|
|
|
|
334
|
|
|
|
|
|
|
#Set the delete flag for all events in this zone |
335
|
|
|
|
|
|
|
foreach my $capId (keys( %{$_[0]->{events}->{$zone}}) ) { |
336
|
|
|
|
|
|
|
$_[0]->{events}->{$zone}{$capId}{'delete'} = 1; |
337
|
|
|
|
|
|
|
} |
338
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
my @atomItems = $atomTwig->root->children; |
340
|
|
|
|
|
|
|
foreach my $atomItem (@atomItems) { |
341
|
|
|
|
|
|
|
my $capId = $atomItem->first_child('id')->text; |
342
|
|
|
|
|
|
|
|
343
|
|
|
|
|
|
|
if( $capId eq $atomURL) { |
344
|
|
|
|
|
|
|
#http://alerts.weather.gov/cap/wwaatmget.php?x=TXZ104 |
345
|
|
|
|
|
|
|
#XXX-Fragile code. How do we unambiguously determine |
346
|
|
|
|
|
|
|
#that this entry is a "null" entry? IMO, there should |
347
|
|
|
|
|
|
|
#not be an "" at all... That would tell us! |
348
|
|
|
|
|
|
|
print "There are no active watches, warnings or advisories for zone $zone\n" if($self->{printLog}); |
349
|
|
|
|
|
|
|
next; |
350
|
|
|
|
|
|
|
} elsif( $capId =~ /^http:\/\/alerts\.weather\.gov\/cap\/\w{2}\.atom/) { |
351
|
|
|
|
|
|
|
#http://alerts.weather.gov/cap/ct.atom |
352
|
|
|
|
|
|
|
#ignore; these occur when parsing the entire US zone. |
353
|
|
|
|
|
|
|
#XXX-Fragile code. How should one unambiguously |
354
|
|
|
|
|
|
|
#determine that this entry is not a real CAP event? |
355
|
|
|
|
|
|
|
next; |
356
|
|
|
|
|
|
|
} |
357
|
|
|
|
|
|
|
|
358
|
|
|
|
|
|
|
if( !exists($_[0]->{events}->{$zone}{$capId})) { |
359
|
|
|
|
|
|
|
if( $firstCAP) { |
360
|
|
|
|
|
|
|
print "Pulling new CAP entries: " if($self->{printLog} and not $self->{printActions}); |
361
|
|
|
|
|
|
|
$firstCAP = 0; |
362
|
|
|
|
|
|
|
} |
363
|
|
|
|
|
|
|
print "." if($self->{printLog} and not $self->{printActions}); |
364
|
|
|
|
|
|
|
|
365
|
|
|
|
|
|
|
if( $_[0]->retrieveCAP($zone, $capId)) { |
366
|
|
|
|
|
|
|
#XXX- Fragile code! why does NOAA make me match on |
367
|
|
|
|
|
|
|
#some random string for expired events?? |
368
|
|
|
|
|
|
|
if( $_[0]->{events}->{$zone}{$capId}{'description'} =~ |
369
|
|
|
|
|
|
|
/alert has expired/ ) { |
370
|
|
|
|
|
|
|
delete($_[0]->{events}->{$zone}{$capId}); |
371
|
|
|
|
|
|
|
print "Exp: $capId\n" if($self->{printActions}); |
372
|
|
|
|
|
|
|
} else { |
373
|
|
|
|
|
|
|
$eventCount++; |
374
|
|
|
|
|
|
|
$addCount++; |
375
|
|
|
|
|
|
|
print "Add: $capId\n" if($self->{printActions}); |
376
|
|
|
|
|
|
|
} |
377
|
|
|
|
|
|
|
} else { |
378
|
|
|
|
|
|
|
$errorCount++; |
379
|
|
|
|
|
|
|
} |
380
|
|
|
|
|
|
|
} else { |
381
|
|
|
|
|
|
|
#Still exists so reset the delete flag |
382
|
|
|
|
|
|
|
$_[0]->{events}->{$zone}{$capId}{'delete'} = 0; |
383
|
|
|
|
|
|
|
$eventCount++; |
384
|
|
|
|
|
|
|
} |
385
|
|
|
|
|
|
|
} |
386
|
|
|
|
|
|
|
|
387
|
|
|
|
|
|
|
#Delete events that were not reset |
388
|
|
|
|
|
|
|
foreach my $capId (keys( %{$_[0]->{events}->{$zone}}) ) { |
389
|
|
|
|
|
|
|
if( $_[0]->{events}->{$zone}{$capId}{'delete'}) { |
390
|
|
|
|
|
|
|
$deleteCount++; |
391
|
|
|
|
|
|
|
delete($_[0]->{events}->{$zone}{$capId}); |
392
|
|
|
|
|
|
|
print "Del: $capId\n" if($self->{printActions}); |
393
|
|
|
|
|
|
|
} |
394
|
|
|
|
|
|
|
} |
395
|
|
|
|
|
|
|
|
396
|
|
|
|
|
|
|
print "No new events" if( $firstCAP and $self->{printLog}); |
397
|
|
|
|
|
|
|
print "\n" if($self->{printLog}); |
398
|
|
|
|
|
|
|
|
399
|
|
|
|
|
|
|
} else { |
400
|
|
|
|
|
|
|
print "Error parsing ATOM file for zone $zone :: $0\n" if($self->{errorLog}); |
401
|
|
|
|
|
|
|
print $atomContent . "\n" if($self->{errorLog}); |
402
|
|
|
|
|
|
|
$errorCount++; |
403
|
|
|
|
|
|
|
} #if ($twig->safe_parse($atomContent)) |
404
|
|
|
|
|
|
|
} else { |
405
|
|
|
|
|
|
|
print "Failed to retrieve ATOM file for zone $zone\n" if($self->{errorLog}); |
406
|
|
|
|
|
|
|
$errorCount++; |
407
|
|
|
|
|
|
|
} #if ($atomContent = get($atomURL)) |
408
|
|
|
|
|
|
|
} #foreach my $zone (@zones) |
409
|
|
|
|
|
|
|
|
410
|
|
|
|
|
|
|
if( defined( $self->{diagFile}) and $self->{diagDump}) { |
411
|
|
|
|
|
|
|
close( $self->{diagFile}); |
412
|
|
|
|
|
|
|
$self->{diagFile} = undef; |
413
|
|
|
|
|
|
|
} |
414
|
|
|
|
|
|
|
|
415
|
|
|
|
|
|
|
return( $errorCount, $eventCount, $addCount, $deleteCount); |
416
|
|
|
|
|
|
|
} |
417
|
|
|
|
|
|
|
|
418
|
|
|
|
|
|
|
sub retrieveCAP { |
419
|
|
|
|
|
|
|
my ($self, $zone, $capId) = @_; |
420
|
|
|
|
|
|
|
|
421
|
|
|
|
|
|
|
my $capTwig= new XML::Twig( |
422
|
|
|
|
|
|
|
TwigRoots => {'info' => 1}, |
423
|
|
|
|
|
|
|
# TwigHandlers => {'info' => \&capInfoHandler}, |
424
|
|
|
|
|
|
|
TwigHandlers => {'info' => sub { capInfoHandler( $self->{formatTime}, $self->{formatAsterisk}, @_) } }, |
425
|
|
|
|
|
|
|
pretty_print => 'indented', |
426
|
|
|
|
|
|
|
); |
427
|
|
|
|
|
|
|
|
428
|
|
|
|
|
|
|
my $capContent; |
429
|
|
|
|
|
|
|
if ($capContent = get($capId)) { |
430
|
|
|
|
|
|
|
|
431
|
|
|
|
|
|
|
if( defined( $self->{diagFile}) and $self->{diagDump}) { |
432
|
|
|
|
|
|
|
print( {$self->{diagFile}} "capId:: $capId\n"); |
433
|
|
|
|
|
|
|
print( {$self->{diagFile}} Dumper( \$capContent) . "\n"); |
434
|
|
|
|
|
|
|
} |
435
|
|
|
|
|
|
|
|
436
|
|
|
|
|
|
|
if ($capTwig->safe_parse($capContent)) { |
437
|
|
|
|
|
|
|
|
438
|
|
|
|
|
|
|
#Parse only the first enclosure |
439
|
|
|
|
|
|
|
#Loop through it appending items to the event hash |
440
|
|
|
|
|
|
|
foreach my $child ($capTwig->root->first_child->children) { |
441
|
|
|
|
|
|
|
#ignore nested items: eventCode, parameter, area; they're too hard :^) |
442
|
|
|
|
|
|
|
if($child->tag ne 'eventCode' and $child->tag ne 'parameter' and $child->tag ne 'area') { |
443
|
|
|
|
|
|
|
$self->{events}->{$zone}{$capId}{$child->tag} = $child->text; |
444
|
|
|
|
|
|
|
} |
445
|
|
|
|
|
|
|
} |
446
|
|
|
|
|
|
|
$self->{events}->{$zone}{$capId}{'delete'} = 0; |
447
|
|
|
|
|
|
|
|
448
|
|
|
|
|
|
|
} else { |
449
|
|
|
|
|
|
|
print "Error parsing CAP file for event $capId :: $0\n" if($self->{errorLog}); |
450
|
|
|
|
|
|
|
print $capContent . "\n" if($self->{errorLog}); |
451
|
|
|
|
|
|
|
return 0; |
452
|
|
|
|
|
|
|
} |
453
|
|
|
|
|
|
|
} else { |
454
|
|
|
|
|
|
|
print "Failed to retrieve CAP file for event $capId\n" if($self->{errorLog}); |
455
|
|
|
|
|
|
|
return 0; |
456
|
|
|
|
|
|
|
} |
457
|
|
|
|
|
|
|
|
458
|
|
|
|
|
|
|
return 1; #No errors encountered |
459
|
|
|
|
|
|
|
} |
460
|
|
|
|
|
|
|
|
461
|
|
|
|
|
|
|
|
462
|
|
|
|
|
|
|
|
463
|
|
|
|
|
|
|
sub atomInfoHandler { |
464
|
|
|
|
|
|
|
my ($twig, $atomInfo) = @_; |
465
|
|
|
|
|
|
|
atomFormatTags($atomInfo); |
466
|
|
|
|
|
|
|
} |
467
|
|
|
|
|
|
|
|
468
|
|
|
|
|
|
|
sub capInfoHandler { |
469
|
|
|
|
|
|
|
my ($twig, $formatTime, $formatAsterisk, $capInfo) = @_; |
470
|
|
|
|
|
|
|
capFormatTags($formatTime, $formatAsterisk, $capInfo); |
471
|
|
|
|
|
|
|
} |
472
|
|
|
|
|
|
|
|
473
|
|
|
|
|
|
|
sub atomFormatTags { |
474
|
|
|
|
|
|
|
my ($atomInfo) = @_; |
475
|
|
|
|
|
|
|
|
476
|
|
|
|
|
|
|
# my @children = $atomInfo->children; |
477
|
|
|
|
|
|
|
# foreach my $child (@children) { |
478
|
|
|
|
|
|
|
# if ($child->tag eq '') { |
479
|
|
|
|
|
|
|
# my $childText = $child->text; |
480
|
|
|
|
|
|
|
# $childText =~ s/^\n//; |
481
|
|
|
|
|
|
|
# $childText =~ s/\n$//; |
482
|
|
|
|
|
|
|
# $child->set_text($childText); |
483
|
|
|
|
|
|
|
|
484
|
|
|
|
|
|
|
# #Insert new tags into the document |
485
|
|
|
|
|
|
|
# my $elt= new XML::Twig::Elt( 'parsedHeadline', $parsedHeadline); |
486
|
|
|
|
|
|
|
# $elt->paste( 'last_child', $capInfo); |
487
|
|
|
|
|
|
|
# } |
488
|
|
|
|
|
|
|
# } |
489
|
|
|
|
|
|
|
} |
490
|
|
|
|
|
|
|
|
491
|
|
|
|
|
|
|
sub capFormatTags { |
492
|
|
|
|
|
|
|
my ($formatTime, $formatAsterisk, $capInfo) = @_; |
493
|
|
|
|
|
|
|
|
494
|
|
|
|
|
|
|
#Format some of the fields. Need to remove newlines from every field |
495
|
|
|
|
|
|
|
#except the description and instruction; they need other adjustments. |
496
|
|
|
|
|
|
|
my @children = $capInfo->children; |
497
|
|
|
|
|
|
|
foreach my $child (@children) { |
498
|
|
|
|
|
|
|
my $childText = $child->text; |
499
|
|
|
|
|
|
|
if ($child->tag ne 'description' and $child->tag ne 'instruction') { |
500
|
|
|
|
|
|
|
#Adjust the formatting a little. Why would a CAP file |
501
|
|
|
|
|
|
|
#need to contain newline formatting? |
502
|
|
|
|
|
|
|
$childText =~ s/^\n//; |
503
|
|
|
|
|
|
|
$childText =~ s/\n/ /g; |
504
|
|
|
|
|
|
|
$childText =~ s/^\s+//; #remove leading spaces |
505
|
|
|
|
|
|
|
$childText =~ s/\s+$//; #remove trailing spaces |
506
|
|
|
|
|
|
|
} else { |
507
|
|
|
|
|
|
|
if( $formatTime) { |
508
|
|
|
|
|
|
|
# if( 1 ) { |
509
|
|
|
|
|
|
|
#Try to add colons to all the time fields. This allows |
510
|
|
|
|
|
|
|
#the MS SAPI engine to correctly pronounce the time |
511
|
|
|
|
|
|
|
$childText =~ s/(\d{1,2}?)(\d{2})\s{1}(AM|PM)\s{1}[A-Z]{3}/$1:$2 $3/g; |
512
|
|
|
|
|
|
|
} |
513
|
|
|
|
|
|
|
if( $formatAsterisk) { |
514
|
|
|
|
|
|
|
# if( 1 ) { |
515
|
|
|
|
|
|
|
#Remove any "*" because it sounds real funny when SAPI |
516
|
|
|
|
|
|
|
#pronounces "asterisk" in the middle of the speech stream |
517
|
|
|
|
|
|
|
$childText =~ s/\*/ /g; |
518
|
|
|
|
|
|
|
} |
519
|
|
|
|
|
|
|
} |
520
|
|
|
|
|
|
|
$child->set_text($childText); |
521
|
|
|
|
|
|
|
} |
522
|
|
|
|
|
|
|
} |
523
|
|
|
|
|
|
|
|
524
|
|
|
|
|
|
|
=head1 NOAA CAP CHALLENGES |
525
|
|
|
|
|
|
|
|
526
|
|
|
|
|
|
|
The following items represent fragile parts of the code caused by ambiguous |
527
|
|
|
|
|
|
|
data in the National Weather Service CAP feeds. These items should be |
528
|
|
|
|
|
|
|
addressed by NWS by publishing a document that clearly states the behavior |
529
|
|
|
|
|
|
|
for each of these conditions or by generally adding more XML enclosures |
530
|
|
|
|
|
|
|
especially replacing the long list of counties that can occur. |
531
|
|
|
|
|
|
|
|
532
|
|
|
|
|
|
|
=over 4 |
533
|
|
|
|
|
|
|
|
534
|
|
|
|
|
|
|
=item * B |
535
|
|
|
|
|
|
|
|
536
|
|
|
|
|
|
|
Expired alerts contain a valid CeventEE/eventE> section |
537
|
|
|
|
|
|
|
with CdescriptionEalert has expiredE/descriptionE>. |
538
|
|
|
|
|
|
|
There seems to be little else in the document that can be used to |
539
|
|
|
|
|
|
|
determine that the event is basically empty and expired. One option |
540
|
|
|
|
|
|
|
might be to key on a null CexpiresEE/expiresE> section |
541
|
|
|
|
|
|
|
but nothing is guaranteed since this behavior is not documented by the |
542
|
|
|
|
|
|
|
national weather service as far as I can tell. |
543
|
|
|
|
|
|
|
|
544
|
|
|
|
|
|
|
=item * B |
545
|
|
|
|
|
|
|
|
546
|
|
|
|
|
|
|
If there are no notifications for a county or zone, the CAP document |
547
|
|
|
|
|
|
|
will contain a valid CeventEE/eventE> section where |
548
|
|
|
|
|
|
|
the CidEE/idE> is the same as the CAP URL (i.e. points |
549
|
|
|
|
|
|
|
to its self). Again there is little else in the document that can be |
550
|
|
|
|
|
|
|
used to unambiguously determine that there are no events for that zone |
551
|
|
|
|
|
|
|
or county. |
552
|
|
|
|
|
|
|
|
553
|
|
|
|
|
|
|
=item * B |
554
|
|
|
|
|
|
|
|
555
|
|
|
|
|
|
|
Events can contain a long list of counties. For anyone trying |
556
|
|
|
|
|
|
|
to perform text to speech or even SMS delivery of the events |
557
|
|
|
|
|
|
|
the text can be too long. Ideally the county list would be in |
558
|
|
|
|
|
|
|
other XML enclosures so that XSL files could still display pages |
559
|
|
|
|
|
|
|
correctly, but other uses would not need to consider the, potentially |
560
|
|
|
|
|
|
|
long list of counties. |
561
|
|
|
|
|
|
|
|
562
|
|
|
|
|
|
|
=back |
563
|
|
|
|
|
|
|
|
564
|
|
|
|
|
|
|
=head1 TODO |
565
|
|
|
|
|
|
|
|
566
|
|
|
|
|
|
|
=over 4 |
567
|
|
|
|
|
|
|
|
568
|
|
|
|
|
|
|
=item * B |
569
|
|
|
|
|
|
|
|
570
|
|
|
|
|
|
|
Events contain a CpolygonEE/polygonE> that could be |
571
|
|
|
|
|
|
|
used to determine if a specific latitude / longitude is in the event |
572
|
|
|
|
|
|
|
area. This would greatly reduce the number of alerts for a specific |
573
|
|
|
|
|
|
|
point in a county since the NWS has recently started issuing and |
574
|
|
|
|
|
|
|
expiring events for specific areas that are not bound by county. |
575
|
|
|
|
|
|
|
Tracking at the county level can trigger several events over a short |
576
|
|
|
|
|
|
|
period as a storm progresses through the county. |
577
|
|
|
|
|
|
|
|
578
|
|
|
|
|
|
|
=back |
579
|
|
|
|
|
|
|
|
580
|
|
|
|
|
|
|
=head1 SEE ALSO |
581
|
|
|
|
|
|
|
|
582
|
|
|
|
|
|
|
=over 4 |
583
|
|
|
|
|
|
|
|
584
|
|
|
|
|
|
|
=item * L - Primary NOAA National Weather Service |
585
|
|
|
|
|
|
|
page for Public Alerts. Has a full list of all county and forecast zone IDs. |
586
|
|
|
|
|
|
|
|
587
|
|
|
|
|
|
|
=item * L - List of possible |
588
|
|
|
|
|
|
|
data that is populated in the field |
589
|
|
|
|
|
|
|
|
590
|
|
|
|
|
|
|
=back |
591
|
|
|
|
|
|
|
|
592
|
|
|
|
|
|
|
=head1 AUTHOR |
593
|
|
|
|
|
|
|
|
594
|
|
|
|
|
|
|
Michael Stovenour, C<< >> |
595
|
|
|
|
|
|
|
|
596
|
|
|
|
|
|
|
=head1 BUGS |
597
|
|
|
|
|
|
|
|
598
|
|
|
|
|
|
|
Please report any bugs or feature requests to C, or through |
599
|
|
|
|
|
|
|
the web interface at L. I will be notified, and then you'll |
600
|
|
|
|
|
|
|
automatically be notified of progress on your bug as I make changes. |
601
|
|
|
|
|
|
|
|
602
|
|
|
|
|
|
|
|
603
|
|
|
|
|
|
|
=head1 SUPPORT |
604
|
|
|
|
|
|
|
|
605
|
|
|
|
|
|
|
You can find documentation for this module with the perldoc command. |
606
|
|
|
|
|
|
|
|
607
|
|
|
|
|
|
|
perldoc Weather::NOAA::Alert |
608
|
|
|
|
|
|
|
|
609
|
|
|
|
|
|
|
|
610
|
|
|
|
|
|
|
You can also look for information at: |
611
|
|
|
|
|
|
|
|
612
|
|
|
|
|
|
|
=over 4 |
613
|
|
|
|
|
|
|
|
614
|
|
|
|
|
|
|
=item * RT: CPAN's request tracker (report bugs here) |
615
|
|
|
|
|
|
|
|
616
|
|
|
|
|
|
|
L |
617
|
|
|
|
|
|
|
|
618
|
|
|
|
|
|
|
=item * AnnoCPAN: Annotated CPAN documentation |
619
|
|
|
|
|
|
|
|
620
|
|
|
|
|
|
|
L |
621
|
|
|
|
|
|
|
|
622
|
|
|
|
|
|
|
=item * CPAN Ratings |
623
|
|
|
|
|
|
|
|
624
|
|
|
|
|
|
|
L |
625
|
|
|
|
|
|
|
|
626
|
|
|
|
|
|
|
=item * Search CPAN |
627
|
|
|
|
|
|
|
|
628
|
|
|
|
|
|
|
L |
629
|
|
|
|
|
|
|
|
630
|
|
|
|
|
|
|
=back |
631
|
|
|
|
|
|
|
|
632
|
|
|
|
|
|
|
=head1 LICENSE AND COPYRIGHT |
633
|
|
|
|
|
|
|
|
634
|
|
|
|
|
|
|
Copyright 2011 Michael Stovenour. |
635
|
|
|
|
|
|
|
|
636
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify it |
637
|
|
|
|
|
|
|
under the terms of either: the GNU General Public License as published |
638
|
|
|
|
|
|
|
by the Free Software Foundation; or the Artistic License. |
639
|
|
|
|
|
|
|
|
640
|
|
|
|
|
|
|
See http://dev.perl.org/licenses/ for more information. |
641
|
|
|
|
|
|
|
|
642
|
|
|
|
|
|
|
|
643
|
|
|
|
|
|
|
=cut |
644
|
|
|
|
|
|
|
|
645
|
|
|
|
|
|
|
1; # End of Weather::NOAA::Alert |