File Coverage

blib/lib/Tesla/API.pm
Criterion Covered Total %
statement 194 299 64.8
branch 52 100 52.0
condition 17 32 53.1
subroutine 39 45 86.6
pod 11 11 100.0
total 313 487 64.2


line stmt bran cond sub pod time code
1             package Tesla::API;
2              
3 20     20   165815 use warnings;
  20         149  
  20         497  
4 20     20   77 use strict;
  20         27  
  20         361  
5              
6 20     20   1318 use Carp qw(croak confess);
  20         1206  
  20         1166  
7 20     20   5775 use Data::Dumper;
  20         67456  
  20         1068  
8 20     20   7539 use Digest::SHA qw(sha256_hex);
  20         46043  
  20         1363  
9 20     20   6871 use FindBin qw($RealBin);
  20         16633  
  20         1759  
10 20     20   7449 use File::Copy;
  20         69968  
  20         1110  
11 20     20   7542 use File::HomeDir;
  20         82760  
  20         1132  
12 20     20   5851 use File::Share qw(:all);
  20         409372  
  20         2588  
13 20     20   7769 use HTTP::Request;
  20         256294  
  20         585  
14 20     20   6154 use JSON;
  20         92685  
  20         113  
15 20     20   8638 use MIME::Base64 qw(encode_base64url);
  20         9307  
  20         1048  
16 20     20   11192 use WWW::Mechanize;
  20         2044010  
  20         783  
17 20     20   159 use URI;
  20         36  
  20         938  
18              
19             our $VERSION = '1.00';
20              
21             $| = 1;
22              
23             my $home_dir;
24              
25             # The %api_cache hash is a cache for Tesla API call data across all objects.
26             # It is managed by the _api_cache() private method. Each cache slot contains the
27             # time() that it was stored, and will time out after
28             # API_CACHE_TIMEOUT_SECONDS/api_cache_time()
29              
30             my %api_cache;
31              
32             BEGIN {
33 20     20   215 $home_dir = File::HomeDir->my_home;
34             }
35              
36             use constant {
37             DEBUG_CACHE => $ENV{DEBUG_TESLA_API_CACHE},
38 20         128 API_CACHE_PERSIST => 0,
39             API_CACHE_TIMEOUT_SECONDS => 2,
40             API_TIMEOUT_RETRIES => 3,
41             AUTH_CACHE_FILE => "$home_dir/tesla_auth_cache.json",
42             ENDPOINTS_FILE => dist_file('Tesla-API', 'endpoints.json'),
43             OPTION_CODES_FILE => dist_file('Tesla-API', 'option_codes.json'),
44             TOKEN_EXPIRY_WINDOW => 5,
45             URL_API => 'https://owner-api.teslamotors.com/',
46             URL_ENDPOINTS => 'https://raw.githubusercontent.com/tdorssers/TeslaPy/master/teslapy/endpoints.json',
47             URL_OPTION_CODES => 'https://raw.githubusercontent.com/tdorssers/TeslaPy/master/teslapy/option_codes.json',
48             URL_AUTH => 'https://auth.tesla.com/oauth2/v3/authorize',
49             URL_TOKEN => 'https://auth.tesla.com/oauth2/v3/token',
50             USERAGENT_STRING => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:98.0) Gecko/20100101 Firefox/98.0',
51             USERAGENT_TIMEOUT => 180,
52 20     20   2199 };
  20         42  
