File Coverage

blib/lib/Bot/Cobalt/Plugin/Weather.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package Bot::Cobalt::Plugin::Weather;
2             $Bot::Cobalt::Plugin::Weather::VERSION = '0.003001';
3 1     1   22041 use List::Objects::WithUtils;
  1         3509  
  1         6  
4              
5 1     1   527804 use Bot::Cobalt;
  1         3145  
  1         6  
6 1     1   1701 use Bot::Cobalt::Common;
  1         473865  
  1         7  
7              
8 1     1   9120 use POE;
  1         48001  
  1         7  
9 1     1   75289 use POEx::Weather::OpenWeatherMap;
  1         1385626  
  1         53  
10              
11 1     1   545 use Object::RateLimiter;
  0            
  0            
12              
13             use PerlX::Maybe;
14              
15             sub new { bless +{ location => +{} }, shift }
16              
17             sub _limiter { $_[0]->{_limiter} }
18              
19             sub pwx { $_[1] ? $_[0]->{pwx} = $_[1] : $_[0]->{pwx} }
20              
21             sub Cobalt_register {
22             my ($self, $core) = splice @_, 0, 2;
23              
24             POE::Session->create(
25             object_states => [
26             $self => [ qw/
27             _start
28              
29             pwx_error
30             pwx_weather
31             pwx_forecast
32             / ],
33             ],
34             );
35              
36             register( $self, SERVER =>
37             'public_cmd_wx',
38             'wx_timer_expire_item',
39             );
40              
41             logger->info("Loaded: wx");
42              
43             PLUGIN_EAT_NONE
44             }
45              
46             sub Cobalt_unregister {
47             my ($self, $core) = splice @_, 0, 2;
48             logger->info("Shutting down POEx::Weather::OpenWeatherMap ...");
49             $core->timer_del_alias( $core->get_plugin_alias($self) );
50             $self->pwx->stop if $self->pwx;
51             logger->info("wx unloaded");
52             PLUGIN_EAT_NONE
53             }
54              
55             sub _start {
56             my $self = $_[OBJECT];
57              
58             my $pcfg = core->get_plugin_cfg($self);
59              
60             my $api_key = $pcfg->{API_Key} || $pcfg->{APIKey};
61             unless (defined $api_key) {
62             logger->error($_) for
63             "No 'API_Key' found in plugin configuration!",
64             "Requests will likely fail."
65             }
66              
67              
68             my $do_cache = $pcfg->{DisableCache} ? 0 : 1;
69             my $cache_expiry = $pcfg->{CacheExpiry};
70             my $cache_dir = $pcfg->{CacheDir};
71              
72             my $ratelimit = $pcfg->{RateLimit} || 60;
73             $self->{_limiter} = Object::RateLimiter->new(
74             seconds => 60, events => $ratelimit
75             );
76              
77             $self->pwx(
78             POEx::Weather::OpenWeatherMap->new(
79             event_prefix => 'pwx_',
80             cache => $do_cache,
81              
82             maybe api_key => $api_key,
83             maybe cache_expiry => $cache_expiry,
84             maybe cache_dir => $cache_dir,
85             )
86             );
87              
88             $self->pwx->start;
89             }
90              
91             sub pwx_error {
92             my $self = $_[OBJECT];
93             my $err = $_[ARG0];
94             my $tag = $err->request->tag;
95              
96             logger->warn("POEx::Weather::OpenWeatherMap failure: $err");
97             broadcast(
98             message => $tag->context => $tag->channel =>
99             "wx: error: $err"
100             );
101             }
102              
103             sub pwx_weather {
104             my $self = $_[OBJECT];
105             my $res = $_[ARG0];
106             my $tag = $res->request->tag;
107              
108             my $place = $res->name;
109              
110             my $tempf = $res->temp_f;
111             my $tempc = $res->temp_c;
112             my $humid = $res->humidity;
113              
114             my $wind = $res->wind_speed_mph;
115             my $gust = $res->wind_gust_mph;
116             my $winddir = $res->wind_direction;
117              
118             my $terse = $res->conditions_terse;
119             my $verbose = $res->conditions_verbose;
120              
121             my $hms = $res->dt->hms;
122              
123             my $str = "$place at ${hms}UTC: ${tempf}F/${tempc}C";
124             $str .= " and ${humid}% humidity;";
125             $str .= " wind is ${wind}mph $winddir";
126             $str .= " gusting to ${gust}mph" if $gust;
127             $str .= ". Current conditions: ${terse}: $verbose";
128              
129             broadcast( message => $tag->context => $tag->channel => $str );
130             }
131              
132             sub pwx_forecast {
133             my $self = $_[OBJECT];
134             my $res = $_[ARG0];
135             my $tag = $res->request->tag;
136             my $place = $res->name;
137              
138             broadcast(
139             message => $tag->context => $tag->channel =>
140             "Forecast for $place ->"
141             );
142              
143             if ($res->hourly) {
144             for my $hr ($res->as_array->sliced(1..3)->all) {
145             my $str = $self->_hourly_forecast_str($hr);
146             broadcast( message => $tag->context => $tag->channel => $str );
147             }
148             } else {
149             my $itr = $res->iter;
150             while (my $day = $itr->()) {
151             my $str = $self->_daily_forecast_str($day);
152             broadcast( message => $tag->context => $tag->channel => $str );
153             }
154             }
155             }
156              
157             sub _hourly_forecast_str {
158             my ($self, $hr) = @_;
159             my $date = $hr->dt->hms;
160             my $temp = $hr->temp;
161             my $temp_c = $hr->temp_c;
162              
163             my $terse = $hr->conditions_terse;
164             my $verbose = $hr->conditions_verbose;
165              
166             my $wind = $hr->wind_speed_mph;
167             my $winddir = $hr->wind_direction;
168              
169             my $rain = $hr->rain;
170             my $snow = $hr->snow;
171              
172             "${date} UTC: ${temp}F/${temp_c}C"
173             . ", wind $winddir at ${wind}mph"
174             . ", ${terse}: $verbose"
175             . ($rain ? ", rain ${rain}mm" : '')
176             . ($snow ? ", snow ${snow}mm" : '')
177             }
178              
179             sub _daily_forecast_str {
180             my ($self, $day) = @_;
181             my $date = $day->dt->day_name;
182              
183             my $temp_hi_f = $day->temp_max_f;
184             my $temp_lo_f = $day->temp_min_f;
185             my $temp_hi_c = $day->temp_max_c;
186             my $temp_lo_c = $day->temp_min_c;
187              
188             my $terse = $day->conditions_terse;
189             my $verbose = $day->conditions_verbose;
190              
191             my $wind = $day->wind_speed_mph;
192             my $winddir = $day->wind_direction;
193              
194             "${date}: High of ${temp_hi_f}F/${temp_hi_c}C"
195             . ", low of ${temp_lo_f}F/${temp_lo_c}C"
196             . ", wind $winddir at ${wind}mph"
197             . "; $terse: $verbose"
198             }
199              
200             sub Bot_wx_timer_expire_item {
201             my ($self, $core) = splice @_, 0, 2;
202             my $context = ${ $_[0] };
203             my $nick = ${ $_[1] };
204             return unless exists $self->{location}->{$context};
205             delete $self->{location}->{$context}->{$nick};
206             delete $self->{location}->{$context}
207             unless keys %{ $self->{location}->{$context} };
208             }
209              
210             sub Bot_public_cmd_wx {
211             my ($self, $core) = splice @_, 0, 2;
212             my $msg = ${ $_[0] };
213             my $context = $msg->context;
214              
215             my $loc = $self->{location};
216             my $lower = lc_irc $msg->src_nick;
217              
218             my ($location, $fcast, $hourly);
219             my @parts = @{ $msg->message_array };
220              
221             if ( ($parts[0] || '') eq 'forecast' ) {
222             $fcast++;
223             shift @parts;
224             } elsif ( ($parts[0] || '' ) eq 'hourly' ) {
225             $hourly++;
226             shift @parts;
227             }
228              
229             if (!@parts) {
230             if ($loc->{ $context } && $loc->{ $context }->{ $lower }) {
231             $location = $loc->{ $context }->{ $lower };
232             } else {
233             broadcast( message => $context => $msg->channel =>
234             $msg->src_nick . ': no location specified'
235             );
236             return PLUGIN_EAT_NONE
237             }
238             } else {
239             $location = join ' ', @parts;
240             $loc->{ $context }->{ $lower } = $location;
241             $core->timer_set( 180,
242             +{
243             Event => 'wx_timer_expire_item',
244             Args => [ $context, $lower ],
245             Alias => $core->get_plugin_alias($self),
246             }
247             );
248             }
249              
250             if ($self->_limiter->delay) {
251             broadcast( message => $msg->context => $msg->channel =>
252             "Weather is currently rate-limited; wait a minute and try again."
253             );
254             return PLUGIN_EAT_NONE
255             }
256              
257             my $tag = hash(
258             context => $msg->context,
259             channel => $msg->channel,
260             )->inflate;
261              
262             $self->pwx->get_weather(
263             location => $location,
264             tag => $tag,
265             ( $fcast ? (forecast => 1, days => 3) : () ),
266             ( $hourly ? (forecast => 1, hourly => 1, days => 1) : () ),
267             );
268              
269             PLUGIN_EAT_NONE
270             }
271              
272             1;
273              
274             =pod
275              
276             =head1 NAME
277              
278             Bot::Cobalt::Plugin::Weather - Weather retrieval plugin for Bot::Cobalt
279              
280             =head1 SYNOPSIS
281              
282             # In your plugins.conf:
283             WX:
284             Module: Bot::Cobalt::Plugin::Weather
285             Opts:
286             API_Key: "my OpenWeatherMap API key here"
287             # OpenWeatherMap's free tier allows 60 requests per minute (default):
288             RateLimit: 60
289             # Caching is on by default:
290             DisableCache: 0
291             # Defaults are probably fine:
292             CacheExpiry: 1200
293             CacheDir: ~
294              
295             # On IRC:
296             > !wx Boston, MA
297             > !wx forecast Toronto, Canada
298             # Previous queries remembered per-user for three minutes:
299             > !wx hourly
300              
301             =head1 DESCRIPTION
302              
303             A weather conditions/forecast retrieval plugin for L<Bot::Cobalt>.
304              
305             Uses L<http://www.openweathermap.org/> via L<POEx::Weather::OpenWeatherMap> /
306             L<Weather::OpenWeatherMap> for retrieval.
307              
308             =head1 AUTHOR
309              
310             Jon Portnoy <avenj@cobaltirc.org>
311              
312             =cut