File Coverage

blib/lib/Tesla/API.pm
Criterion Covered Total %
statement 78 277 28.1
branch 11 94 11.7
condition 3 24 12.5
subroutine 19 39 48.7
pod 10 10 100.0
total 121 444 27.2


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