Tesla.psm1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# credit to HJespers for https://github.com/hjespers/teslams

#region Globals
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version 2.0

[string]$TeslaPSModule_Uri = 'https://owner-api.teslamotors.com'
[string]$apiUri = "$TeslaPSModule_Uri/api/1"
[string]$TeslaPSModule_VehicleId = $null

[Hashtable]$headers = $null
[string]$activity = 'TeslaPSModule'

# emulate the android mobile app
$version = '2.1.79'; 
$model = 'SM-G900V'; 
$codename = 'REL'; 
$release = '4.4.4'; 
$locale = 'en_US'; 
[string]$user_agent = "Model S $version ($model; Android $codename $release; $locale)"
#endregion Globals

#region Utility
function Status
{
    [CmdletBinding()]
    param(
        [string][parameter(Position=0,Mandatory=$true)]$Status
        )
    Write-Verbose -Message "$activity`: $Status"
    Write-Progress -Activity $activity -Status $Status
}

function CtoF([double][parameter(Mandatory=$true)]$celsius)
{
    return [Math]::Round($celsius * (9.0 / 5.0) + 32)
}
#endregion Utility

#region Invoke
function InvokeCarCommand
{
    [CmdletBinding()]
    param(
        [string][parameter(Mandatory=$true)]$command
        )
    GetConnection
    Status "Sending $command to vehicle..."
    $uri = "$apiUri/vehicles/$TeslaPSModule_VehicleId/command/$Command"
    Write-Verbose -Message $uri
    $resp = Invoke-RestMethod -Uri $uri `
                              -Method Post `
                              -Headers $headers `
                              -UserAgent $user_agent `
                              -ContentType 'application/json'
    Write-Debug -Message $resp
    Write-Debug -Message $resp.response
    if ($resp.response.result -ne "true")
    {
        throw "Error calling $command. Reason returned: ""$($resp.response.reason)"""
    }

    $script:m_delayTime = 5
}

function InvokeTeslaDataRequest
{
    [CmdletBinding()]
    param(
        [string][parameter(Mandatory=$true)]$Command
        )
    if (-not $activity)
    {
        $activity = "$($MyInvocation.InvocationName): $Command"
    }
    GetConnection
    $uri = "$apiUri/vehicles/$TeslaPSModule_VehicleId/data_request/$Command"
    Write-Verbose -Message "Sending command $uri"

    Status "Invoking $Command"
    $resp = Invoke-RestMethod -Uri $uri `
                              -Method Get `
                              -Headers $headers `
                              -UserAgent $user_agent `
                              -ContentType 'application/json'
    Write-Debug -Message $resp
    Write-Debug -Message $resp.response
    Write-Output -InputObject $resp.response

    # CODEWORK still need to implement this $script:m_delayTime = 5
}
#endregion Invoke

