From 19f457957934c34a589aa7c35e08a9fdc09c6fcf Mon Sep 17 00:00:00 2001 From: Jan Kratochvil Date: Fri, 1 Apr 2022 18:52:59 +0200 Subject: [PATCH] Add scheduled_charging{,_set}, preconditioning{,_set}, windows_set, charge_{on,off}, sentry{,_on,_off}, temperatures_set Also add an example charge.pl . --- examples/charge.pl | 175 ++++++++++++++++++++++++++++ examples/vehicle.pl | 7 ++ lib/Tesla/Vehicle.pm | 267 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 449 insertions(+) create mode 100755 examples/charge.pl diff --git a/examples/charge.pl b/examples/charge.pl new file mode 100755 index 0000000..92ee485 --- /dev/null +++ b/examples/charge.pl @@ -0,0 +1,175 @@ +#!/usr/bin/env perl +use warnings; +use strict; +use feature 'say'; +use POSIX; +use Tesla::Vehicle; + +my $TEMPERATURE=23; + +my $car; +my $cmd_ran; +sub cmd($@) { + my($cmd,@args)=@_; + $cmd_ran=1; + my $retval=$car->$cmd(@args); + say "$cmd: $retval"; + return $retval; +} +sub minutes_to_time($) { + my($minutes)=@_; + return "none" if !defined $minutes; + return sprintf "%02d:%02d",$minutes/60,$minutes%60; +} +my $timere=qr/0*(\d+):0*(\d+)/; +sub time_to_minutes($) { + my($time)=@_; + return undef if !defined $time; + my($h,$m)=($time=~/^$timere$/o) or die "$time is not d+:d+"; + die "Hours is not 0..23" if $h<0||$h>23; + die "Minutes is not 0..59" if $m<0||$m>59; + return $h*60+$m; +} + +my $windows; +sub windows() { + my $windows_text=join ", ",map {0=>"closed",1=>"venting",2=>"opened"}->{$_},map $car->state->{"${_}_window"},qw(fd fp rd rp); + $windows=$windows_text=~s/^([^,]+)(?:, \1)+$/all $1/?$car->state->{"fd_window"}:undef; + say "Windows are $windows_text."; +} +sub doors() { + my $doors=join ", ",map $_==0?"closed":"open$_",map $car->state->{"${_}"},qw(df pf dr pr); + $doors=~s/^([^,]+)(?:, \1)+$/all $1/; + say "Doors are $doors." if $doors ne "all closed"; +} +sub state() { + cmd "charge_limit_soc"; + cmd "charging_state"; + say "scheduled_charging: ".minutes_to_time $car->scheduled_charging; + say "preconditioning: ".minutes_to_time $car->preconditioning; + windows; + doors; + if ($car->is_climate_on) { + say "Car conditioning is on."; + say "Temperature set to ".$car->temperature_setting_driver." != $TEMPERATURE." if $car->temperature_setting_driver!=$TEMPERATURE; + say "warning: temperature_setting_passenger ".$car->temperature_setting_passenger." != ".$car->temperature_setting_driver." temperature_setting_driver" + if $car->temperature_setting_passenger!=$car->temperature_setting_driver; + } + print "Both conditioning and preconditioning are set!\n" if $car->is_climate_on&&$car->preconditioning; + say "Car is locked." if $car->locked; + say "Sentry mode is on." if $car->sentry; + say "Sentry mode is not available!" if !$car->state->{"sentry_mode_available"}; + print "Sentry mode != car lock.\n" if $car->sentry!=$car->locked; +} + +my $battery_want; +my $time_want; +my $minutes_want_from; +my $minutes_want_to; +my $precondition; +my $reset; +my $windows_want; +my $auto_wake=1; +my $battery_direct; +my $conditioning; +my $lock; +my $sentry; +my $dump; +for my $arg (@ARGV) { + if ($arg=~/^\d+$/&&$arg>=50&&$arg<=120) { + if (!defined $battery_want) { + die "battery $arg > 100" if $arg>100; + $battery_want=$arg; + } elsif (!defined $time_want) { + $time_want=$arg; + } else { + die "Excessive battery/time: $arg"; + } + } elsif ($arg=~/^(@?)($timere)$/o) { + ($1?$minutes_want_from:$minutes_want_to)=time_to_minutes $2; + } elsif ($arg eq "p") { # precondition + $precondition=1; + } elsif ($arg eq "r") { + $reset=1; + } elsif ($arg eq "s") { # sleep + $auto_wake=0; + } elsif ($arg=~/^b([01])$/) { # battery-on/off + $battery_direct=$1; + } elsif ($arg=~/^c([01])$/) { # conditioning-on/off + $conditioning=$1; + } elsif ($arg=~/^l([01])$/) { # lock-on/off + $lock=$1; + } elsif ($arg=~/^s([01])$/) { # sentry-on/off + $sentry=$1; + } elsif ($arg=~/^w([012])$/) { # windows-closed/vent/open + $windows_want=$1; + } elsif ($arg eq "dump") { + $dump=1; + } else { + die "Unrecognized arg: $arg"; + } +} +die "No precondition time" if $precondition&&!defined $minutes_want_to; +$car=Tesla::Vehicle->new(auto_wake=>$auto_wake); +say strftime("%T",localtime)." ".$car->name; +cmd "battery_level"; +state; +$cmd_ran=0; +use Data::Dumper;$Data::Dumper::Deepcopy=1;$Data::Dumper::Sortkeys=1;print Dumper $car->data if $dump; +my($sec,$min,$hour)=localtime; +my $minutes=$min+$hour*60; +my $battery_done=$car->battery_level>=($battery_want||$car->charge_limit_soc); +$battery_want//=50 if $battery_done; +$battery_want//=$car->charge_limit_soc; +$time_want//=$battery_want; +$minutes_want_to//=$minutes_want_from; +$sentry//=$lock; +$windows_want//=0 if $lock; +my $battery_level=$car->battery_level; +my $charging_state=$car->charging_state; +my $preconditioning=$car->preconditioning; +my $is_climate_on=$car->is_climate_on; +my $scheduled_charging=$car->scheduled_charging; +my $charge_limit_soc=$car->charge_limit_soc; +cmd "temperatures_set",$TEMPERATURE if ($car->temperature_setting_driver!=$TEMPERATURE||$car->temperature_setting_passenger!=$TEMPERATURE) + &&($precondition||$conditioning||$reset); +if ($precondition) { + cmd "preconditioning_set",$minutes_want_to if ($preconditioning//-1)!=$minutes_want_to; + $preconditioning=$minutes_want_to; +} elsif ($reset||$is_climate_on||defined $minutes_want_to) { + cmd "preconditioning_set",undef if defined $preconditioning; + $preconditioning=undef; +} +my $battery_minutes=ceil(($time_want-$battery_level)*4.1) if $time_want; # 4.2 is too much +if ($battery_done||$battery_direct||(!defined $minutes_want_to&&($reset||$charging_state eq "Charging"))||($battery_minutes//0)<=0) { + cmd "scheduled_charging_set",undef if defined $scheduled_charging; +} elsif (defined $minutes_want_to) { + my $when=$minutes_want_from; + if (!$when) { + $when=($minutes_want_to-$battery_minutes)%(24*60); + my $tolerance=5; + if (($when-$minutes-$tolerance)%(24*60)>=(24*60-3*60)) { + $battery_direct=1 if !defined $battery_direct; + cmd "scheduled_charging_set",undef if defined $scheduled_charging; + $when=undef; + } + } + if (defined $when) { + cmd "scheduled_charging_set",$when if ($scheduled_charging//-1)!=$when; + } +} +print "warning: Car may get charged up to ".($battery_want+3)."%!\n" if $battery_want&&$battery_want>87&&$battery_want<=90; +cmd "charge_limit_set",$battery_want//50 if $battery_want&&$battery_want!=$charge_limit_soc; # charge_limit_set only after scheduled_charging_set! +$windows_want=0 if !defined $windows_want&&($conditioning||(defined $preconditioning&&defined $minutes_want_to&&($minutes_want_to-$minutes)%(24*60)<3*60)); +if (($windows//-3)!=($windows_want//$windows//-3)) { + if (($windows_want//-2)==2) { + print "There is no way to open windows!\n"; + } else { + cmd "windows_set",$windows_want; + } +} +cmd "climate_".($conditioning?"on":"off") if defined $conditioning||($reset&&$is_climate_on); +cmd "charge_" .($battery_direct?"on":"off") if defined $battery_direct; +cmd "doors_".($lock?"lock":"unlock") if defined $lock; +cmd "sentry_".($sentry?"on":"off") if defined $sentry; +state if $cmd_ran; diff --git a/examples/vehicle.pl b/examples/vehicle.pl index 2247dca..06d811f 100755 --- a/examples/vehicle.pl +++ b/examples/vehicle.pl @@ -61,3 +61,10 @@ $car->temperature_outside, $car->is_climate_on ? 'on' : 'off' ); + +my $windows=join ", ",map {0=>"closed",1=>"venting",2=>"opened"}->{$_},map $car->state->{"${_}_window"},qw(fd fp rd rp); +$windows=~s/^([^,]+)(?:, \1)+$/all $1/; +printf( + "Windows are %s.\n", + $windows +); diff --git a/lib/Tesla/Vehicle.pm b/lib/Tesla/Vehicle.pm index 07b288f..37a4db9 100644 --- a/lib/Tesla/Vehicle.pm +++ b/lib/Tesla/Vehicle.pm @@ -179,6 +179,9 @@ sub dashcam { sub locked { return $_[0]->data->{vehicle_state}{locked}; } +sub sentry { + return $_[0]->data->{vehicle_state}{sentry_mode}; +} sub online { my $status = $_[0]->summary->{state}; return $status eq 'online' ? 1 : 0; @@ -285,6 +288,12 @@ sub charging_state { sub minutes_to_full_charge { return $_[0]->data->{charge_state}{minutes_to_full_charge}; } +sub scheduled_charging { + my $mode = $_[0]->data->{charge_state}{scheduled_charging_mode}; + return undef if $mode eq "Off"; + print "Unknown scheduled_charging_mode $mode\n" if $mode ne "StartAt"; + return $_[0]->data->{charge_state}{scheduled_charging_start_time_minutes}; +} # Climate State Methods @@ -342,6 +351,11 @@ sub temperature_setting_passenger { return $_[0]->data->{climate_state}{passenger_temp_setting}; } +sub preconditioning { + return undef if !$_[0]->data->{charge_state}{preconditioning_enabled}; + return ($_[0]->data->{charge_state}{scheduled_departure_time_minutes} - 15) % (24*60); +} + # Command Related Methods sub charge_limit_set { @@ -368,6 +382,123 @@ sub charge_limit_set { return $return->{result}; } +sub charge_on { + my ($self) = @_; + $self->_online_check; + my $return = $self->api(endpoint => 'START_CHARGE', id => $self->id); + + $self->api_cache_clear; + + if (! $return->{result} && $self->warn) { + print "Couldn't turn charge on: '$return->{reason}'\n"; + } + + return $return->{result}; +} +sub charge_off { + my ($self) = @_; + $self->_online_check; + + my $return = $self->api(endpoint => 'STOP_CHARGE', id => $self->id); + + $self->api_cache_clear; + + if (! $return->{result} && $self->warn) { + print "Couldn't turn charge off: '$return->{reason}'\n"; + } + + return $return->{result}; +} + +sub scheduled_charging_set { + my ($self, $minutes) = @_; + + if (defined $minutes && ($minutes < 0 || $minutes >= 24 * 60)) { + croak "scheduled_charging_set() requires minutes integer between 0 and 24*60-1"; + } + + $self->_online_check; + + my $return = $self->api( + endpoint => 'SCHEDULED_CHARGING', + id => $self->id, + api_params => { + enable => defined $minutes ? "true" : "false", + time => $minutes // 0, + }, + ); + + $self->api_cache_clear; + + if (! $return->{result} && $self->warn) { + print "Couldn't schedule charging: '$return->{reason}'\n"; + } + + return $return->{result}; +} + +sub preconditioning_set { + my ($self, $minutes) = @_; + + if (defined $minutes && ($minutes < 0 || $minutes >= 24 * 60)) { + croak "preconditioning_set() requires minutes integer between 0 and 24*60-1"; + } + + $self->_online_check; + + my $return = $self->api( + endpoint => 'SCHEDULED_DEPARTURE', + id => $self->id, + api_params => { + enable => defined $minutes ? "true" : "false", + preconditioning_enabled => defined $minutes ? "true" : "false", + departure_time => !defined $minutes ? 0 : ($minutes + 15) % (24*60), + preconditioning_weekdays_only => "false", + off_peak_charging_enabled => "false", + off_peak_charging_weekdays_only => "false", + end_off_peak_time => 0, + }, + ); + + $self->api_cache_clear; + + if (! $return->{result} && $self->warn) { + print "Couldn't schedule preconditioning: '$return->{reason}'\n"; + } + + return $return->{result}; +} + +sub temperatures_set { + my ($self, $celsius) = @_; + + if (!defined $celsius || $celsius < $self->climate_state->{min_avail_temp} + || $celsius > $self->climate_state->{max_avail_temp}) { + croak "temperatures_set() requires temperature between" + ." ".$self->climate_state->{min_avail_temp} + ." and ".$self->climate_state->{max_avail_temp}; + } + + $self->_online_check; + + my $return = $self->api( + endpoint => 'CHANGE_CLIMATE_TEMPERATURE_SETTING', + id => $self->id, + api_params => { + driver_temp => $celsius, + passenger_temp => $celsius, + }, + ); + + $self->api_cache_clear; + + if (! $return->{result} && $self->warn) { + print "Couldn't set temperatures: '$return->{reason}'\n"; + } + + return $return->{result}; +} + sub bioweapon_mode_toggle { my ($self) = @_; $self->_online_check; @@ -453,6 +584,43 @@ sub doors_unlock { return $return->{result}; } +sub sentry_on { + my ($self) = @_; + $self->_online_check; + + my $return = $self->api( + endpoint => 'SET_SENTRY_MODE', + id => $self->id, + api_params => { on => "true" }, + ); + + $self->api_cache_clear; + + if (! $return->{result} && $self->warn) { + print "Couldn't turn on the sentry: '$return->{reason}'\n"; + } + + return $return->{result}; +} +sub sentry_off { + my ($self) = @_; + $self->_online_check; + + my $return = $self->api( + endpoint => 'SET_SENTRY_MODE', + id => $self->id, + api_params => { on => "false" }, + ); + + $self->api_cache_clear; + + if (! $return->{result} && $self->warn) { + print "Couldn't turn off the sentry: '$return->{reason}'\n"; + } + + return $return->{result}; +} + sub horn_honk { my ($self) = @_; $self->_online_check; @@ -479,6 +647,32 @@ sub lights_flash { return $return->{result}; } +sub windows_set { + my ($self, $state) = @_; + + if (!defined $state || ($state != 0 && $state != 1)) { + croak "windows_set() requires state 0 (close) or 1 (vent)"; + } + + $self->_online_check; + + my $return = $self->api( + endpoint => 'WINDOW_CONTROL', + id => $self->id, + api_params => { + command => $state ? "vent" : "close", + }, + ); + + $self->api_cache_clear; + + if (! $return->{result} && $self->warn) { + print "Couldn't control windows: '$return->{reason}'\n"; + } + + return $return->{result}; +} + sub media_playback_toggle { my ($self) = @_; $self->_online_check; @@ -873,6 +1067,23 @@ Example warning: Couldn't turn volume up: 'user_not_present' +=head2 preconditioning_set($minutes) + +Set time when car should finish conditioning. Time is specified in minutes +since midnight. Tesla normally conditions 15 minutes before the specified time, +this API already compensates it so specify the real time when the conditioning +should finish. The conditioning is unplanned if the parameter is undefined. + +=head2 temperatures_set($celsius) + +Set temperature for both driver and passenger seats. The C<$celsius> parameter +must be set. One cannot set different temperature for driver and passenger. + +Returns true on success. + +Follow up with calls to C and +C to verify. + =head2 bioweapon_mode_toggle Toggles the HVAC Bio Weapon mode on or off. @@ -911,6 +1122,18 @@ Unlocks the car doors. Returns true on success. Follow up with a call to C to verify. +=head2 sentry_on + +Turn on car sentry mode. Returns true on success. + +Follow up with a call to C to verify. + +=head2 sentry_off + +Turn off car sentry mode. Returns true on success. + +Follow up with a call to C to verify. + =head2 horn_honk Honks the horn once. Returns true on success. @@ -921,6 +1144,13 @@ Flashes the exterior lights of the vehicle. Returns true on success. +=head2 windows_set + +Close or vent all windows. Value 1 means vent them all, value 0 means close them all. +It is not possible to open the windows. + +Returns true on success. + =head2 media_playback_toggle Play/Pause the currently loaded audio in the vehicle. @@ -969,6 +1199,27 @@ Returns true if the operation was successful, and false if not. Follow up with a call to C. +=head2 charge_on + +Turns the battery charging on up to the last value of charge_limit_set. + +Returns true on success. + +Follow up with a call to C to verify. + +=head2 charge_off + +Turns the battery charging off. + +Returns true on success. + +Follow up with a call to C to verify. + +=head2 scheduled_charging_set($minutes) + +Schedule charging to start at C<$minutes> since midnight local time. +Cancel scheduled charging if C<$minutes> is undefined. + =head2 trunk_rear_actuate Opens or closes the rear trunk. @@ -1082,6 +1333,10 @@ Returns a string of the state of the dashcam (eg. "Recording"). Returns true if the doors are locked, false if not. +=head2 sentry + +Returns true if the car sentry mode is on, false if not. + =head2 online Returns true if the vehicle is online and ready to communicate, and false if @@ -1238,6 +1493,11 @@ Returns an integer containing the estimated number of minutes to fully charge the batteries, taking into consideration voltage level, Amps requested and drawn etc. +=head2 scheduled_charging + +Returns an integer representing time in minutes since midnight local time to +start charging. Returns undef if no charging is scheduled. + =head1 CLIMATE STATE ATTRIBUTE METHODS =head2 bioweapon_mode @@ -1308,6 +1568,13 @@ What the driver's side temperature setting is set to. What the passenger's side temperature setting is set to. +=head2 preconditioning + +When car should finish its conditioning. If none is set return undef. +Tesla normally conditions 15 minutes before the specified time, this API +already compensates it so the returned time is the real time when the +conditioning will finish. + =head1 AUTHOR Steve Bertrand, C<< >>