File Coverage

blib/lib/Weather/OWM.pm
Criterion Covered Total %
statement 135 135 100.0
branch 77 80 96.2
condition 57 59 96.6
subroutine 25 25 100.0
pod 11 11 100.0
total 305 310 98.3


line stmt bran cond sub pod time code
1             package Weather::OWM;
2              
3 3     3   642462 use 5.008;
  3         12  
4 3     3   28 use strict;
  3         26  
  3         102  
5 3     3   15 use warnings;
  3         6  
  3         240  
6              
7 3     3   20 use Carp;
  3         5  
  3         259  
8              
9 3     3   589 use parent 'Weather::API::Base';
  3         362  
  3         61  
10 3     3   104235 use Weather::API::Base qw(:all);
  3         6  
  3         9767  
11              
12             =head1 NAME
13              
14             Weather::OWM - Perl client for the OpenWeatherMap (OWM) API
15              
16             =cut
17              
18             our $VERSION = '0.2';
19              
20             =head1 SYNOPSIS
21              
22             use Weather::OWM;
23             use v5.10;
24              
25             my $owm = Weather::OWM->new(key => 'Your API key');
26              
27             ### Using the free endpoints of the "classic" Weather API 2.5:
28            
29             # Get current weather for the Stonehenge area using coordinates...
30             my %re = $owm->get_weather(lat => 51.18, lon => -1.83);
31              
32             # ...and print temperature, humidity, wind speed
33             say "$re{main}->{temp}C $re{main}->{humidity}% $re{wind}->{speed}m/s"
34             unless $re{error};
35              
36             # Get 3h/5d forecast for London, UK...
37             %re = $owm->get_weather(product => 'forecast', loc => 'London,UK');
38              
39             # ...and print the temperature for every three hours over the next 5 days
40             say scalar(localtime($_->{dt}))." $_->{main}->{temp}C" for @{$re{list}};
41              
42             ### Using the newer One Call 3.0 API:
43              
44             # Get current weather and min/h/day forecast for Punxsutawney, PA...
45             %re = $owm->one_call(city => 'Punxsutawney,PA,US', units => 'imperial');
46              
47             # ...print current temperature, humidity, wind speed...
48             say "$re{current}->{temp}F $re{current}->{humidity}% $re{current}->{wind_speed}mph"
49             unless $re{error};
50            
51             # ...and print the temperature for every hour over the next 2 days
52             say "$_->{temp}F" for @{$re{hourly}};
53              
54             ### Using the History API 2.5:
55              
56             # Get the historical weather for the first 72 hours of 2023 at Greenwich, UK...
57             my %report = $owm->get_history(
58             product => 'hourly',
59             loc => 'Greenwich,UK',
60             start => '2023-01-01 00:00:00',
61             cnt => '72'
62             );
63              
64             # ...and print the temperatures next to the date/time
65             say scalar(localtime($_->{dt}))." $_->{main}->{temp}C" for @{$re{list}};
66              
67             ### Using the Geocoder API:
68              
69             # Fetch Portland, Maine from the Geocoder API...
70             my @locations = $owm->geo(city => 'Portland,ME,US');
71              
72             # ...and print the latitude,longitude
73             say "$locations[0]->{lat},$locations[0]->{lon}";
74              
75             # Get the top 5 cities named "Portland" in the US...
76             @locations = $owm->geo(city => 'Portland,US', limit=>5);
77              
78             # ...and print their state and coordinates.
79             say "$_->{state} $_->{lat},$_->{lon}" for @locations;
80              
81             # Perform reverse geocoding of coordinates 51.51 North, 0.12 West...
82             @locations = $owm->geo(lat=>51.51, lon=>-0.12);
83              
84             # ...and print the location name, country
85             say "$locations[0]->{name}, $locations[0]->{country}";
86              
87             =head1 DESCRIPTION
88              
89             L is a lightweight Perl client supporting most OpenWeatherMap (OWM) APIs,
90             including the latest One Call v3.0.
91              
92             There is an easy-to-use object oriented interface that can return the data in hashes.
93             There are virtually no dependencies, except L for the requests, and
94             optionally L or L if you want to decode JSON (most common) or XML data.
95              
96             Current OWM API support:
97              
98             =over 4
99              
100             =item * OneCall API 3.0 for current weather, forecast and weather history.
101              
102             =item * Weather API 2.5 including free (current weather, 3h/5d forecast) and paid forecasts.
103              
104             =item * Historical APIs 2.5 (history, statistical, accumulated).
105              
106             =item * Geocoding API 1.0 for direct/reverse geocoding.
107              
108             =back
109              
110             Please see L for extensive
111             documentation. Note that even the free APIs require L
112             for an API key.
113              
114             This module belongs to a family of weather modules (along with L
115             and L) created to serve the apps L
116             and L, but if your
117             service requires some extra functionality, feel free to contact the author about adding it.
118              
119             =head1 CONSTRUCTOR
120              
121             =head2 C
122              
123             my $owm = Weather::OWM->new(
124             key => $api_key, #required
125             timeout => $timeout_sec?,
126             agent => $user_agent_string?,
127             ua => $lwp_ua?,
128             lang => $lang?,
129             units => $units?,
130             error => $die_or_return?,
131             debug => $debug?,
132             scheme => $url_scheme?
133             );
134              
135             Creates a Weather::OWM object.
136              
137             Required parameters:
138              
139             =over 4
140              
141             =item * C : The API key is required for both free and paid APIs. For the former,
142             you can L for a free account.
143              
144             =back
145              
146             Optional parameters:
147              
148             =over 4
149              
150             =item * C : Timeout for requests in secs. Default: C<30>.
151              
152             =item * C : Customize the user agent string.
153              
154             =item * C : Pass your own L to customise further. Will override C.
155              
156             =item * C : Set language (two letter language code) for requests. You can override per API call. Default: C.
157              
158             =item * C : Set units (standard, metric, imperial). You can override per API call. Default: C. Available options:
159              
160             =over 4
161              
162             =item * C : Temperature in Kelvin. Wind speed in metres/sec.
163              
164             =item * C : Temperature in Celsius. Wind speed in metres/sec.
165              
166             =item * C : Temperature in Fahrenheit. Wind speed in mph.
167              
168             =back
169              
170             =item * C : If there is an error response with the main methods, you have the options to C or C it. You can override per API call. Default: C.
171              
172             =item * C : If debug mode is enabled, API URLs accessed are printed in STDERR. Default: C.
173              
174             =item * C : You can use C as an option if you have trouble building https support for LWP in your system. Default: C.
175              
176             =back
177              
178             =head1 MAIN METHODS
179              
180             The main methods will return a string containing the JSON (or XML etc where specified),
181             except in the array context (C<< my %hash = $owm->method >>), where L (or similar)
182             will be used to conveniently decode it to a Perl hash.
183              
184             If the request is not successful, by default an C will be returned
185             in scalar context or C<(error => HTTP::Response)> in array context.
186             If the constructor was set with an C equal to C, then it will die throwing
187             the C<< HTTP::Response->status_line >>.
188              
189             For custom error handling, see the alternative methods.
190              
191             =head2 C
192              
193             One Call 3.0 API
194              
195             my $report = $owm->one_call(
196             product => $product, # Can be: forecast, historical, daily
197             lat => $lat, # Required unless city/zip specified
198             lon => $lon, # Required unless city/zip specified
199             city => $city?, # City,country (via Geocoder API)
200             zip => $zip?, # Zip/postcode,country (via Geocoder API)
201             date => $date?, # Date or unix timestamp: required for daily/historical
202             );
203              
204             my %report = $owm->one_call( ... );
205              
206             Fetches a One Call API v3.0 response for the desired location. The One Call API
207             offers 3 products, which differ in some options as listed below. C, C
208             and C options specified in the constructor can be overridden on each call.
209              
210             For an explanation to the returned data, refer to the L.
211              
212             Parameters common to all products:
213              
214             =over 4
215              
216              
217             =item * C : Latitude (-90 to 90). South is negative.
218              
219             =item * C : Longitude (-180 to 180). West is negative.
220              
221             Latitude/Longitude are normally required.
222             As a convenience, you can specify a city name or a zip/post code instead:
223              
224             =item * C : Expects C or C,
225             where C is ISO 3166.
226              
227             =item * C : Expects C, where C is ISO 3166.
228              
229             Note that to avoid issues with ambiguity of city names etc you can use the
230             Geocoder API manually.
231              
232             =back
233              
234             =head4 One Call API products
235              
236             Three call types/products to use listed below, along with any custom parameters they support.
237             If no product is specified, C is used.
238              
239             =over 4
240              
241             =item * C : B: Provides a minute forecast for 1 hour, hourly for 48 hours and daily for 8 days.
242             Optional parameter:
243              
244             =over 4
245              
246             =item * C : Exclude data from the API response (to reduce size). It expects a comma-delimited list with any combination of the possible values:
247             C,C,C,C,C
248              
249             =back
250              
251             =item * C : B: 40+ year historical archive and 4 days ahead forecast.
252             Required parameter:
253              
254             =over 4
255              
256             =item * C : Unix timestamp for start of the data. Data is available from 1979-01-01.
257             For convenience, C can be specified instead in iso format C
258             for your local time (or C for UTC).
259              
260             =back
261              
262             =item * C : B: 40+ year weather archive and 1.5 years ahead forecast.
263             Required parameter:
264              
265             =over 4
266              
267             =item * C : Date of request in the format C.
268             For convenience, the timestamp/date formats of the C product can be used (will be truncated to just the plain date).
269              
270             =back
271              
272             =back
273              
274             =head2 C
275              
276             Weather API 2.5
277              
278             my $report = $owm->get_weather(
279             product => $product, # Can be: current, forecast, hourly, daily, climate
280             lat => $lat, # Required unless loc/zip/city_id specified
281             lon => $lon, # Required unless loc/zip/city_id specified
282             loc => $location?, # Named location (deprecated)
283             zip => $zip?, # Zip/postcode (deprecated)
284             city_id => $city_id?, # city_id (deprecated)
285             mode => $mode?, # output mode - default: json
286             );
287              
288             my %report = $owm->get_weather( ... );
289              
290             Fetches a weather API v2.5 response for the desired location. The weather API has
291             several products available (some free, others requiring paid subscription), some have
292             special arguments as listed below. C, C and C options specified
293             in the constructor can be overridden on each call.
294              
295             For an explanation to the returned data, refer to the L
296             or see below in the products list the links for each endpoint.
297              
298             Parameters common to all products:
299              
300             =over 4
301            
302             =item * C : Latitude (-90 to 90). South is negative. Required unless loc/zip/city_id specified.
303              
304             =item * C : Longitude (-180 to 180). West is negative. Required unless loc/zip/city_id specified.
305              
306             =item * C : Deprecated (lat/lon recommended - see Geocoder API). Location given either as a C
307             or C or C.
308              
309             =item * C : Deprecated (lat/lon recommended - see Geocoder API). Expects C (US) or C.
310              
311             =item * C : Deprecated (lat/lon recommended - see Geocoder API). City id from list L.
312              
313             =item * C : Output mode. Default is json C, C is the supported alternative (unless otherwise specified).
314              
315             =back
316              
317             =head4 API products
318              
319             There are several API endpoints which are selected via C (two of them accessible with a free key).
320             They are listed below, along with any custom parameters they support. If no product is provided, C is used.
321              
322             =over 4
323              
324             =item * C : B (free product). For response details see L.
325              
326             =over 4
327              
328             =item * C : C and C are supported as alternatives. (Optional)
329              
330             =back
331              
332             =item * C : B<5 Day / 3 Hour Forecast> (free product). For response details see L.
333              
334             =over 4
335              
336             =item * C : Limit the number of timestamps returned. (Optional)
337              
338             =back
339              
340             =item * C : B. For response details see L.
341              
342             =over 4
343              
344             =item * C : Limit the number of timestamps returned. (Optional)
345              
346             =back
347              
348             =item * C : B. For response details see L.
349              
350             =over 4
351              
352             =item * C : Number of days (1 to 16) to be returned. (Optional)
353              
354             =back
355              
356             =item * C : B. For response details see L.
357              
358             =over 4
359              
360             =item * C : Number of days (1 to 30) to be returned. (Optional)
361              
362             =back
363              
364             =back
365              
366             =head2 C
367              
368             History API 2.5
369              
370             my $report = $owm->get_history(
371             product => $product, # Can be: hourly, year, month, day, temp, precip
372             lat => $lat, # Required unless loc/zip/city_id specified
373             lon => $lon, # Required unless loc/zip/city_id specified
374             loc => $location?, # Named location (deprecated)
375             zip => $zip?, # Zip/postcode (deprecated)
376             city_id => $city_id?, # city_id (deprecated)
377             );
378              
379             my %report = $owm->get_history( ... );
380              
381             Fetches a historical weather API v2.5 response for the desired location. The weather API has
382             several products available, some have special arguments as listed below. C, C
383             and C options specified in the constructor can be overridden on each call.
384              
385             For an explanation to the returned data, refer to the L
386             or see below in the products list the links for each endpoint.
387              
388             Parameters common to all products:
389              
390             =over 4
391            
392             =item * C : Latitude (-90 to 90). South is negative. Required unless loc/zip/city_id specified.
393              
394             =item * C : Longitude (-180 to 180). West is negative. Required unless loc/zip/city_id specified.
395              
396             =item * C : Deprecated (lat/lon recommended - see Geocoder API). Location given either as a C
397             or C or C.
398              
399             =item * C : Deprecated (lat/lon recommended - see Geocoder API). Expects C (US) or C.
400              
401             =item * C : Deprecated (lat/lon recommended - see Geocoder API). City id from list L.
402              
403             =back
404              
405             =head4 API products
406              
407             There are several API endpoints which are selected via C.
408             They are listed below, along with any custom parameters they support.
409             If none is specified, C is used.
410              
411             =over 4
412              
413             =item * C : B. For response details see L.
414             Parameters:
415              
416             =over 4
417              
418             =item * C : (required) Start date. Unix timestamp (or iso date).
419              
420             =item * C : (required unless C specified) End date. Unix timestamp (or iso date).
421              
422             =item * C : (required unless C specified) Number of timestamps returned (used instead of C).
423              
424             =back
425              
426             =item * C : B. Returns statistical climate indicators for the entire year. For response details see L.
427              
428             =item * C : B. Returns statistical climate indicators for a specific month of the year. For response details see L.
429             Parameters:
430              
431             =over 4
432              
433             =item * C : (required) Specify the month (1-12) for which to return statistical climate data.
434              
435             =back
436              
437             =item * C : B. Returns statistical climate indicators for a specific month of the year. For response details see L.
438             Parameters:
439              
440             =over 4
441              
442             =item * C : (required) Specify the month (1-12) for which to return statistical climate data.
443              
444             =item * C : (required) Specify the day (1-31) of the month for which to return statistical climate data.
445              
446             =back
447              
448             =item * C : B: The sum, counted in degrees (Kelvin), by which the actual air temperature rises above or falls below a threshold level during the chosen time period. For response details see L.
449             Parameters:
450              
451             =over 4
452              
453             =item * C : (required) Start date. Unix timestamp (or iso date).
454              
455             =item * C : (required) End date. Unix timestamp (or iso date).
456              
457             =item * C : All values smaller than indicated value are not taken into account.
458              
459             =back
460              
461             =item * C : B: The sum, counted in millimetres, of daily precipitation during the chosen time period. For response details see L.
462             Parameters:
463              
464             =over 4
465              
466             =item * C : (required) Start date. Unix timestamp (or iso date).
467              
468             =item * C : (required) End date. Unix timestamp (or iso date).
469              
470             =back
471              
472             =back
473              
474             =head2 C
475              
476             Geocoding API 1.0
477              
478             # Direct geocoding
479              
480             my $locations = $owm->geo(
481             city => $city?, # City,country. Required if zip not specified.
482             zip => $zip?, # Zip/postcode,country. Required if city not specified.
483             limit => $limit # Limit number of results.
484             );
485              
486             my @locations = $owm->geo( ... );
487              
488             my ($lat, $lon) = ($locations[0]->{lat},$locations[0]->{lon});
489              
490             # Reverse geocoding
491              
492             my $locations = $owm->geo(
493             lat => $lat, # Latitude.
494             lon => $lon, # Longitude
495             limit => $limit # Limit number of results.
496             );
497              
498             my @locations = $owm->geo( ... );
499              
500             my ($name, $country) = ($locations[0]->{name},$locations[0]->{country});
501              
502             # Checking for error with default error handling behaviour
503             warn "Error" if @locations && $locations[0] eq 'error';
504             warn "No results" if !@locations;
505              
506             Will return a list of named locations with their central coordinates (lat/lon) that
507             match the request. The request can include either city or zip/postcode (geocoding),
508             or latitude/longitude (reverse geocoding).
509              
510             All the OWM APIs work with coordinates, which are unambiguous. As a convenience,
511             the 2.5 API accepted city names or zip codes. This is now deprecated and you are
512             advised to use the geocoding to get the latitude/longitude of the desired location.
513             The Weather::OWM C method also accepts city or zip as a convenience,
514             the top result of from the Geocoding API is used. You may want to use this API
515             directly yourself as well to verify the location is as intended.
516              
517             For an explanation to the returned data, refer to the L.
518              
519             Due to the data returned being an array, for the default error mode (C),
520             on error a size-2 array will be returned: C<('error', HTTP::Response)>. Alternatives
521             are using the C function, or passing an C'die'> parameter
522             and using C.
523              
524             Common parameters:
525              
526             =over 4
527              
528             =item * C : Limit the number of location results from 1 to 5. Currently, the API
529             default seems to be set to 1. Note that both direct and reverse geocoding can produce
530             more than one result (either different cities with the same name, of a location belonging
531             to different administrative units (e.g. city vs local municipality).
532              
533             =back
534              
535             Geocoding parameters:
536              
537             =over 4
538              
539             =item * C : Expects C, C or C,
540             where C is ISO 3166. If the C is skipped, the result
541             may be ambiguous if there are similarly named/sized cities in different countries.
542              
543             =item * C : Expects C, where C is ISO 3166.
544              
545             =back
546              
547             Reverse geocoding parameters:
548              
549             =over 4
550              
551             =item * C : Latitude.
552              
553             =item * C : Longitude.
554              
555             =back
556              
557             =head1 ALTERNATIVE METHODS
558              
559             The main methods handle the HTTP response errors with a C that throws the status line.
560             There are alternative methods you can use that work exactly the same, except you
561             get the full L object from the API endpoint, so that you can do
562             the error handling yourself.
563              
564             =head2 C
565              
566             my $response = $owm->one_call_response(
567             %args
568             );
569              
570             Alternative to C (same parameters).
571              
572             =head2 C
573              
574             my $response = $owm->get_weather_response(
575             %args
576             );
577              
578             Alternative to C (same parameters).
579              
580             =head2 C
581              
582             my $response = $owm->get_history_response(
583             %args
584             );
585              
586             Alternative to C (same parameters).
587              
588             =head2 C
589              
590             my $response = $owm->geo_response(
591             %args
592             );
593              
594             Alternative to C (same parameters).
595              
596             =head1 HELPER METHODS
597              
598             =head2 C
599              
600             my $url = $owm->icon_url($icon, $small?);
601              
602             The APIs may return an C key which corresponds to a specific icon. The URL
603             to the 100x100 icon (png with transparent background) is provided by this function,
604             unless you pass C<$small> in which case you get the URL to the 50x50 icon.
605              
606             =head2 C
607              
608             my $data = $owm->icon_data($icon, $small?);
609              
610             Similar to L above, but downloads the png data (undef on error).
611              
612             =head1 HELPER FUNCTIONS FROM Weather::API::Base
613              
614             The parent class L contains some useful functions:
615              
616             use Weather::API::Base qw(:all);
617              
618             # Get time in YYYY-MM-DD HH:mm:ss format, local time zone from a unix timestamp
619             my $datetime = ts_to_date(time());
620              
621             # Convert 30 degrees Celsius to Fahrenheit
622             my $result = convert_units('C', 'F', 30);
623              
624             See the doc for that module for more details.
625              
626             =cut
627              
628             my $geocache;
629              
630             sub new {
631 10     10 1 495221 my ($class, %args) = @_;
632              
633 10 100       277 croak("key required ") unless $args{key};
634              
635 9         61 my $self = $class->SUPER::new(lang => 'en', %args);
636 8         534 $self->{key} = $args{key};
637              
638 8         29 return $self;
639             }
640              
641             sub one_call {
642 13     13 1 11438 my $self = shift;
643 13         53 return $self->_get(wantarray, 'one_call', @_);
644             }
645              
646             sub get_weather {
647 7     7 1 86209 my $self = shift;
648 7         31 return $self->_get(wantarray, 'weather', @_);
649             }
650              
651             sub get_history {
652 10     10 1 7697 my $self = shift;
653 10         36 return $self->_get(wantarray, 'history', @_);
654             }
655              
656             sub geo {
657 10     10 1 3983 my $self = shift;
658 10         32 return $self->_get(wantarray, 'geo', @_);
659             }
660              
661             sub one_call_response {
662 14     14 1 2371 my $self = shift;
663 14         40 my %args = $self->_preprocess_params('one_call', @_);
664              
665 14 100 100     82 $args{product} = '' if !$args{product} || $args{product} eq 'forecast';
666              
667             croak("product has to be 'forecast', 'historical' or 'daily'")
668 14 100 100     238 if $args{product} && $args{product} ne 'historical' && $args{product} ne 'daily';
      100        
669              
670 13         45 $self->_geocode(\%args);
671              
672 11         43 Weather::API::Base::_verify_lat_lon(\%args);
673              
674 9 100       108 if ($args{product}) { # Not forecast
675             croak("date expected")
676 7 100       295 unless $args{date};
677              
678 6 100       26 if ($args{product} eq 'daily') {
    50          
679             $args{date} = ts_to_date($args{date})
680 3 100       27 if $args{date} =~ /^\d+$/;
681              
682 3 100       79 if ($args{date} =~ /^(\d{4}-\d{2}-\d{2})/) {
683 2         25 $args{date} = $1;
684             } else {
685 1         192 croak("date expected in the format YYYY-MM-DD");
686             }
687             croak("date of at least 1979-01-02 expected")
688 2 100       231 unless $args{date} ge "1979-01-02";
689             } elsif ($args{product} eq 'historical') {
690             $args{date} = datetime_to_ts($args{date})
691 3 100       31 unless $args{date} =~ /^\d+$/;
692              
693             croak("dt / date of at least 1979-01-01 expected")
694 2 100       284 unless $args{date} >= 283999530;
695 1         3 $args{dt} = delete $args{date};
696             }
697             }
698              
699 4         15 return $self->_get_ua($self->_onecall_url(%args));
700             }
701              
702             sub get_weather_response {
703 8     8 1 4413 my $self = shift;
704 8         47 my %args = $self->_preprocess_params('weather', @_);
705              
706             Weather::API::Base::_verify_lat_lon(\%args)
707 8 100 100     62 unless $args{q} || $args{zip} || $args{city_id};
      100        
708              
709 8         49 return $self->_get_ua($self->_weather_url(%args));
710             }
711              
712             sub get_history_response {
713 11     11 1 2938 my $self = shift;
714 11         28 my %args = $self->_preprocess_params('history', @_);
715              
716 11   100     47 $args{product} ||= 'hourly';
717              
718             Weather::API::Base::_verify_lat_lon(\%args)
719 11 100 100     98 unless $args{q} || $args{zip} || $args{city_id};
      100        
720              
721 11         129 my %req = (
722             hourly => ['start'],
723             month => ['month'],
724             day => [qw/month day/],
725             temp => [qw/start end/],
726             precip => [qw/start end/],
727             );
728              
729 11         24 my $req = $req{$args{product}};
730              
731 11         30 foreach (@$req) {
732 12 100       261 croak("$_ is expected") unless $args{$_};
733             }
734             croak("end or cnt is expected")
735 10 100 100     249 if $args{product} eq 'hourly' && !$args{end} && !$args{cnt};
      100        
736              
737 9         21 foreach (qw/start end/) {
738 18 100       274 next unless $args{$_};
739             $args{$_} = datetime_to_ts($args{$_})
740 8 100       57 unless $args{$_} =~ /^\d+$/;
741             }
742              
743 9         66 return $self->_get_ua($self->_history_url(%args));
744             }
745              
746             sub geo_response {
747 10     10 1 15 my $self = shift;
748 10         23 my %args = $self->_preprocess_params('geo', @_);
749              
750 10 100 100     70 if (defined $args{lat} && defined $args{lon}) {
751 1         5 Weather::API::Base::_verify_lat_lon(\%args);
752             } else {
753             croak("either both lat & lon, or either of city or zip parameters expected")
754 9 100 100     672 unless $args{city} || $args{zip};
755             }
756              
757 7         34 return $self->_get_ua($self->_geo_url(%args));
758             }
759              
760             sub icon_url {
761 5     5 1 2441 my $self = shift;
762 5         6 my $icon = shift;
763 5         6 my $sm = shift;
764              
765 5 100       14 return unless $icon;
766              
767 3 100       8 $icon .= '@2x' unless $sm;
768 3         13 return $self->{scheme} . "://openweathermap.org/img/wn/$icon.png";
769             }
770              
771             sub icon_data {
772 2     2 1 1516 my $self = shift;
773 2         6 my $url = $self->icon_url(@_);
774              
775 2 100       6 if ($url) {
776 1         21 my $res = $self->_get_ua($url);
777 1 50       579 return $res->content if $res->is_success;
778             }
779              
780 1         5 return;
781             }
782              
783             sub _preprocess_params {
784 43     43   61 my $self = shift;
785 43         66 my $api = shift;
786 43         104 my %args = @_;
787              
788 43         109 $args{q} = delete($args{loc});
789 43 100 100     165 if ($api eq 'one_call' || $api eq 'weather') {
790 22   66     165 $args{$_} //= $self->{$_} for qw/lang units/;
791 22 100       109 delete $args{units} if $args{units} eq 'standard';
792 22 100 100     105 delete $args{lang} if $args{lang} && $args{lang} eq 'en';
793             }
794 43         128 $args{appid} = $self->{key};
795              
796 43         282 return %args;
797             }
798              
799             sub _get {
800 40     40   88 my $self = shift;
801 40         74 my $wantarray = shift;
802 40         87 my $api = shift;
803 40         180 my %args = @_;
804              
805 40   100     216 $self->{output} = $args{mode} || 'json';
806              
807 40 100       227 my $resp =
    100          
    100          
808             $api eq 'one_call' ? $self->one_call_response(%args)
809             : $api eq 'weather' ? $self->get_weather_response(%args)
810             : $api eq 'history' ? $self->get_history_response(%args)
811             : $self->geo_response(%args);
812              
813 23         25107 return $self->_get_output($resp, $wantarray);
814             }
815              
816             sub _geocode {
817 13     13   22 my $self = shift;
818 13         20 my $args = shift;
819 13 100 100     50 return if defined $args->{lat} && defined $args->{lon};
820              
821 7         35 my @location = $self->geo(city => $args->{city}, zip => $args->{zip}, limit => 1);
822 5 50       18735 croak("requested location not found") unless @location;
823 5         36 $args->{$_} = $location[0]->{$_} for qw/lat lon/;
824             }
825              
826             sub _weather_url {
827 8     8   13 my $self = shift;
828 8         27 my %args = @_;
829 8   100     43 my $prod = delete $args{product} || 'current';
830 8         50 my %products = (
831             current => 'weather',
832             forecast => 'forecast',
833             hourly => 'forecast/hourly',
834             daily => 'forecast/daily',
835             climate => 'forecast/climate',
836             );
837 8         12 my $sub = 'api';
838 8 100 100     33 $sub = 'pro' if $prod eq 'hourly' || $prod eq 'climate';
839             croak('valid products: '.join(", ", keys %products))
840 8 100       221 unless $products{$prod};
841              
842 7         29 return "$sub.openweathermap.org/data/2.5/$products{$prod}?" . _join_args(\%args);
843             }
844              
845             sub _history_url {
846 9     9   12 my $self = shift;
847 9         54 my %args = @_;
848 9         18 my $prod = delete $args{product};
849 9         40 my %products = (
850             hourly => 'history/city',
851             year => 'aggregated/year',
852             month => 'aggregated/month',
853             day => 'aggregated/day',
854             temp => 'history/accumulated_temperature',
855             precip => 'history/accumulated_precipitation',
856             );
857              
858 9 100       23 $args{type} = 'hour' if $prod eq 'hourly';
859              
860             croak('valid products: '.join(", ", keys %products))
861 9 100       261 unless $products{$prod};
862              
863 8         25 return "history.openweathermap.org/data/2.5/$products{$prod}?" . _join_args(\%args);
864             }
865              
866             sub _onecall_url {
867 4     4   5 my $self = shift;
868 4         13 my %args = @_;
869 4   100     14 my $prod = delete $args{product} || '';
870 4 100       10 $prod = '/timemachine' if $prod eq 'historical';
871 4 100       9 $prod = '/day_summary' if $prod eq 'daily';
872              
873 4         10 return "api.openweathermap.org/data/3.0/onecall$prod?" . _join_args(\%args);
874             }
875              
876             sub _geo_url {
877 7     7   14 my $self = shift;
878 7         24 my %args = @_;
879              
880             return "api.openweathermap.org/geo/1.0/reverse?" . _join_args(\%args)
881 7 100 66     29 if defined $args{lat} && defined $args{lon};
882              
883 6         13 $args{q} = delete $args{city};
884 6         72 return "api.openweathermap.org/geo/1.0/direct?" . _join_args(\%args);
885             }
886              
887             sub _join_args {
888 26     26   40 my $args = shift;
889 26 100       104 return join "&", map {defined $args->{$_} ? "$_=$args->{$_}" : ()} keys %$args;
  113         646  
890             }
891              
892             =head1 PERL WEATHER MODULES
893              
894             A quick listing of Perl modules for current weather and forecasts from various sources:
895              
896             =head2 OpenWeatherMap
897              
898             OpenWeatherMap uses various weather sources combined with their own ML and offers
899             a couple of free endpoints (the v2.5 current weather and 5d/3h forecast) with generous
900             request limits. Their newer One Call 3.0 API also offers some free usage (1000 calls/day)
901             and the cost is per call above that. If you want access to history APIs, extended
902             hourly forecasts etc, there are monthly subscriptions. If you want to access an API
903             that is missing from L, feel free to ask the author.
904              
905             Note that there is an older L module, but it is no longer
906             maintained and only supports the old (v2.5) Weather API. I looked into updating it
907             for my purposes, but it was more complex (returns objects, so a new object definition
908             is required per API endpoint added etc) and with more dependencies (including L),
909             than what I wanted for such a module.
910              
911             =head2 Apple WeatherKit
912              
913             An alternative source for multi-source forecasts is Apple's WeatherKit (based on
914             the old Dark Sky weather API). It offers 500k calls/day for free, but requires a
915             paid Apple developer account. You can use L, which is very
916             similar to this module (same author).
917              
918             =head2 7Timer!
919              
920             If you are interested in astronomy/stargazing the 7Timer! weather forecast might be
921             useful. It uses the standard NOAA forecast, but calculates astronomical seeing and
922             transparency. It is completely free and can be accessed with L,
923             which is another very similar to this module (same author).
924              
925             =head2 YR.no
926              
927             Finally, the Norwegian Meteorological Institute offers the free YR.no service, which
928             can be accessed via L, although I am not affiliated and have not tested
929             that module.
930              
931             =head1 AUTHOR
932              
933             Dimitrios Kechagias, C<< >>
934              
935             =head1 BUGS
936              
937             Please report any bugs or feature requests on L.
938              
939             =head1 GIT
940              
941             L
942              
943             =head1 LICENSE AND COPYRIGHT
944              
945             This software is copyright (c) 2024 by Dimitrios Kechagias.
946              
947             This is free software; you can redistribute it and/or modify it under
948             the same terms as the Perl 5 programming language system itself.
949              
950             =cut
951              
952             1;