#region Connect
function Connect-Tesla
{
<#
.SYNOPSIS
Connect to a vehicle
.DESCRIPTION
Connect to one Tesla vehicle. You must specify the credentials you use
to connect with the Tesla website, email address and password.
The credentials will be cached securely, so it should only be
necessary to call this once for any computer+user.
However you may need to invoke this again if you change
either your Tesla password or your Windows password.
.PARAMETER Credential
Specify the credentials you use to connect with the Tesla website,
email address and password.
.PARAMETER VehicleIndex
Specify this if you have more than one vehicle and want to connect
with a different vehicle than the first.
.PARAMETER VIN
Specify this if you have more than one vehicle and want to specify
the VIN of the specific vehicle.
.PARAMETER NoPersist
This will prevent your credentials from being cached.
They will only be effective for this PowerShell session.
.LINK
Get-Tesla
Set-Tesla
#>

    [CmdletBinding(DefaultParameterSetName='VehicleIndex')]
    param(
        [PSCredential][parameter(Mandatory=$true,Position=0)]$Credential,
        [int][parameter(ParameterSetName='VehicleIndex')]$VehicleIndex = 0,
        [string][parameter(ParameterSetName='VIN')]$VIN = '',
        [switch]$NoPersist
        )

    $activity = $MyInvocation.InvocationName
    $passwordBstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Credential.Password)
    $plaintextpwd = [Runtime.InteropServices.Marshal]::PtrToStringAuto($passwordBstr)
    [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($passwordBstr);

    $encr = '76492d1116743f0423413b16050a5345MgB8AFcATgBuAFcASABCAE8AWQA5AHUAbwArAFoASQBBAEwAOQBLAGwATgAwAHcAPQA9AHwAMQA5ADEAOAA0
AGEAMwBmAGEAMgAzAGUAMAAwAGQAMwAwAGEAMQBhADAAMwBhAGIAYwBlAGUAYwAzADAAMwAxADQANgBjADYAMgAxADkAMwBiADcAOAA3ADQAZQBkADQAMwA5ADkAZQA3A
GUAZQA4AGEAZQAwADIAYQAwADEAYgA5ADkAMwAyAGYAZAA4ADgAMQAxADMAYQA0ADEAOAA3ADAAZABjADIAOQA3AGUAMwA5ADUAMwA4ADIANAA0ADIANwAwAGEAYgBhAG
YAMgA2ADIAZQBjADUANgA1AGYAMAAwADMANwBmADgAOQAwAGEAMABhADAANABlAGYAOAA2ADIAZgA1ADEANwAyADIAZAAxAGEAYQBjADcAYQBiADUAYQBhADUAMgBlADM
AMQA4ADkAYgA4ADMAZgAzADYAZgBlAGUANwA4ADUANQA5ADIAMwBlADIAYwAyADQAMQAxAGYANwBhAGIAZQAwADgAZAA0AGUAZgAzAGYAYQAxADQAMQA5ADYAMQAwADcA
OABiADEAZAA5AGUANwAyADcAZgA0ADkAMABkAGUANwA0AGIANwBhADMAMAA2ADUAOQBiADQAOAA5AGYAOABiAGEAZABiADkAZgBhAGYAZQA0ADQAMQA1AGYANwBkAGQAN
gBlAGMAYQA5ADUANAA3ADEAMwAxADQAYwAxADQAMgA0AGMAOABjADAAZABiADgANwA4ADUAMABmADkANgBiADIAYQA5AGIAOQA3ADkAZQBmADQAMABiADIAYQAxAGEAYQ
BlADEAZgBhAGMANgAzAGEAMgA0ADkAYgA3AGMAMgBlAGIAZgA3ADAAZQA2AGUAOABiADgAZAAxADAAOAA4AGQAMgA4ADEAMwAxAGIAMwAyAGEAMwBjADMAOQAyAGIAMgA
wADYAYwBkADkANgBkADUAYQA3AGUAMgBiAGIANAAwADUANAA3ADQAZAA0AGEAYwA0ADEANwBlADYAOABkADgANgA0AGYANQBhADIAZAA5AGYAZQAzAGEANQBmADAAZQA3
ADQAZgA4AGEAZQA4AGUANwAzAGYAYwAyAGQAMgA0AGUAZgBkAGMAMQBhADkANAA1ADQAMwAyAGEAYQBkAGEAYgAwADAANgAxADAAMwBkADkAYwBjAGUAZAA2ADkAZQAzA
DcAMwBiAGMAMQA0AGIAMAA3ADcAMgBiAGIAOQA2AGQANQBlADcANwBiADYAYQA2ADMAMAA1ADQANQAyADcANQA3ADEAMABlADUANQAzAGMAZgBjADUAZAA1ADQAMQA5AG
IANQA0ADIAYgA4ADQANwAxADgAZQA1AGMAMwBmAGMAMQAzADIAMAAwADQAYgBkADAAMgA0AGQANgA0AGMAOABmAGYAMgAyAGMAOAAzADcANgA3AGIAMgBiAGYAYwBjADQ
ANQA5AGQAYgA2AGQAYgA='

    [byte[]]$k = ('236 231 222 136 19 9 157 113 158 51 236 240 116 17 176 100 91 179 20 162 238 103 10 192 113 251 135 59 95 82 109 114'.Split(' ')) -as [byte[]]
    $ps = ConvertTo-SecureString -String $encr -Key $k
    $b = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ps)
    $p = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($b)

    $loginBody = @{
        'grant_type' = 'password'
        'client_id' = ($p.Split(',')[0])
        'client_secret' = ($p.Split(',')[1])
        'email' = $Credential.UserName
        'password' = $plaintextpwd
        }
    Status "Login GET"
    $r = Invoke-RestMethod -Uri "$TeslaPSModule_Uri/oauth/token" `
                           -Method Post `
                           -Body $loginBody
    $r | Out-String | Write-Debug
    $access_token = $r.access_token
    $script:headers = @{
        'Authorization' = "Bearer $access_token"
        'Accept-Encoding' = 'gzip,deflate'
        }

    Status "Get vehicle list"
    $resp = Invoke-RestMethod -Uri "$apiUri/vehicles" `
                              -Method Get `
                              -UserAgent $user_agent `
                              -Headers $headers
    Write-Debug -Message $resp
    $vehicles = $resp.response
    $vehicles | Write-Debug
    Status "Received $($vehicles.Count) vehicles in response"
    if ($VIN)
    {
        for ($i = 0; $i -lt $vehicles.Count; $i++)
        {
            Status "Vehicle $i has VIN $($vehicle.VIN)"
            $vehicle = $vehicles[$i]
            if ($vehicle.VIN -eq $VIN)
            {
                Status "VIN match for vehicle $i"
                $VehicleIndex = $i;
                break
            }
        }
        if ($i -ge $vehicles.Count)
        {
            throw "$activity`: Vehicle with VIN $VIN not found"
        }
    }
    $vehicle = $vehicles[$VehicleIndex]
    $vehicleId = $vehicle.id
    Status "VehicleID is $vehicleId"

    if ($vehicle.state -ne 'online') {
        if ($vehicle.state -ne 'asleep')
        {
            throw "$activity`: Current vehicle state is $($vehicle.state). Please try again later."
        }

        # The wake_up command will return error 408 (vehicle unavailable).
        try {
            Status "Waking vehicle"
            InvokeCarCommand wake_up
        }
        catch {
            Write-Debug -Message "$activity`: Exception $_"
            # Do nothing
        }

        # Check vehicle state periodically and continue when it's "online"
        do {
            Start-Sleep -Seconds 5
            Status "Checking whether vehicle woke up yet"
            $vehicle = Invoke-RestMethod -Uri "$apiUri/vehicles" `
                                         -Method Get `
                                         -UserAgent $user_agent `
                                         -Headers $headers
            $vehicle = $vehicle | Where-Object id -eq $vehicleId
            Write-Verbose -Message "Vehicle state is $($vehicle.state)."
        }
        while ($vehicle.state -ne 'online')
    }

    Status "Caching connection"
    $script:TeslaPSModule_VehicleId = $vehicleId

    if (-not $NoPersist)
    {
        $fileName = Join-Path -Path $env:APPDATA -ChildPath 'TeslaPSModule_CachedConnection.xml'
        Status "Persisting cached connection to $fileName"
        $connection = New-Object -TypeName PSObject -Property @{
            Email = $Credential.UserName
            Password = $plaintextpwd
            VIN = $vehicle.VIN
            }
        $xmlContent = ConvertTo-Xml -InputObject $connection -As String
        Write-Verbose -Message "$activity`: Writing connection to file $fileName"
        $xmlContent = ConvertTo-SecureString -String $xmlContent -AsPlainText -Force | ConvertFrom-SecureString
        Set-Content -Path $fileName -Value $xmlContent -ErrorAction Stop
    }

    Write-Progress -Activity $activity -Status 'Completed' -Completed
}

