From 42ac182b97e25597aeba2300c368123dd712f13f Mon Sep 17 00:00:00 2001 From: Adrian Kumpf Date: Sat, 19 Dec 2020 20:47:23 +0100 Subject: [PATCH] Use Tesla client with custom auth middleware --- lib/tesla_api.ex | 84 +++----------------------------- lib/tesla_api/auth.ex | 44 +++++++++-------- lib/tesla_api/auth/middleware.ex | 14 ++++++ lib/tesla_api/vehicle.ex | 48 ++++++++++++++++-- 4 files changed, 87 insertions(+), 103 deletions(-) create mode 100644 lib/tesla_api/auth/middleware.ex diff --git a/lib/tesla_api.ex b/lib/tesla_api.ex index 9d6e20ad..2a03f180 100644 --- a/lib/tesla_api.ex +++ b/lib/tesla_api.ex @@ -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 diff --git a/lib/tesla_api/auth.ex b/lib/tesla_api/auth.ex index 27158f06..d774328f 100644 --- a/lib/tesla_api/auth.ex +++ b/lib/tesla_api/auth.ex @@ -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} -> diff --git a/lib/tesla_api/auth/middleware.ex b/lib/tesla_api/auth/middleware.ex new file mode 100644 index 00000000..260ca818 --- /dev/null +++ b/lib/tesla_api/auth/middleware.ex @@ -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 diff --git a/lib/tesla_api/vehicle.ex b/lib/tesla_api/vehicle.ex index 63f2473a..c99bc803 100644 --- a/lib/tesla_api/vehicle.ex +++ b/lib/tesla_api/vehicle.ex @@ -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