File Coverage

blib/lib/Tesla/API.pm
Criterion Covered Total %
statement 292 322 90.6
branch 93 114 81.5
condition 34 46 73.9
subroutine 43 45 95.5
pod 11 11 100.0
total 473 538 87.9


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