function GetConnection
{
    [CmdletBinding()]
    param(
        )
    if ($headers -and $TeslaPSModule_VehicleId)
    {
        Write-Verbose -Message "Connection already cached"
        return
    }
    $path = Join-Path -Path $env:APPDATA -ChildPath 'TeslaPSModule_CachedConnection.xml'
    if (-not (Test-Path -Path $path -ErrorAction SilentlyContinue))
    {
        Status "You must first call Connect-Tesla"
        throw "You must first call Connect-Tesla"
    }

    Status "Reading cached connection from $path"
    try
    {
        $fileContent = Get-Content -Path $path -ErrorAction Stop
        $secureString = ConvertTo-SecureString -String $fileContent -ErrorAction SilentlyContinue
        $fileContentDecrypted = $secureString `
            | ForEach-Object {[Runtime.InteropServices.Marshal]::PtrToStringAuto( `
                    [Runtime.InteropServices.Marshal]::SecureStringToBSTR($_))}
        $xmlContent = [xml]$fileContentDecrypted

        $email = ($xmlContent.Objects.Object.Property | Where-Object Name -eq "Email").'#text'
        $password = ($xmlContent.Objects.Object.Property | Where-Object Name -eq "Password").'#text'
        $VIN = ($xmlContent.Objects.Object.Property | Where-Object Name -eq "VIN").'#text'
        $securePassword = ConvertTo-SecureString -String $password -AsPlainText -Force
        $credential = New-Object -TypeName PSCredential -ArgumentList $email,$securePassword
    }
    catch
    {
        throw "Error reading cached connection; retry Connect-Tesla. Error is: $_"
    }
    Connect-Tesla -Credential $credential -VIN $VIN -NoPersist
}
#endregion Connect

