File Coverage

blib/lib/Weather/WeatherKit.pm
Criterion Covered Total %
statement 63 63 100.0
branch 14 16 87.5
condition 20 26 76.9
subroutine 13 13 100.0
pod 4 4 100.0
total 114 122 93.4


line stmt bran cond sub pod time code
1             package Weather::WeatherKit;
2              
3 3     3   713603 use 5.008;
  3         11  
4 3     3   20 use strict;
  3         4  
  3         159  
5 3     3   21 use warnings;
  3         8  
  3         164  
6              
7 3     3   16 use Carp;
  3         5  
  3         263  
8 3     3   2328 use Crypt::JWT qw(encode_jwt);
  3         214782  
  3         267  
9              
10 3     3   328 use parent 'Weather::API::Base';
  3         235  
  3         26  
11 3     3   76117 use Weather::API::Base qw(:all);
  3         16  
  3         3964  
12              
13             =head1 NAME
14              
15             Weather::WeatherKit - Apple WeatherKit REST API client
16              
17             =cut
18              
19             our $VERSION = '0.12';
20              
21             =head1 SYNOPSIS
22              
23             use Weather::WeatherKit;
24              
25             my $wk = Weather::WeatherKit->new(
26             team_id => $apple_team_id, # Apple Developer Team Id
27             service_id => $weatherkit_service_id, # WeatherKit Service Id
28             key_id => $key_id, # WeatherKit developer key ID
29             key => $private_key # Encrypted private key (PEM)
30             );
31            
32             # Request current weather:
33             my $report = $wk->get(
34             lat => $lat, # Latitude
35             lon => $lon, # Longitude
36             dataSets => 'currentWeather'
37             );
38              
39             # Request forecast for 8 days, use Weather::API::Base helper functions
40             # for ISO dates, and get full HTTP::Response object to check for success
41             use Weather::API::Base qw(:all);
42              
43             my $response = $wk->get_response(
44             lat => $lat,
45             lon => $lon,
46             dataSets => 'forecastHourly',
47             hourlyStart => ts_to_iso_date(time()),
48             hourlyEnd => ts_to_iso_date(time()+8*24*3600)
49             );
50              
51             if ($response->is_success) {
52             my $json = $response->decoded_content;
53             } else {
54             die $response->status_line;
55             }
56              
57             =head1 DESCRIPTION
58              
59             Weather::WeatherKit provides basic access to the Apple WeatherKit REST API (v1).
60             WeatherKit replaces the Dark Sky API and requires an Apple developer subscription.
61              
62             Pease see the L
63             for datasets and usage options as well as the L.
64              
65             It was made to serve the apps L and
66             L, but if your service
67             requires some extra functionality, feel free to contact the author about it.
68              
69             =head1 CONSTRUCTOR
70              
71             =head2 C
72              
73             my $wk = Weather::WeatherKit->new(
74             team_id => "MLU84X58U4",
75             service_id => "com.domain.myweatherapp",
76             key_id => $key_id,
77             key => $private_key?,
78             key_file => $private_key_pem?,
79             language => $lang_code?,
80             timeout => $timeout_sec?,
81             expiration => $expire_secs?,
82             ua => $lwp_ua?,
83             curl => $use_curl?
84             );
85            
86             Required parameters:
87              
88             =over 4
89              
90             =item * C : Your 10-character Apple developer Team Id - it can be located
91             on the Apple developer portal.
92              
93             =item * C : The WeatherKit Service Identifier created on the Apple
94             developer portal. Usually a reverse-domain type string is used for this.
95              
96             =item * C : The ID of the WeatherKit key created on the Apple developer portal.
97              
98             =item * C : The encrypted WeatherKit private key file that you created on
99             the Apple developer portal. On the portal you download a PKCS8 format file (.p8),
100             which you first need to convert to the PEM format. On a Mac you can convert it simply:
101              
102             openssl pkcs8 -nocrypt -in AuthKey_.p8 -out AuthKey_.pem
103              
104             =item * C : Instead of the C<.pem> file, you can pass its contents directly
105             as a string. If both are provided C takes precedence over C.
106              
107             =back
108              
109             Optional parameters:
110              
111             =over 4
112              
113             =item * C : Language code. Default: C.
114              
115             =item * C : Timeout for requests in secs. Default: C<30>.
116              
117             =item * C : Pass your own L to customise the agent string etc.
118              
119             =item * C : If true, fall back to using the C command line program.
120             This is useful if you have issues adding http support to L, which
121             is the default method for the WeatherKit requests. It assumes the C program
122             is installed in C<$PATH>.
123              
124             =item * C : Token expiration time in seconds. Tokens are cached until
125             there are less than 10 minutes left to expiration. Default: C<7200>.
126              
127             =back
128              
129             =head1 METHODS
130              
131             =head2 C
132              
133             my $report = $wk->get(
134             lat => $lat,
135             lon => $lon,
136             dataSets => $datasets
137             %args?
138             );
139              
140             my %report = $wk->get( ... );
141              
142             Fetches datasets (weather report, forecast, alert...) for the requested location.
143             Returns a string containing the JSON data, except in array context, in which case,
144             as a convenience, it will use L to decode it directly to a Perl hash.
145              
146             Requires L, unless the C option was set.
147              
148             If the request is not successful, it will C throwing the C<< HTTP::Response->status_line >>.
149              
150             =over 4
151            
152             =item * C : Latitude (-90 to 90).
153              
154             =item * C : Longitude (-18 to 180).
155              
156             =item * C : A comma-separated string of the dataset(s) you request. Example
157             supported data sets: C.
158             Some data sets might not be available for all locations. Will return empty results
159             if parameter is missing.
160              
161             =item * C<%args> : See the official API documentation for the supported weather API
162             query parameters which you can pass as key/value pairs.
163              
164             =back
165              
166             =head2 C
167              
168             my $response = $wk->get_response(
169             lat => $lat,
170             lon => $lon,
171             dataSets => $datasets
172             %args?
173             );
174              
175             Same as C except it returns the full L from the API (so you
176             can handle bad requests yourself).
177              
178             =head1 CONVENIENCE METHODS
179              
180             =head2 C
181              
182             my $jwt = $wk->jwt(
183             iat => $iat?,
184             exp => $exp?
185             );
186              
187             Returns the JSON Web Token string in case you need it. Will return a cached one
188             if it has more than 10 minutes until expiration and you don't explicitly pass an
189             C argument.
190              
191             =over 4
192            
193             =item * C : Specify the token creation timestamp. Default is C.
194              
195             =item * C : Specify the token expiration timestamp. Passing this parameter
196             will force the creation of a new token. Default is C (or what you
197             specified in the constructor).
198              
199             =back
200              
201             =head1 HELPER FUNCTIONS (from Weather::API::Base)
202              
203             The parent class L contains some useful functions e.g.:
204              
205             use Weather::API::Base qw(:all);
206              
207             # Get time in ISO (YYYY-MM-DDTHH:mm:ss) format
208             my $datetime = ts_to_iso_date(time());
209              
210             # Convert 30 degrees Celsius to Fahrenheit
211             my $result = convert_units('C', 'F', 30);
212              
213             See the doc for that module for more details.
214              
215             =head1 KNOWN ISSUES
216              
217             =head2 400 errors on 10 day forecast
218              
219             Although WeatherKit is supposed to provide 10 days of forecast, at some point users
220             started getting C<400> errors when requesting (e.g. with C) more than 8 or 9
221             days of forecast. If you encounter this issue, limit your forecast request to 9 or
222             8 days in the future.
223              
224             =head1 OTHER PERL WEATHER MODULES
225              
226             Some Perl modules for current weather and forecasts from other sources:
227              
228             =head2 L
229              
230             OpenWeatherMap uses various weather sources combined with their own ML and offers
231             a couple of free endpoints (the v2.5 current weather and 5d/3h forecast) with generous
232             request limits. Their newer One Call 3.0 API also offers some free usage (1000 calls/day)
233             and the cost is per call above that. If you want access to history APIs, extended
234             hourly forecasts etc, there are monthly subscriptions. L is from the
235             same author as this module and similar in use.
236              
237             =head2 L
238              
239             The 7Timer! weather forecast is completely free and would be of extra interest if
240             you are interested in astronomy/stargazing. It uses the standard NOAA forecast,
241             but also calculates astronomical seeing and transparency. It can be accessed with
242             L, which is another module similar to this (same author).
243              
244             =cut
245              
246             sub new {
247 10     10 1 581136 my ($class, %args) = @_;
248             croak("10 digit team_id expected.")
249 10 100 100     441 unless $args{team_id} && length($args{team_id}) == 10;
250              
251 8         64 my $self = $class->SUPER::new(
252             language => 'en_US',
253             error => 'die',
254             %args
255             );
256              
257             ($self->{$_} = $args{$_} || croak("$_ required."))
258 8   66     938 for qw/service_id key_id/;
259              
260 6 100       21 unless ($args{key}) {
261 3 100       177 croak("key or key_file required.") unless $args{key_file};
262 2 100       138 open my $fh, '<', $args{key_file} or die "Can't open file $args{key_file}: $!";
263 1         2 $args{key} = do {local $/; <$fh>};
  1         5  
  1         32  
264             }
265              
266 4         15 $self->{key} = \$args{key};
267 4         15 $self->{team_id} = $args{team_id};
268 4   100     25 $self->{expiration} = $args{expiration} || 7200;
269              
270 4         71 return $self;
271             }
272              
273             sub get {
274 8     8 1 9138 my $self = shift;
275 8         29 my %args = @_;
276 8         35 my $resp = $self->get_response(%args);
277              
278 3         400 return $self->_get_output($resp, wantarray);
279             }
280              
281             sub get_response {
282 8     8 1 19 my $self = shift;
283 8         22 my %args = @_;
284 8   66     52 $args{language} ||= $self->{language};
285              
286 8         40 Weather::API::Base::_verify_lat_lon(\%args);
287              
288 4 50 66     74 $self->_ua unless $self->{ua} || $self->{curl};
289              
290 4         7444 return _fetch($self->{ua}, _weather_url(%args), $self->jwt);
291             }
292              
293             sub jwt {
294 9     9 1 7724 my $self = shift;
295 9         23 my %args = @_;
296              
297             # Return cached one
298             return $self->{jwt}
299 9 100 100     90 if !$args{exp} && $self->{jwt_exp} && $self->{jwt_exp} >= time() + 600;
      66        
300              
301 5   66     30 $args{iat} ||= time();
302 5   66     25 $self->{jwt_exp} = $args{exp} || (time() + $self->{expiration});
303              
304             my $data = {
305             iss => $self->{team_id},
306             sub => $self->{service_id},
307             exp => $self->{jwt_exp},
308             iat => $args{iat}
309 5         34 };
310              
311             $self->{jwt} = encode_jwt(
312             payload => $data,
313             alg => 'ES256',
314             key => $self->{key},
315             extra_headers => {
316             kid => $self->{key_id},
317 5         55 id => "$self->{team_id}.$self->{service_id}",
318             typ => "JWT"
319             }
320             );
321              
322 4         47702 return $self->{jwt};
323             }
324              
325             sub _fetch {
326 3     3   9 my ($ua, $url, $jwt) = @_;
327              
328             return
329 3 50       14 `curl "$url" -A "Curl Weather::WeatherKit/$VERSION" -s -H 'Authorization: Bearer $jwt'`
330             unless $ua;
331              
332 3         27 return $ua->get($url, Authorization => "Bearer $jwt");
333             }
334              
335             sub _weather_url {
336 6     6   2639 my %args = @_;
337 6         13 my $url =
338             "https://weatherkit.apple.com/api/v1/weather/{language}/{lat}/{lon}";
339              
340 6         159 $url =~ s/{$_}/delete $args{$_}/e foreach qw/language lat lon/;
  18         198  
341              
342 6         20 my $params = join("&", map {"$_=$args{$_}"} keys %args);
  1         5  
343              
344 6 100       17 $url .= "?$params" if $params;
345              
346 6         45 return $url;
347             }
348              
349             =head1 AUTHOR
350              
351             Dimitrios Kechagias, C<< >>
352              
353             =head1 BUGS
354              
355             Please report any bugs or feature requests either on L (preferred), or on RT (via the email
356             C or L).
357              
358             I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
359              
360             =head1 GIT
361              
362             L
363              
364             =head1 LICENSE AND COPYRIGHT
365              
366             This software is copyright (c) 2023 by Dimitrios Kechagias.
367              
368             This is free software; you can redistribute it and/or modify it under
369             the same terms as the Perl 5 programming language system itself.
370              
371             =cut
372              
373             1;