Use Tesla client with custom auth middleware

This commit is contained in:
Adrian Kumpf
2020-12-19 20:47:23 +01:00
parent df6b96b1c8
commit 42ac182b97
4 changed files with 87 additions and 103 deletions
+6 -78
View File
@@ -1,82 +1,10 @@
defmodule TeslaApi do
alias Finch.Response, as: Res
alias TeslaMate.HTTP
alias __MODULE__.Error
use Tesla
@base_url URI.parse("https://owner-api.teslamotors.com/")
@user_agent "github.com/adriankumpf/teslamate"
@timeout 30_000
adapter Tesla.Adapter.Finch, name: TeslaMate.HTTP, receive_timeout: 35_000
def get(path, token, opts \\ []) when is_binary(token) do
headers = [{"user-agent", @user_agent}, {"Authorization", "Bearer " <> token}]
case HTTP.get(url(path), headers: headers, receive_timeout: @timeout) do
{:ok, %Res{} = response} ->
case decode_body(response) do
%Res{status: status, body: %{"response" => res}} when status in 200..299 ->
transform = Keyword.get(opts, :transform, & &1)
{:ok, if(is_list(res), do: Enum.map(res, transform), else: transform.(res))}
%Res{status: 401} = env ->
{:error, %Error{reason: :unauthorized, env: env}}
%Res{status: 404, body: %{"error" => "not_found"}} = env ->
{:error, %Error{reason: :vehicle_not_found, env: env}}
%Res{status: 405, body: %{"error" => "vehicle is curently in service"}} = env ->
{:error, %Error{reason: :vehicle_in_service, env: env}}
%Res{status: 408, body: %{"error" => "vehicle unavailable:" <> _}} = env ->
{:error, %Error{reason: :vehicle_unavailable, env: env}}
%Res{status: 504} = env ->
{:error, %Error{reason: :timeout, env: env}}
%Res{status: status, body: %{"error" => msg}} = env when status >= 500 ->
{:error, %Error{reason: :unknown, message: msg, env: env}}
%Res{body: body} = env ->
{:error, %Error{reason: :unknown, message: inspect(body), env: env}}
end
{:error, %Mint.TransportError{reason: reason} = transport_error} ->
{:error, %Error{reason: reason, message: Exception.message(transport_error)}}
{:error, %Mint.HTTPError{reason: reason} = transport_error} ->
{:error, %Error{reason: reason, message: Exception.message(transport_error)}}
end
end
def post(path, token, params) when is_map(params) do
body = Jason.encode!(params)
headers = [
{"user-agent", @user_agent},
{"content-type", "application/json"}
| if(is_nil(token), do: [], else: [{"Authorization", "Bearer " <> token}])
]
with {:ok, response} <-
HTTP.post(url(path), body, headers: headers, receive_timeout: @timeout) do
{:ok, decode_body(response)}
end
end
## Private
defp url(path) do
@base_url
|> Map.put(:path, path)
|> URI.to_string()
end
defp decode_body(%Res{body: body} = response) do
body =
case Jason.decode(body) do
{:ok, decoded_body} -> decoded_body
{:error, _reason} -> body
end
Map.put(response, :body, body)
end
plug Tesla.Middleware.BaseUrl, "https://owner-api.teslamotors.com"
plug Tesla.Middleware.Headers, [{"user-agent", "github.com/adriankumpf/teslamate"}]
plug TeslaApi.Auth.Middleware
plug Tesla.Middleware.JSON
end
+23 -21
View File
@@ -1,6 +1,4 @@
defmodule TeslaApi.Auth do
import TeslaApi
require Logger
alias TeslaApi.{Auth, Error}
@@ -23,40 +21,44 @@ defmodule TeslaApi.Auth do
end
def legacy_login(email, password) do
post("/oauth/token", nil, %{
"grant_type" => "password",
"client_id" => @client_id,
"client_secret" => @client_secret,
"email" => email,
"password" => password
})
data = %{
grant_type: "password",
client_id: @client_id,
client_secret: @client_secret,
email: email,
password: password
}
TeslaApi.post("/oauth/token", data)
|> handle_response()
end
def refresh(%Auth{token: token, refresh_token: refresh_token}) do
post("/oauth/token", token, %{
"grant_type" => "refresh_token",
"client_id" => @client_id,
"client_secret" => @client_secret,
"refresh_token" => refresh_token
})
data = %{
grant_type: "refresh_token",
client_id: @client_id,
client_secret: @client_secret,
refresh_token: refresh_token
}
TeslaApi.post("/oauth/token", data, opts: [access_token: token])
|> handle_response()
end
def revoke(%Auth{token: token}) do
post("/oauth/revoke", token, %{"token" => token})
TeslaApi.post("/oauth/revoke", %{token: token}, opts: [access_token: token])
|> handle_response()
end
defp handle_response(response) do
case response do
{:ok, %Finch.Response{status: 200, body: body}} when body == %{} ->
{:ok, %Tesla.Env{status: 200, body: body}} when body == %{} ->
:ok
{:ok, %Finch.Response{status: 200, body: %{"response" => true}}} ->
{:ok, %Tesla.Env{status: 200, body: %{"response" => true}}} ->
:ok
{:ok, %Finch.Response{status: 200, body: body}} when is_map(body) ->
{:ok, %Tesla.Env{status: 200, body: body}} when is_map(body) ->
auth = %__MODULE__{
token: body["access_token"],
type: body["token_type"],
@@ -67,7 +69,7 @@ defmodule TeslaApi.Auth do
{:ok, auth}
{:ok, %Finch.Response{status: 401} = e} ->
{:ok, %Tesla.Env{status: 401} = e} ->
error = %Error{
reason: :authentication_failure,
message: "Failed to authenticate.",
@@ -76,7 +78,7 @@ defmodule TeslaApi.Auth do
{:error, error}
{:ok, %Finch.Response{} = e} ->
{:ok, %Tesla.Env{} = e} ->
{:error, %Error{reason: :unknown, message: "An unknown error has occurred.", env: e}}
{:error, %{reason: reason} = e} ->
+14
View File
@@ -0,0 +1,14 @@
defmodule TeslaApi.Auth.Middleware do
@behaviour Tesla.Middleware
@impl Tesla.Middleware
def call(%Tesla.Env{} = env, next, _opts) do
env =
case env.opts[:access_token] do
nil -> env
token -> Tesla.put_header(env, "Authorization", "Bearer " <> token)
end
Tesla.run(env, next)
end
end
+44 -4
View File
@@ -1,6 +1,6 @@
defmodule TeslaApi.Vehicle do
alias __MODULE__.State.{Charge, Climate, Drive, VehicleConfig, VehicleState}
alias TeslaApi.Auth
alias TeslaApi.{Auth, Error}
defstruct id: nil,
vehicle_id: nil,
@@ -23,15 +23,18 @@ defmodule TeslaApi.Vehicle do
vehicle_state: nil
def list(%Auth{token: token}) do
TeslaApi.get("/api/1/vehicles", token, transform: &result/1)
TeslaApi.get("/api/1/vehicles", opts: [access_token: token])
|> handle_response(transform: &result/1)
end
def get(%Auth{token: token}, id) do
TeslaApi.get("/api/1/vehicles/#{id}", token, transform: &result/1)
TeslaApi.get("/api/1/vehicles/#{id}", opts: [access_token: token])
|> handle_response(transform: &result/1)
end
def get_with_state(%Auth{token: token}, id) do
TeslaApi.get("/api/1/vehicles/#{id}/vehicle_data", token, transform: &result/1)
TeslaApi.get("/api/1/vehicles/#{id}/vehicle_data", opts: [access_token: token])
|> handle_response(transform: &result/1)
end
def result(v) do
@@ -56,4 +59,41 @@ defmodule TeslaApi.Vehicle do
vehicle_state: if(v["vehicle_state"], do: VehicleState.result(v["vehicle_state"]))
}
end
defp handle_response({:ok, %Tesla.Env{} = env}, opts) do
case env do
%Tesla.Env{status: status, body: %{"response" => res}} when status in 200..299 ->
transform = Keyword.get(opts, :transform, & &1)
{:ok, if(is_list(res), do: Enum.map(res, transform), else: transform.(res))}
%Tesla.Env{status: 401} = env ->
{:error, %Error{reason: :unauthorized, env: env}}
%Tesla.Env{status: 404, body: %{"error" => "not_found"}} = env ->
{:error, %Error{reason: :vehicle_not_found, env: env}}
%Tesla.Env{status: 405, body: %{"error" => "vehicle is curently in service"}} = env ->
{:error, %Error{reason: :vehicle_in_service, env: env}}
%Tesla.Env{status: 408, body: %{"error" => "vehicle unavailable:" <> _}} = env ->
{:error, %Error{reason: :vehicle_unavailable, env: env}}
%Tesla.Env{status: 504} = env ->
{:error, %Error{reason: :timeout, env: env}}
%Tesla.Env{status: status, body: %{"error" => msg}} = env when status >= 500 ->
{:error, %Error{reason: :unknown, message: msg, env: env}}
%Tesla.Env{body: body} = env ->
{:error, %Error{reason: :unknown, message: inspect(body), env: env}}
end
end
defp handle_response({:error, reason}, _opts) when is_atom(reason) do
{:error, %Error{reason: reason}}
end
defp handle_response({:error, reason}, _opts) do
{:error, %Error{reason: :unknown, message: reason}}
end
end