function Get-Tesla
{
<#
.SYNOPSIS
Retrieve information about a Tesla vehicle
.DESCRIPTION
Retrieve information about a Tesla vehicle in a specific category.
You must first call Connect-Tesla once to cache connection information
for this computer+user. Connection information will be encrypted and
cached in your user profile.
.PARAMETER Command
Specify the category of information you want to retrieve.
.LINK
Connect-Tesla
Set-Tesla
#>

    [CmdletBinding()]
    param(
        [parameter(Mandatory=$true,Position=0)]
        [ValidateSet('charge_state',
                     'climate_state',
                     'drive_state',
                     'gui_settings',
                     'vehicle_state',
                     'vehicles'
                     )]
        [string]$Command
        )
    $activity = "$($MyInvocation.InvocationName): $Command"
    GetConnection
    if ($Command -eq 'vehicles')
    {
        Status "Reading vehicle list"
        $result = Invoke-RestMethod -Uri "$apiUri/vehicles" `
                                    -Method Get `
                                    -UserAgent $user_agent `
                                    -Headers $headers
        return $result.response
    }
    InvokeTeslaDataRequest -Command $Command
    Write-Progress -Activity $activity -Status 'Completed' -Completed
}

function Set-Tesla
{
<#
.SYNOPSIS
Change one setting of a Tesla vehicle
.DESCRIPTION
Change one setting of a Tesla vehicle.
You must first call Connect-Tesla once to cache connection information
for this computer+user. Connection information will be encrypted and
cached in your user profile.
.PARAMETER Command
Specify the command you want to issue.
.NOTES
Not yet implemented:
set_charge_limit <percent>
set_temps <driver_temp> <passenger_temp>
sun_roof_control (open | close | comfort | vent | move <percent>)
sun_roof_control move <percent>
streaming response from https://streaming.vn.teslamotors.com/stream/...
.LINK
Connect-Tesla
Get-Tesla
#>

    [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Low')]
    param(
        [parameter(Mandatory=$true,Position=0)]
        [ValidateSet('auto_conditioning_start',
                     'auto_conditioning_stop',
                     'door_lock',
                     'door_unlock',
                     'charge_port_door_open',
                     'charge_max_range',
                     'charge_standard',
                     'charge_start',
                     'charge_stop',
                     'flash_lights',
                     'honk_horn',
                     'wake_up'
                     )]
        [string]$Command
        )
    $activity = "$($MyInvocation.InvocationName): $Command"
    if ($PSCmdlet.ShouldProcess($Command))
    {
        InvokeCarCommand $Command
    }
    Write-Progress -Activity $activity -Status 'Completed' -Completed
}

Export-ModuleMember Connect-Tesla,Get-Tesla,Set-Tesla