53              
54             # Public object methods
55              
56             sub new {
57 19     19 1 1482 my ($class, %params) = @_;
58 19         47 my $self = bless {}, $class;
59              
60 19         89 $self->endpoints;
61              
62             # Return early if the user specifies that they are
63             # not authenticated. Use this param for unit tests
64              
65 19 50       68 if ($params{unauthenticated}) {
66 19         80 return $self;
67             }
68              
69 0         0 $self->api_cache_persist($params{api_cache_persist});
70 0         0 $self->api_cache_time($params{api_cache_time});
71              
72 0         0 $self->mech;
73 0         0 $self->_access_token;
74              
75 0         0 return $self;
76             }
77             sub api {
78 2     2 1 1008 my ($self, %params) = @_;
79              
80 2         3 my $endpoint_name = $params{endpoint};
81 2         2 my $id = $params{id};
82 2         3 my $api_params = $params{api_params};
83              
84 2 100       5 if (! defined $endpoint_name) {
85 1         162 croak "Tesla::API::api() requires an endpoint name sent in";
86             }
87              
88 1         3 my $endpoint = $self->endpoints($endpoint_name);
89              
90 1         3 my $type = $endpoint->{TYPE};
91 1         2 my $auth = $endpoint->{AUTH};
92 1         2 my $uri = $endpoint->{URI};
93              
94 1 50       4 if ($uri =~ /\{/) {
95 1 50 33     3 if (! defined $id || $id !~ /^\d+$/) {
96 1         55 croak "Endpoint $endpoint_name requires an \$id as an integer";
97             }
98 0         0 $uri =~ s/\{.*?\}/$id/;
99             }
100              
101             # Return early if all cache mechanisms check out
102              
103 0 0 0     0 if ($self->api_cache_persist || $self->api_cache_time) {
104 0         0 my $cache = $self->_api_cache(endpoint => $endpoint_name, id => $id);
105 0 0       0 if ($cache) {
106 0 0 0     0 if ($self->api_cache_persist || time - $cache->{time} <= $self->api_cache_time) {
107 0         0 print "Returning cache for $endpoint_name/$id pair...\n" if DEBUG_CACHE;
108 0         0 return $cache->{data};
109             }
110             }
111             }
112              
113 0         0 my $url = URI->new(URL_API . $uri);
114              
115 0         0 my $header = ['Content-Type' => 'application/json; charset=UTF-8'];
116              
117 0 0       0 if ($auth) {
118 0         0 my $token_string = "Bearer " . $self->_access_token;
119 0         0 push @$header, 'Authorization' => $token_string;
120             }
121              
122 0         0 my $request = HTTP::Request->new(
123             $type,
124             $url,
125             $header,
126             JSON->new->allow_nonref->encode($api_params)
127             );
128              
129 0         0 my ($success, $code, $response_data) = $self->_tesla_api_call($request);
130 0         0 my $data = _decode($response_data)->{response};
131              
132 0         0 $self->_api_cache(
133             endpoint => $endpoint_name,
134             id => $id,
135             data => $data
136             );
137              
138 0         0 return $data;
139             }
140             sub api_cache_clear {
141 6     6 1 3034 my ($self) = @_;
142 6         12 %api_cache = ();
143             }
144             sub api_cache_persist {
145 2     2 1 13 my ($self, $persist) = @_;
146 2 100       4 if (defined $persist) {
147 1         2 $self->{api_cache_persist} = $persist;
148             }
149 2   100     16 return $self->{api_cache_persist} // API_CACHE_PERSIST;
150             }
151             sub api_cache_time {
152 9     9 1 5422 my ($self, $cache_seconds) = @_;
153 9 100       18 if (defined $cache_seconds) {
154 8 100       38 if ($cache_seconds !~ /^\d+$/) {
155 5         356 croak "api_cache_time() requires an int as \$cache_seconds param";
156             }
157 3         8 $self->{api_cache_time} = $cache_seconds;
158             }
159 4   100     23 return $self->{api_cache_time} // API_CACHE_TIMEOUT_SECONDS;
160             }
161             sub endpoints {
162 21     21 1 772 my ($self, $endpoint) = @_;
163              
164 21 100 66     142 if (! $self->{endpoints} || $self->{reset_data}) {
165 19         45 $self->{reset_data} = 0;
166              
167 19         25 my $json_endpoints;
168             {
169 19         25 local $/;
  19         52  
170 19 50       692 open my $fh, '<', ENDPOINTS_FILE
171 0         0 or die "Can't open ${\ENDPOINTS_FILE}: $!";
172 19         1951 $json_endpoints = <$fh>;
173             }
174              
175 19         11458 my $perl_endpoints = decode_json($json_endpoints);
176 19         83 $self->{endpoints} = $perl_endpoints;
177             }
178              
179 21 100       103 if ($endpoint) {
180 1 50       4 if (! exists $self->{endpoints}{$endpoint}) {
181 0         0 croak "Tesla API endpoint $endpoint does not exist";
182             }
183 1         3 return $self->{endpoints}{$endpoint};
184             }
185              
186 20         85 return $self->{endpoints};
187             }
188             sub mech {
189 1     1 1 6 my ($self) = @_;
190              
191 1 50       3 return $self->{mech} if $self->{mech};
192              
193 1         3 my $www_mech = WWW::Mechanize->new(
194             agent => $self->useragent_string,
195             autocheck => 0,
196             cookie_jar => {},
197             timeout => $self->useragent_timeout
198             );
199              
200 1         11591 $self->{mech} = $www_mech;
201              
202 1         5 return $self->{mech};
203             }
204             sub option_codes {
205 580     580 1 422790 my ($self, $code) = @_;
206              
207 580 100       1110 if (! $self->{option_codes}) {
208 1         1 my $json_option_codes;
209             {
210 1         1 local $/;
  1         3  
211 1 50       31 open my $fh, '<', OPTION_CODES_FILE
212 0         0 or die "Can't open ${\OPTION_CODES_FILE}: $!";
213 1         62 $json_option_codes = <$fh>;
214             }
215              
216 1         238 my $perl_option_codes = decode_json($json_option_codes);
217              
218 1         4 $self->{option_codes} = $perl_option_codes;
219             }
220              
221 580 100       831 if ($code) {
222 579 100       904 if (! exists $self->{option_codes}{$code}) {
223 1         440 croak "Tesla API option code $code does not exist";
224             }
225 578         1945 return $self->{option_codes}{$code};
226             }
227              
228 1         4 return $self->{option_codes};
229             }
230             sub update_data_files {
231 0     0 1 0 my ($self) = @_;
232              
233 0         0 for my $data_url (URL_ENDPOINTS, URL_OPTION_CODES) {
234 0         0 my $filename;
235              
236 0 0       0 if ($data_url =~ /.*\/(\w+\.json)$/) {
237 0         0 $filename = $1;
238             }
239              
240 0 0       0 if (! defined $filename) {
241 0         0 croak "Couldn't extract the filename from '$data_url'";
242             }
243              
244 0         0 (my $data_method = $filename) =~ s/\.json//;
245              
246 0         0 my $url = URI->new($data_url);
247              
248 0         0 my $request = HTTP::Request->new('GET', $url,);
249              
250 0         0 my ($success, $code, $response_data) = $self->_tesla_api_call($request);
251 0         0 my $new_data = decode_json($response_data);
252 0         0 my $existing_data = $self->$data_method;
253              
254 0         0 my $data_differs;
255              
256 0 0       0 if (scalar keys %$new_data != scalar keys %$existing_data) {
257 0         0 $data_differs = 1;
258             }
259              
260 0 0       0 if (! $data_differs) {
261 0         0 for my $key (keys %$new_data) {
262 0 0       0 if (! exists $existing_data->{$key}) {
263 0         0 $data_differs = 1;
264 0         0 last;
265             }
266             }
267 0         0 for my $key (keys %$existing_data) {
268 0 0       0 if (! exists $new_data->{$key}) {
269 0         0 $data_differs = 1;
270 0         0 last;
271             }
272             }
273             }
274              
275 0 0       0 if ($data_differs) {
276 0         0 $self->{reset_data} = 1;
277 0         0 my $file = dist_file('Tesla-API', $filename);
278              
279             # Make a backup copy
280              
281 0         0 my $backup = "$file." . time;
282 0 0       0 copy($file, $backup) or die "Can't create $file backup file!: $!";
283              
284 0         0 chmod 0644, $file;
285              
286 0 0       0 open my $fh, '>', "$file"
287             or die "Can't open '$file' for writing: $!";
288              
289 0         0 print $fh JSON->new->pretty->encode($new_data);
290             }
291             }
292             }
293             sub useragent_string {
294 3     3 1 15 my ($self, $ua_string) = @_;
295              
296 3 100       16 if (defined $ua_string) {
297 1         2 $self->{useragent_string} = $ua_string;
298             }
299              
300 3   100     25 return $self->{useragent_string} // USERAGENT_STRING;
301             }
302             sub useragent_timeout {
303 16     16 1 5216 my ($self, $timeout) = @_;
304              
305 16 100       25 if (defined $timeout) {
306 14 100       73 if ($timeout !~ /^\d+?(?:\.\d+)?$/) {
307 9         546 croak "useragent_timeout() requires an integer or float";
308             }
309 5         8 $self->{useragent_timeout} = $timeout;
310             }
311              
312 7   100     46 return $self->{useragent_timeout} // USERAGENT_TIMEOUT;
313             }
314              
315             # Private methods
316              
317             sub _access_token {
318             # Returns the access token from the cache file or generates
319             # that cache file (with token) if it isn't available
320              
321 0     0   0 my ($self) = @_;
322              
323 0 0       0 if (! -e $self->_authentication_cache_file) {
324 0         0 $self->_access_token_generate;
325             }
326              
327 0 0       0 if (! $self->_access_token_valid) {
328 0         0 $self->_access_token_refresh;
329             }
330              
331 0         0 $self->{access_token} = $self->_access_token_data->{access_token};
332              
333 0         0 return $self->{access_token};
334             }
335             sub _access_token_data {
336             # Fetches and stores the access token data to the AUTH_CACHE_FILE
337              
338 9     9   3593 my ($self, $data) = @_;
339              
340 9 100       21 $self->{cache_data} = $data if defined $data;
341              
342 9 100       26 return $self->{cache_data} if $self->{cache_data};
343              
344             {
345 2         2 local $/;
  2         6  
346 2 100       8 open my $fh, '<', $self->_authentication_cache_file or die
347             "Can't open Tesla cache file " .
348             $self->_authentication_cache_file .
349             ": $!";
350              
351 1         18 my $json = <$fh>;
352 1         22 $self->{cache_data} = decode_json($json);
353             }
354              
355 1         4 return $self->{cache_data};
356             }
357             sub _access_token_generate {
358             # Generates an access token and stores it in the cache file
359              
360 1     1   177 my ($self) = @_;
361              
362 1         3 my $auth_code = $self->_authentication_code;
363              
364 1         8 my $url = URI->new(URL_TOKEN);
365 1         5659 my $header = ['Content-Type' => 'application/json; charset=UTF-8'];
366              
367 1         5 my $request_data = {
368             grant_type => "authorization_code",
369             client_id => "ownerapi",
370             code => $auth_code,
371             code_verifier => $self->_authentication_code_verifier,
372             redirect_uri => "https://auth.tesla.com/void/callback",
373             };
374              
375 1         32 my $request = HTTP::Request->new(
376             'POST',
377             $url,
378             $header,
379             JSON->new->allow_nonref->encode($request_data)
380             );
381              
382 1         209 my ($success, $code, $response_data)= $self->_tesla_api_call($request);
383              
384 1         41 my $token_data = decode_json($response_data);
385              
386 1         5 $token_data = $self->_access_token_set_expiry($token_data);
387 1         4 $self->_access_token_update($token_data);
388              
389 1         17 return $token_data;
390             }
391             sub _access_token_valid {
392             # Checks the validity of an existing token
393              
394 2     2   7 my ($self) = @_;
395              
396 2         5 my $token_expires_at = $self->_access_token_data->{expires_at};
397              
398 2         3 my $valid = 0;
399              
400 2 100       6 if (time + TOKEN_EXPIRY_WINDOW < $token_expires_at) {
401 1         1 $valid = 1;
402             }
403              
404 2         9 return $valid;
405             }
406             sub _access_token_set_expiry {
407             # Sets the access token expiry date/time after generation and
408             # renewal
409              
410 5     5   1268 my ($self, $token_data) = @_;
411              
412 5 50 33     40 if (! defined $token_data || ref($token_data) ne 'HASH') {
413 0         0 croak "_access_token_set_expiry() needs a hash reference of token data";
414             }
415              
416 5         14 my $expiry = time + $token_data->{expires_in};
417              
418 5         9 $token_data->{expires_at} = $expiry;
419              
420 5         10 return $token_data;
421             }
422             sub _access_token_refresh {
423             # Renews an expired/invalid access token
424              
425 0     0   0 my ($self) = @_;
426              
427 0         0 my $url = URI->new(URL_TOKEN);
428 0         0 my $header = ['Content-Type' => 'application/json; charset=UTF-8'];
429              
430 0         0 my $refresh_token = $self->_access_token_data->{refresh_token};
431              
432 0         0 my $request_data = {
433             grant_type => 'refresh_token',
434             refresh_token => $refresh_token,
435             client_id => 'ownerapi',
436             };
437              
438 0         0 my $request = HTTP::Request->new(
439             'POST',
440             $url,
441             $header,
442             JSON->new->allow_nonref->encode($request_data)
443             );
444              
445 0         0 my ($success, $code, $response_data) = $self->_tesla_api_call($request);
446              
447             # Extract the token data
448 0         0 my $token_data = decode_json($response_data);
449              
450             # Re-add the existing refresh token; its still valid
451 0         0 $token_data->{refresh_token} = $refresh_token;
452              
453             # Set the expiry time
454 0         0 $token_data = $self->_access_token_set_expiry($token_data);
455              
456             # Update the cached token
457 0         0 $self->_access_token_update($token_data);
458             }
459             sub _access_token_update {
460             # Writes the new or updated token to the cache file
461              
462 2     2   7 my ($self, $token_data) = @_;
463              
464 2 50 33     9 if (! defined $token_data || ref($token_data) ne 'HASH') {
465 0         0 croak "_access_token_update() needs a hash reference of token data";
466             }
467              
468 2         7 $self->_access_token_data($token_data);
469              
470 2 50       9 open my $fh, '>', $self->_authentication_cache_file or die $!;
471              
472 2         180 print $fh JSON->new->allow_nonref->encode($token_data);
473             }
474             sub _api_attempts {
475             # Stores and returns the number of attempts of each API call to Tesla
476              
477 0     0   0 my ($self, $add) = @_;
478              
479 0 0       0 if (defined $add) {
480 0         0 $self->{api_attempts}++;
481             }
482              
483 0   0     0 return $self->{api_attempts} || 0;
484             }
485             sub _api_cache {
486             # Stores the Tesla API fetched data
487 6     6   426 my ($self, %params) = @_;
488              
489 6         11 my $endpoint = $params{endpoint};
490 6   100     17 my $id = $params{id} // 0;
491 6         9 my $data = $params{data};
492              
493 6 100       12 if (! $endpoint) {
494 1         135 croak "_api_cache() requires an endpoint name sent in";
495             }
496              
497 5 100       11 if ($data) {
498 3         7 $api_cache{$endpoint}{$id}{data} = $data;
499 3         8 $api_cache{$endpoint}{$id}{time} = time;
500             }
501              
502 5         14 return $api_cache{$endpoint}{$id};
503             }
504             sub _api_cache_data {
505             # Returns the entire API cache (for testing)
506 5     5   404 return %api_cache;
507             }
508             sub _authentication_cache_file {
509 11     11   1623 my ($self, $filename) = @_;
510              
511 11 100       23 if (defined $filename) {
512 4         8 $self->{authentication_cache_file} = $filename;
513             }
514              
515 11   100     289 return $self->{authentication_cache_file} || AUTH_CACHE_FILE;
516             }
517             sub _authentication_code {
518             # If an access token is unavailable, prompt the user with a URL to
519             # authenticate to Tesla, and have them paste in the resulting URL
520             # We then extract and return the access code to generate the access
521             # token
522              
523 1     1   2 my ($self) = @_;
524              
525 1 50       6 return $self->{authentication_code} if $self->{authentication_code};
526              
527 0         0 my $auth_url = $self->_authentication_code_url;
528              
529 0         0 print "\n$auth_url\n";
530              
531 0         0 print "\nPaste URL here: ";
532              
533 0         0 my $code_url = ;
534 0         0 chomp $code_url;
535              
536 0         0 my $code = $self->_authentication_code_extract($code_url);
537              
538 0         0 $self->{authentication_code} = $code;
539              
540 0         0 return $code;
541             }
542             sub _authentication_code_extract {
543             # Pull in the pasted URL with the code, extract the code,
544             # and return it
545              
546 1     1   7 my ($self, $code_url) = @_;
547              
548 1         2 my $code;
549              
550 1 50       8 if ($code_url =~ /code=(.*?)\&/) {
551 1         3 $code = $1;
552             }
553             else {
554 0         0 croak "Could not extract the authorization code from the URL";
555             }
556              
557 1         2 return $code;
558             }
559             sub _authentication_code_url {
560             # Generate Tesla's authentication URL
561              
562 1     1   10 my ($self) = @_;
563 1         11 my $auth_url = URI->new(URL_AUTH);
564              
565             my %params = (
566             client_id => 'ownerapi',
567             code_challenge => $self->_authentication_code_verifier,
568             code_challenge_method => 'S256',
569             redirect_uri => 'https://auth.tesla.com/void/callback',
570             response_type => 'code',
571             scope => 'openid email offline_access',
572             state => '123',
573             login_hint => $ENV{TESLA_EMAIL},
574 1         6046 );
575              
576 1         10 $auth_url->query_form(%params);
577              
578 1         277 print
579             "\nPlease follow the URL displayed below in your browser and log into Tesla, " .
580             "then paste the URL from the resulting 'Page Not Found' page's address bar, " .
581             "then hit ENTER:\n";
582              
583 1         9 return $auth_url;
584             }
585             sub _authentication_code_verifier {
586             # When generating an access token, generate and store a code
587             # validation key
588              
589 5     5   34345 my ($self) = @_;
590              
591 5 100       20 if (defined $self->{authentication_code_verifier}) {
592             return $self->{authentication_code_verifier}
593 2         13 }
594              
595 3         20 my $code_verifier = _random_string();
596 3         46 $code_verifier = sha256_hex($code_verifier);
597 3         14 $code_verifier = encode_base64url($code_verifier);
598              
599 3         57 return $self->{authentication_code_verifier} = $code_verifier;
600             }
601             sub _decode {
602             # Decode JSON to Perl
603 0     0   0 my ($json) = @_;
604 0         0 my $perl = decode_json($json);
605 0         0 return $perl;
606             }
607             sub _random_string {
608             # Returns a proper length alpha-num string for Tesla API token code
609             # verification key
610              
611 3     3   65 my @chars = ('A' .. 'Z', 'a' .. 'z', 0 .. 9);
612 3         6 my $rand_string;
613 3         158 $rand_string .= $chars[rand @chars] for 1 .. 85;
614 3         15 return $rand_string;
615             }
616             sub _tesla_api_call {
617             # Responsible for all calls to the Tesla API
618              
619 0     0     my ($self, $request) = @_;
620              
621 0           my $response;
622              
623 0           for (1 .. API_TIMEOUT_RETRIES) {
624 0           $response = $self->mech->request($request);
625              
626 0           $self->_api_attempts(1);
627              
628 0 0         if ($response->is_success) {
629 0           last;
630             }
631             }
632              
633 0 0         if (! $response->is_success) {
634 0           my $error_string = sprintf(
635             "Error - Tesla API said: '%s'",
636             $self->mech->response->status_line
637             );
638              
639 0           croak $error_string;
640             }
641              
642             return (
643 0           $response->is_success,
644             $response->code,
645             $response->decoded_content
646             );
647             }
648              
649             1;
650              
651             =head1 NAME
652              
653             Tesla::API - Interface to Tesla's API
654              
655             =for html
656            
657             Coverage Status
658              
659             =head1 SYNOPSIS
660              
661             use Tesla::API;
662              
663             my $tesla = Tesla::API->new;
664              
665             my @endpoint_names = keys %{ $tesla->endpoints };
666              
667             # See Tesla::Vehicle for direct access to vehicle-related methods
668              
669             my $endpoint_name = 'VEHICLE_DATA';
670             my $vehicle_id = 3234234242124;
671              
672             # Get the entire list of car data
673              
674             my $car_data = $tesla->api(
675             endpoint => $endpoint_name,
676             id => $vehicle_id
677             );
678              
679             # Send the open trunk command
680              
681             $tesla->api(
682             endpoint => 'ACTUATE_TRUNK',
683             id => $vehicle_id,
684             api_params => {which_trunk => 'rear'}
685             );
686              
687             =head1 DESCRIPTION
688              
689             This distribution provides access to the Tesla API.
690              
691             This class is designed to be subclassed. For example, I have already begun a
692             new L distribution which will have access and update methods
693             that deal specifically with Tesla autos, then a C
694             distribution for their battery storage etc.
695              
696             =head1 METHODS - CORE
697              
698             =head2 new(%params)
699              
700             Instantiates and returns a new L object.
701              
702             B: When instantiating an object and you haven't previously authenticated,
703             a URL will be displayed on the console for you to navigate to. You will then
704             be redirected to Tesla's login page where you will authenticate. You will
705             be redirected again to a "Page Not Found" page, in which you must copy the URL
706             from the address bar and paste it back into the console.
707              
708             We then internally generate an access token for you, store it in a
709             C file in your home directory, and use it on all
710             subsequent accesses.
711              
712             B: If you do not have a Tesla account, you can still instantiate a
713             L object by supplying the C<< unauthenticated => 1 >> parameter
714             to C.
715              
716             B:
717              
718             All parameters are to be sent in the form of a hash.
719              
720             unauthenticated
721              
722             I: Set to true to bypass the access token generation.
723              
724             I: C
725              
726             api_cache_persist
727              
728             I: Set this to true if you want to make multiple calls against
729             the same data set, where having the cache time out and re-populated between
730             these calls would be non-beneficial.
731              
732             I: False
733              
734             api_cache_time
735              
736             I: By default, we cache the fetched data from the Tesla API
737             for two seconds. If you make calls that have already been called within that
738             time, we will return the cached data.
739              
740             Send in the number of seconds you'd like to cache the data for. A value of zero
741             (C<0>) will disable caching and all calls through this library will go directly
742             to Tesla every time.
743              
744             I: Integer, the number of seconds we're caching Tesla API data for.
745              
746             =head2 api(%params)
747              
748             Responsible for disseminating the endpoints and retrieving data through the
749             Tesla API.
750              
751             All parameters are to be sent in as a hash.
752              
753             B:
754              
755             endpoint
756              
757             I: A valid Tesla API endpoint name. The entire list can be
758             found in the C file for the time being.
759              
760             id
761              
762             I: Some endpoints require an ID sent in (eg. vehicle ID,
763             Powerwall ID etc).
764              
765             api_params
766              
767             I: Some API calls require additional parameters. Send
768             in a hash reference where the keys are the API parameter name, and the value is,
769             well, the value.
770              
771             I: Hash or array reference, depending on the endpoint.
772              
773             =head2 endpoints
774              
775             Returns a hash reference of hash references. Each key is the name of the
776             endpoint, and its value contains data on how we process the call to Tesla.
777              
778             Example (snipped for brevity):
779              
780             {
781             MEDIA_VOLUME_DOWN => {
782             TYPE => 'POST',
783             URI => 'api/1/vehicles/{vehicle_id}/command/media_volume_down'
784             AUTH => $VAR1->{'UPGRADES_CREATE_OFFLINE_ORDER'}{'AUTH'},
785             },
786             VEHICLE_DATA => {
787             TYPE => 'GET',
788             URI => 'api/1/vehicles/{vehicle_id}/vehicle_data',
789             AUTH => $VAR1->{'UPGRADES_CREATE_OFFLINE_ORDER'}{'AUTH'}
790             },
791             }
792              
793             Bracketed names in the URI (eg: C<{vehicle_id}>) are variable placeholders.
794             It will be replaced with the ID sent in to the various method or C
795             call.
796              
797             To get a list of endpoint names:
798              
799             my @endpoint_names = keys %{ $tesla->endpoints };
800              
801             =head2 mech
802              
803             Returns the L object we've instantiated internally.
804              
805             =head2 option_codes
806              
807             B: I'm unsure if the option codes are vehicle specific, or general for
808             all Tesla products, so I'm leaving this method here for now.
809              
810             Returns a hash reference of 'option code' => 'description' pairs.
811              
812             =head2 update_data_files
813              
814             Checks to see if there are any updates to the C or
815             C files online, and updates them locally.
816              
817             Takes no parameters, there is no return. Cs on failure.
818              
819             =head2 useragent_string($ua_string)
820              
821             Sets/gets the useragent string we send to the Tesla API.
822              
823             I: The user agent browser string to send to the Tesla API.
824              
825             I: String, the currently set value.
826              
827             =head2 useragent_timeout($timeout)
828              
829             Sets/gets the timeout we use in the L object that we
830             communicate to the Tesla API with.
831              
832             Parameters:
833              
834             $timeout
835              
836             I: The timeout in seconds or fractions of a second.
837              
838             I: Integer/Float, the currently set value.
839              
840             =head1 METHODS - API CACHE
841              
842             =head2 api_cache_clear
843              
844             Some methods chain method calls. For example, calling
845             C<< $vehicle->doors_lock >> will poll the API, then cache the state data.
846              
847             if another call is made to C<< $vehicle->locked >> immediately thereafter to
848             check whether the door is actually closed or not, the old cached data would
849             normally be returned.
850              
851             If we don't clear the cache out between these two calls, we will be returned
852             stale data.
853              
854             Takes no parameters, has no return. Only use this call in API calls that
855             somehow manipulate the state of the object you're working with.
856              
857             =head2 api_cache_persist($bool)
858              
859             $bool
860              
861             I: Set this to true if you want to make multiple calls against
862             the same data set, where having the cache time out and re-populated between
863             these calls would be non-beneficial.
864              
865             You can ensure fresh data for the set by making a call to C
866             before the first call that fetches data.
867              
868             I: False
869              
870             =head2 api_cache_time($cache_seconds)
871              
872             The number of seconds we will cache retrieved endpoint data from the Tesla API
873             for, to reduce the number of successive calls to retrieve the same data.
874              
875             B:
876              
877             $cache_seconds
878              
879             I: By default, we cache the fetched data from the Tesla API
880             for two seconds. If you make calls that have already been called within that
881             time, we will return the cached data.
882              
883             Send in the number of seconds you'd like to cache the data for. A value of zero
884             (C<0>) will disable caching and all calls through this library will go directly
885             to Tesla every time.
886              
887             I: Integer, the number of seconds we're caching Tesla API data for.
888              
889             =head1 API CACHING
890              
891             We've employed a complex caching mechanism for data received from Tesla's API.
892              
893             By default, we cache retrieved data for every endpoint/ID pair in the cache for
894             two seconds (modifiable by C, or C in
895             C).
896              
897             This means that if you call three methods in a row that all extract information
898             from the data returned via a single endpoint/ID pair, you may get back the
899             cached result, or if the cache has timed out, you'll get data from another call
900             to the Tesla API. In some cases, having the data updated may be desirable,
901             sometimes you want data from the same set.
902              
903             Here are some examples on how to deal with the caching mechanism. We will use
904             a L object for this example:
905              
906             =head2 Store API cache for 10 seconds
907              
908             Again, by default, we cache and return data from the Tesla API for two seconds.
909             Change it to 10:
910              
911             my $api = Tesla::API->new(api_cache_timeout => 10);
912              
913             ...or:
914              
915             my $car = Tesla::Vehicle->new(api_cache_timeout => 10);
916              
917             ...or:
918              
919             $car->api_cache_timeout(10);
920              
921             =head2 Disable API caching
922              
923             my $api = Tesla::API->new(api_cache_timeout => 0);
924              
925             ...or:
926              
927             my $car = Tesla::Vehicle->new(api_cache_timeout => 0);
928              
929             ...or:
930              
931             $car->api_cache_timeout(0);
932              
933             =head2 Flush the API cache
934              
935             $api->api_cache_clear;
936              
937             ...or:
938              
939             $car->api_cache_clear;
940              
941             =head2 Permanently use the cached data until manually flushed
942              
943             my $api = Tesla::API->new(api_cache_persist => 1);
944              
945             ...or:
946              
947             my $car = Tesla::Vehicle->new(api_cache_persist => 1);
948              
949             ...or:
950              
951             $car->api_cache_persist(1);
952              
953             =head2 Use the cache for a period of time
954              
955             If making multiple calls to methods that use the same data set and want to be
956             sure the data doesn't change until you're done, do this:
957              
958             my $car = Tesla::Vehicle->new; # Default caching of 2 seconds
959              
960             sub work {
961              
962             # Clear the cache so it gets updated, but set it to persistent so once
963             # the cache data is updated, it remains
964              
965             $car->api_cache_clear;
966             $car->api_cache_persist(1);
967              
968             say $car->online;
969             say $car->lat;
970             say $car->lon;
971             say $car->battery_level;
972              
973             # Now unset the persist flag so other parts of your program won't be
974             # affected by it
975              
976             $car->api_cache_persist(0);
977             }
978              
979             If you are sure no other parts of your program will be affected by having a
980             persistent cache, you can set it globally:
981              
982             my $car = Tesla::Vehicle->new(api_cache_persist => 1);
983              
984             while (1) {
985              
986             # Clear the cache at the beginning of the loop so it gets updated,
987             # unless you never want new data after the first saving of data
988              
989             $car->api_cache_clear;
990              
991             say $car->online;
992             say $car->lat;
993             say $car->lon;
994             say $car->battery_level;
995             }
996              
997             =head1 EXAMPLE USAGE
998              
999             See L for vehicle specific methods.
1000              
1001             use Data::Dumper;
1002             use Tesla::API;
1003             use feature 'say';
1004              
1005             my $tesla = Tesla::API->new;
1006             my $vehicle_id = 1234238782349137;
1007              
1008             print Dumper $tesla->api(endpoint => 'VEHICLE_DATA', id => $vehicle_id);
1009              
1010             Output (massively and significantly snipped for brevity):
1011              
1012             $VAR1 = {
1013             'vehicle_config' => {
1014             'car_type' => 'modelx',
1015             'rear_seat_type' => 7,
1016             'rear_drive_unit' => 'Small',
1017             'wheel_type' => 'Turbine22Dark',
1018             'timestamp' => '1647461524710',
1019             'rear_seat_heaters' => 3,
1020             'trim_badging' => '100d',
1021             'headlamp_type' => 'Led',
1022             'driver_assist' => 'TeslaAP3',
1023             },
1024             'id_s' => 'XXXXXXXXXXXXXXXXX',
1025             'vehicle_id' => 'XXXXXXXXXX',
1026             'charge_state' => {
1027             'usable_battery_level' => 69,
1028             'battery_range' => '189.58',
1029             'charge_limit_soc_std' => 90,
1030             'charge_amps' => 48,
1031             'charge_limit_soc' => 90,
1032             'battery_level' => 69,
1033             },
1034             'vin' => 'XXXXXXXX',
1035             'in_service' => $VAR1->{'vehicle_config'}{'use_range_badging'},
1036             'user_id' => 'XXXXXX',
1037             'id' => 'XXXXXXXXXXXXX',
1038             'drive_state' => {
1039             'shift_state' => 'P',
1040             'heading' => 92,
1041             'longitude' => '-XXX.XXXXXX',
1042             'latitude' => 'XX.XXXXXX',
1043             'power' => 0,
1044             'speed' => undef,
1045             },
1046             'api_version' => 34,
1047             'display_name' => 'Dream machine',
1048             'state' => 'online',
1049             'access_type' => 'OWNER',
1050             'option_codes' => 'AD15,MDL3,PBSB,RENA,BT37,ID3W,RF3G,S3PB,DRLH,DV2W,W39B,APF0,COUS,BC3B,CH07,PC30,FC3P,FG31,GLFR,HL31,HM31,IL31,LTPB,MR31,FM3B,RS3H,SA3P,STCP,SC04,SU3C,T3CA,TW00,TM00,UT3P,WR00,AU3P,APH3,AF00,ZCST,MI00,CDM0',
1051             'vehicle_state' => {
1052             'valet_mode' => $VAR1->{'vehicle_config'}{'use_range_badging'},
1053             'vehicle_name' => 'Dream machine',
1054             'sentry_mode_available' => $VAR1->{'vehicle_config'}{'plg'},
1055             'sentry_mode' => $VAR1->{'vehicle_config'}{'use_range_badging'},
1056             'car_version' => '2022.4.5.4 abcfac6bfcdc',
1057             'homelink_device_count' => 3,
1058             'is_user_present' => $VAR1->{'vehicle_config'}{'use_range_badging'},
1059             'odometer' => 'XXXXXXX.233656',
1060             'media_state' => {
1061             'remote_control_enabled' => $VAR1->{'vehicle_config'}{'plg'}
1062             },
1063             },
1064             'autopark_style' => 'dead_man',
1065             'software_update' => {
1066             'expected_duration_sec' => 2700,
1067             'version' => ' ',
1068             'status' => '',
1069             'download_perc' => 0,
1070             'install_perc' => 1
1071             },
1072             'speed_limit_mode' => {
1073             'max_limit_mph' => 90,
1074             'min_limit_mph' => '50',
1075             'active' => $VAR1->{'vehicle_config'}{'use_range_badging'},
1076             'current_limit_mph' => '80.029031',
1077             'pin_code_set' => $VAR1->{'vehicle_config'}{'plg'}
1078             },
1079             'climate_state' => {
1080             'passenger_temp_setting' => '20.5',
1081             'driver_temp_setting' => '20.5',
1082             'side_mirror_heaters' => $VAR1->{'vehicle_config'}{'use_range_badging'},
1083             'is_climate_on' => $VAR1->{'vehicle_config'}{'use_range_badging'},
1084             'fan_status' => 0,
1085             'seat_heater_third_row_right' => 0,
1086             'seat_heater_right' => 0,
1087             'is_front_defroster_on' => $VAR1->{'vehicle_config'}{'use_range_badging'},
1088             'battery_heater' => $VAR1->{'vehicle_config'}{'use_range_badging'},
1089             'is_rear_defroster_on' => $VAR1->{'vehicle_config'}{'use_range_badging'},
1090             },
1091             'gui_settings' => {
1092             'gui_temperature_units' => 'C',
1093             'gui_charge_rate_units' => 'km/hr',
1094             'gui_24_hour_time' => $VAR1->{'vehicle_config'}{'use_range_badging'},
1095             'gui_range_display' => 'Ideal',
1096             'show_range_units' => $VAR1->{'vehicle_config'}{'plg'},
1097             'gui_distance_units' => 'km/hr',
1098             'timestamp' => '1647461524710'
1099             }
1100             };
1101              
1102             =head1 CONFIGURATION VARIABLES
1103              
1104             Most configuration options used in this software are defined as constants in the
1105             library file. Some are defaults that can be overridden via method calls, others
1106             are only modifiable by updating the actual value in the file.
1107              
1108             =head2 DEBUG_CACHE
1109              
1110             Prints to C debugging output from the caching mechanism.
1111              
1112             I: C<$ENV{DEBUG_TESLA_API_CACHE}>
1113              
1114             =head2 API_CACHE_PERSIST
1115              
1116             Always/never use the cache once data has been retrieved through the Tesla API.
1117              
1118             I: False (C<0>).
1119              
1120             I: None
1121              
1122             =head2 API_CACHE_TIMEOUT_SECONDS
1123              
1124             How many seconds to reuse the cached data retrieved from the Tesla API.
1125              
1126             I: C<2>
1127              
1128             I: L
1129              
1130             =head2 API_CACHE_RETRIES
1131              
1132             How many times we'll try a Tesla API call in the event of a failure.
1133              
1134             I: C<3>
1135              
1136             I: None
1137              
1138             =head2 AUTH_CACHE_FILE
1139              
1140             The path and filename of the file we'll store the Tesla API access token
1141             information.
1142              
1143             I: C<$home_dir/tesla_api_cache.json>
1144              
1145             I: C<_authentication_cache_file()>, used primarily for unit testing.
1146              
1147             =head2 ENDPOINTS_FILE
1148              
1149             The path and filename of the file we'll store the Tesla API endpoint description
1150             file.
1151              
1152             I: C
1153              
1154             I: None
1155              
1156             =head2 OPTION_CODES_FILE
1157              
1158             The path and filename of the file we'll store the product option code list file
1159             for Tesla products.
1160              
1161             I: C
1162              
1163             I: None
1164              
1165             =head2 TOKEN_EXPIRY_WINDOW
1166              
1167             The number of seconds we'll add to the current time when validating the token
1168             expiry. This is effectively a cusion window so that the token has at least this
1169             many seconds before expiring to ensure the next call won't use an invalidated
1170             token
1171              
1172             I: C<5>
1173              
1174             I: None
1175              
1176             =head2 URI_API
1177              
1178             The URL we use to communicate with the Tesla API for data retrieval operations.
1179              
1180             I: C
1181              
1182             I: None
1183              
1184             =head2 URI_ENDPOINTS
1185              
1186             The URL we use to retrieve the updated Tesla API endpoints file.
1187              
1188             I: C
1189              
1190             I: None
1191              
1192             =head2 URI_OPTION_CODES
1193              
1194             The URL we use to retrieve the updated Tesla API product option codes file.
1195              
1196             I: C
1197              
1198             I: None
1199              
1200             =head2 URI_AUTH
1201              
1202             The URL we use to perform the Tesla API authentication routines.
1203              
1204             I: C
1205              
1206             I: None
1207              
1208             =head2 URI_TOKEN
1209              
1210             The URL we use to fetch and update the Tesla API access tokens.
1211              
1212             I: C
1213              
1214             I: None
1215              
1216             =head2 USERAGENT_STRING
1217              
1218             String used to identify the 'browser' we're using to access the Tesla API.
1219              
1220             I: C
1221              
1222             I: L
1223              
1224             =head2 USERAGENT_TIMEOUT
1225              
1226             Number of seconds before we classify a call to the Tesla API as timed out.
1227              
1228             I: C<180>
1229              
1230             I: L
1231              
1232             =head1 AUTHOR
1233              
1234             Steve Bertrand, C<< >>
1235              
1236             =head1 ACKNOWLEDGEMENTS
1237              
1238             This distribution suite has been a long time in the works. For my other projects
1239             written in Perl previous to writing this code that required data from the Tesla
1240             API, I wrapped L wonderful
1241             L Python project.
1242              
1243             Much of the code in this distribution is heavily influenced by the code his
1244             project, and currently, we're using a direct copy of its
1245             L.
1246              
1247             Thanks Tim, and great work!
1248              
1249             Also thanks goes out to L, as a lot of the actual request
1250             parameter information and response data layout I learned from that site while
1251             implementing the actual REST calls to the Tesla API.
1252              
1253             =head1 LICENSE AND COPYRIGHT
1254              
1255             Copyright 2022 Steve Bertrand.
1256              
1257             This program is free software; you can redistribute it and/or modify it
1258             under the terms of the the Artistic License (2.0). You may obtain a
1259             copy of the full license at:
1260              
1261             L
1262              
1263             The copied endpoint code data borrowed from Tim's B project has been
1264             rebranded with the Perl license here, as permitted by the MIT license TeslaPy
1265             is licensed under.
1266              
1267             =cut