From d301ac86f68af2a1f5b0ae78175acf1bca8d5ef7 Mon Sep 17 00:00:00 2001 From: Adrian Kumpf Date: Fri, 13 Dec 2019 18:25:26 +0100 Subject: [PATCH] Include customized :tesla_api package --- coveralls.json | 20 + lib/tesla_api.ex | 81 ++++ lib/tesla_api/auth.ex | 78 ++++ lib/tesla_api/error.ex | 4 + lib/tesla_api/vehicle.ex | 59 +++ lib/tesla_api/vehicle/state.ex | 365 ++++++++++++++++++ lib/teslamate/api.ex | 60 +-- lib/teslamate/vehicles.ex | 2 +- lib/teslamate/vehicles/vehicle.ex | 2 +- lib/teslamate_web/live/car_live/summary.ex | 3 +- mix.exs | 2 - mix.lock | 1 - priv/gettext/de/LC_MESSAGES/default.po | 6 +- priv/gettext/default.pot | 6 +- priv/gettext/en/LC_MESSAGES/default.po | 6 +- test/support/mocks/tesla_api.ex | 2 +- test/teslamate/api_errors_test.exs | 34 +- test/teslamate/vehicles/vehicle_test.exs | 14 +- .../controllers/car_controller_test.exs | 2 +- 19 files changed, 672 insertions(+), 75 deletions(-) create mode 100644 coveralls.json create mode 100644 lib/tesla_api.ex create mode 100644 lib/tesla_api/auth.ex create mode 100644 lib/tesla_api/error.ex create mode 100644 lib/tesla_api/vehicle.ex create mode 100644 lib/tesla_api/vehicle/state.ex diff --git a/coveralls.json b/coveralls.json new file mode 100644 index 00000000..ce5213a4 --- /dev/null +++ b/coveralls.json @@ -0,0 +1,20 @@ +{ + "coverage_options": { + "treat_no_relevant_lines_as_covered": true, + "output_dir": "cover/", + "minimum_coverage": 0 + }, + + "terminal_options": { + "file_column_width": 40 + }, + + "skip_files": [ + "lib/tesla_api", + "lib/teslamate/release.ex", + "lib/teslamate/application.ex", + "lib/teslamate/custom_expressions.ex", + "lib/teslamate_web.ex" + ] +} + diff --git a/lib/tesla_api.ex b/lib/tesla_api.ex new file mode 100644 index 00000000..461d0ae0 --- /dev/null +++ b/lib/tesla_api.ex @@ -0,0 +1,81 @@ +defmodule TeslaApi do + alias Mojito.Response, as: Res + alias Mojito.Error, as: Err + alias __MODULE__.Error + + @base_url URI.parse("https://owner-api.teslamotors.com/") + @user_agent "github.com/adriankumpf/teslamate" + @timeout 60_000 + + def get(path, token, opts \\ []) when is_binary(token) do + headers = [{"user-agent", @user_agent}, {"Authorization", "Bearer " <> token}] + + case Mojito.get(url(path), headers, timeout: @timeout) do + {:ok, %Res{} = response} -> + case decode_body(response) do + %Res{complete: false} = env -> + {:error, %Error{reason: :incomplete_response, env: env}} + + %Res{status_code: 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_code: 401} = env -> + {:error, %Error{reason: :unauthorized, env: env}} + + %Res{status_code: 404, body: %{"error" => "not_found"}} = env -> + {:error, %Error{reason: :vehicle_not_found, env: env}} + + %Res{status_code: 405, body: %{"error" => "vehicle is curently in service"}} = env -> + {:error, %Error{reason: :vehicle_in_service, env: env}} + + %Res{status_code: 408, body: %{"error" => "vehicle unavailable:" <> _}} = env -> + {:error, %Error{reason: :vehicle_unavailable, env: env}} + + %Res{status_code: 504} = env -> + {:error, %Error{reason: :timeout, env: env}} + + %Res{status_code: 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, %Err{reason: reason, message: msg}} -> + {:error, %Error{reason: reason, message: msg || "An unknown error has occurred"}} + 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} <- path |> url() |> Mojito.post(headers, body, 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 +end diff --git a/lib/tesla_api/auth.ex b/lib/tesla_api/auth.ex new file mode 100644 index 00000000..106efff3 --- /dev/null +++ b/lib/tesla_api/auth.ex @@ -0,0 +1,78 @@ +defmodule TeslaApi.Auth do + import TeslaApi + + alias TeslaApi.{Auth, Error} + + defstruct [:token, :type, :expires_in, :refresh_token, :created_at] + + @client_id "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384" + @client_secret "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3" + + def login(email, password) do + post("/oauth/token", nil, %{ + "grant_type" => "password", + "client_id" => @client_id, + "client_secret" => @client_secret, + "email" => email, + "password" => password + }) + |> 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 + }) + |> handle_response() + end + + def revoke(%Auth{token: token}) do + post("/oauth/revoke", token, %{"token" => token}) + |> handle_response() + end + + defp handle_response(response) do + case response do + {:ok, %Mojito.Response{status_code: 200, body: body}} when body == %{} -> + :ok + + {:ok, %Mojito.Response{status_code: 200, body: %{"response" => true}}} -> + :ok + + {:ok, %Mojito.Response{status_code: 200, body: body}} when is_map(body) -> + auth = %__MODULE__{ + token: body["access_token"], + type: body["token_type"], + expires_in: body["expires_in"], + refresh_token: body["refresh_token"], + created_at: body["created_at"] + } + + {:ok, auth} + + {:ok, %Mojito.Response{status_code: 401} = e} -> + error = %Error{ + reason: :authentication_failure, + message: "Failed to authenticate.", + env: e + } + + {:error, error} + + {:ok, %Mojito.Response{} = e} -> + {:error, %Error{reason: :unknown, message: "An unknown error has occurred.", env: e}} + + {:error, %Mojito.Error{reason: reason} = e} -> + error = %Error{ + reason: :unknown, + message: "An unknown error has occurred: #{inspect(reason)}", + env: e + } + + {:error, error} + end + end +end diff --git a/lib/tesla_api/error.ex b/lib/tesla_api/error.ex new file mode 100644 index 00000000..3122cd1b --- /dev/null +++ b/lib/tesla_api/error.ex @@ -0,0 +1,4 @@ +defmodule TeslaApi.Error do + @enforce_keys [:reason] + defstruct [:reason, :message, :env] +end diff --git a/lib/tesla_api/vehicle.ex b/lib/tesla_api/vehicle.ex new file mode 100644 index 00000000..48143790 --- /dev/null +++ b/lib/tesla_api/vehicle.ex @@ -0,0 +1,59 @@ +defmodule TeslaApi.Vehicle do + alias __MODULE__.State.{Charge, Climate, Drive, VehicleConfig, VehicleState} + alias TeslaApi.Auth + + defstruct id: nil, + vehicle_id: nil, + vin: nil, + tokens: [], + state: "unknown", + option_codes: [], + in_service: false, + display_name: nil, + color: nil, + calendar_enabled: nil, + backseat_token: nil, + backseat_token_updated_at: nil, + api_version: nil, + charge_state: nil, + climate_state: nil, + drive_state: nil, + gui_settings: nil, + vehicle_config: nil, + vehicle_state: nil + + def list(%Auth{token: token}) do + TeslaApi.get("/api/1/vehicles", token, transform: &vehicle/1) + end + + def get(%Auth{token: token}, id) do + TeslaApi.get("/api/1/vehicles/#{id}", token, transform: &vehicle/1) + end + + def get_with_state(%Auth{token: token}, id) do + TeslaApi.get("/api/1/vehicles/#{id}/vehicle_data", token, transform: &vehicle/1) + end + + defp vehicle(v) do + %__MODULE__{ + id: v["id"], + vehicle_id: v["vehicle_id"], + vin: v["vin"], + tokens: v["tokens"], + state: v["state"] || "unknown", + option_codes: String.split(v["option_codes"] || "", ","), + in_service: v["in_service"], + display_name: v["display_name"], + color: v["color"], + calendar_enabled: v["calendar_enabled"], + backseat_token: v["backseat_token"], + backseat_token_updated_at: v["backseat_token_updated_at"], + api_version: v["api_version"], + charge_state: if(v["charge_state"], do: Charge.result(v["charge_state"])), + climate_state: if(v["climate_state"], do: Climate.result(v["climate_state"])), + drive_state: if(v["drive_state"], do: Drive.result(v["drive_state"])), + vehicle_config: if(v["vehicle_config"], do: VehicleConfig.result(v["vehicle_config"])), + vehicle_state: if(v["vehicle_state"], do: VehicleState.result(v["vehicle_state"])) + } + end +end diff --git a/lib/tesla_api/vehicle/state.ex b/lib/tesla_api/vehicle/state.ex new file mode 100644 index 00000000..a7e0db9e --- /dev/null +++ b/lib/tesla_api/vehicle/state.ex @@ -0,0 +1,365 @@ +defmodule TeslaApi.Vehicle.State do + defmodule Charge do + defstruct [ + :charge_miles_added_rated, + :charge_current_request, + :charger_power, + :managed_charging_start_time, + :charger_phases, + :charge_energy_added, + :charger_voltage, + :fast_charger_type, + :time_to_full_charge, + :ideal_battery_range, + :usable_battery_level, + :scheduled_charging_pending, + :charger_actual_current, + :est_battery_range, + :charge_limit_soc_min, + :charge_port_door_open, + :managed_charging_active, + :charge_limit_soc_max, + :fast_charger_present, + :fast_charger_brand, + :scheduled_charging_start_time, + :conn_charge_cable, + :timestamp, + :user_charge_enable_request, + :charge_port_cold_weather_mode, + :charge_to_max_range, + :max_range_charge_counter, + :charge_limit_soc_std, + :charge_port_latch, + :managed_charging_user_canceled, + :charger_pilot_current, + :trip_charging, + :battery_range, + :charging_state, + :charge_rate, + :not_enough_power_to_heat, + :charge_limit_soc, + :charge_enable_request, + :charge_current_request_max, + :battery_level, + :charge_miles_added_ideal, + :battery_heater_on + ] + + def result(charge) when is_map(charge) do + %__MODULE__{ + charge_miles_added_rated: charge["charge_miles_added_rated"], + charge_current_request: charge["charge_current_request"], + charger_power: charge["charger_power"], + managed_charging_start_time: charge["managed_charging_start_time"], + charger_phases: charge["charger_phases"], + charge_energy_added: charge["charge_energy_added"], + charger_voltage: charge["charger_voltage"], + fast_charger_type: charge["fast_charger_type"], + time_to_full_charge: charge["time_to_full_charge"], + ideal_battery_range: charge["ideal_battery_range"], + usable_battery_level: charge["usable_battery_level"], + scheduled_charging_pending: charge["scheduled_charging_pending"], + charger_actual_current: charge["charger_actual_current"], + est_battery_range: charge["est_battery_range"], + charge_limit_soc_min: charge["charge_limit_soc_min"], + charge_port_door_open: charge["charge_port_door_open"], + managed_charging_active: charge["managed_charging_active"], + charge_limit_soc_max: charge["charge_limit_soc_max"], + fast_charger_present: charge["fast_charger_present"], + fast_charger_brand: charge["fast_charger_brand"], + scheduled_charging_start_time: charge["scheduled_charging_start_time"], + conn_charge_cable: charge["conn_charge_cable"], + timestamp: charge["timestamp"], + user_charge_enable_request: charge["user_charge_enable_request"], + charge_port_cold_weather_mode: charge["charge_port_cold_weather_mode"], + charge_to_max_range: charge["charge_to_max_range"], + max_range_charge_counter: charge["max_range_charge_counter"], + charge_limit_soc_std: charge["charge_limit_soc_std"], + charge_port_latch: charge["charge_port_latch"], + managed_charging_user_canceled: charge["managed_charging_user_canceled"], + charger_pilot_current: charge["charger_pilot_current"], + trip_charging: charge["trip_charging"], + battery_range: charge["battery_range"], + charging_state: charge["charging_state"], + charge_rate: charge["charge_rate"], + not_enough_power_to_heat: charge["not_enough_power_to_heat"], + charge_limit_soc: charge["charge_limit_soc"], + charge_enable_request: charge["charge_enable_request"], + charge_current_request_max: charge["charge_current_request_max"], + battery_level: charge["battery_level"], + charge_miles_added_ideal: charge["charge_miles_added_ideal"], + battery_heater_on: charge["battery_heater_on"] + } + end + end + + defmodule Climate do + defstruct [ + :battery_heater, + :battery_heater_no_power, + :climate_keeper_mode, + :defrost_mode, + :driver_temp_setting, + :fan_status, + :inside_temp, + :is_auto_conditioning_on, + :is_climate_on, + :is_front_defroster_on, + :is_preconditioning, + :is_rear_defroster_on, + :left_temp_direction, + :max_avail_temp, + :min_avail_temp, + :outside_temp, + :passenger_temp_setting, + :remote_heater_control_enabled, + :right_temp_direction, + :seat_heater_left, + :seat_heater_rear_center, + :seat_heater_rear_left, + :seat_heater_rear_right, + :seat_heater_rear_left_back, + :seat_heater_rear_right_back, + :seat_heater_right, + :side_mirror_heaters, + :steering_wheel_heater, + :smart_preconditioning, + :timestamp, + :wiper_blade_heater + ] + + def result(climate) when is_map(climate) do + %__MODULE__{ + battery_heater: climate["battery_heater"], + battery_heater_no_power: climate["battery_heater_no_power"], + climate_keeper_mode: climate["climate_keeper_mode"], + defrost_mode: climate["defrost_mode"], + driver_temp_setting: climate["driver_temp_setting"], + fan_status: climate["fan_status"], + inside_temp: climate["inside_temp"], + is_auto_conditioning_on: climate["is_auto_conditioning_on"], + is_climate_on: climate["is_climate_on"], + is_front_defroster_on: climate["is_front_defroster_on"], + is_preconditioning: climate["is_preconditioning"], + is_rear_defroster_on: climate["is_rear_defroster_on"], + left_temp_direction: climate["left_temp_direction"], + max_avail_temp: climate["max_avail_temp"], + min_avail_temp: climate["min_avail_temp"], + outside_temp: climate["outside_temp"], + passenger_temp_setting: climate["passenger_temp_setting"], + remote_heater_control_enabled: climate["remote_heater_control_enabled"], + right_temp_direction: climate["right_temp_direction"], + seat_heater_left: climate["seat_heater_left"], + seat_heater_rear_center: climate["seat_heater_rear_center"], + seat_heater_rear_left: climate["seat_heater_rear_left"], + seat_heater_rear_right: climate["seat_heater_rear_right"], + seat_heater_rear_left_back: climate["seat_heater_rear_left_back"], + seat_heater_rear_right_back: climate["seat_heater_rear_right_back"], + seat_heater_right: climate["seat_heater_right"], + side_mirror_heaters: climate["side_mirror_heaters"], + steering_wheel_heater: climate["steering_wheel_heater"], + smart_preconditioning: climate["smart_preconditioning"], + timestamp: climate["timestamp"], + wiper_blade_heater: climate["wiper_blade_heater"] + } + end + end + + defmodule Drive do + defstruct [ + :gps_as_of, + :heading, + :latitude, + :longitude, + :native_latitude, + :native_location_supported, + :native_longitude, + :native_type, + :power, + :shift_state, + :speed, + :timestamp + ] + + def result(drive) when is_map(drive) do + %__MODULE__{ + gps_as_of: drive["gps_as_of"], + heading: drive["heading"], + latitude: drive["latitude"], + longitude: drive["longitude"], + native_latitude: drive["native_latitude"], + native_location_supported: drive["native_location_supported"], + native_longitude: drive["native_longitude"], + native_type: drive["native_type"], + power: drive["power"], + shift_state: drive["shift_state"], + speed: drive["speed"], + timestamp: drive["timestamp"] + } + end + end + + defmodule VehicleConfig do + defstruct [ + :can_accept_navigation_requests, + :can_actuate_trunks, + :car_special_type, + :car_type, + :charge_port_type, + :eu_vehicle, + :exterior_color, + :has_air_suspension, + :has_ludicrous_mode, + :key_version, + :motorized_charge_port, + :perf_config, + :plg, + :rear_seat_heaters, + :rear_seat_type, + :rhd, + :roof_color, + :seat_type, + :spoiler_type, + :sun_roof_installed, + :third_row_seats, + :timestamp, + :trim_badging, + :use_range_badging, + :wheel_type + ] + + def result(vehicle_config) when is_map(vehicle_config) do + %__MODULE__{ + can_accept_navigation_requests: vehicle_config["can_accept_navigation_requests"], + can_actuate_trunks: vehicle_config["can_actuate_trunks"], + car_special_type: vehicle_config["car_special_type"], + car_type: vehicle_config["car_type"], + charge_port_type: vehicle_config["charge_port_type"], + eu_vehicle: vehicle_config["eu_vehicle"], + exterior_color: vehicle_config["exterior_color"], + has_air_suspension: vehicle_config["has_air_suspension"], + has_ludicrous_mode: vehicle_config["has_ludicrous_mode"], + key_version: vehicle_config["key_version"], + motorized_charge_port: vehicle_config["motorized_charge_port"], + perf_config: vehicle_config["perf_config"], + plg: vehicle_config["plg"], + rear_seat_heaters: vehicle_config["rear_seat_heaters"], + rear_seat_type: vehicle_config["rear_seat_type"], + rhd: vehicle_config["rhd"], + roof_color: vehicle_config["roof_color"], + seat_type: vehicle_config["seat_type"], + spoiler_type: vehicle_config["spoiler_type"], + sun_roof_installed: vehicle_config["sun_roof_installed"], + third_row_seats: vehicle_config["third_row_seats"], + timestamp: vehicle_config["timestamp"], + trim_badging: vehicle_config["trim_badging"], + use_range_badging: vehicle_config["use_range_badging"], + wheel_type: vehicle_config["wheel_type"] + } + end + end + + defmodule VehicleState do + defstruct [ + :api_version, + :autopark_state_v3, + :autopark_style, + :calendar_supported, + :car_version, + :center_display_state, + :df, + :dr, + :ft, + :homelink_device_count, + :homelink_nearby, + :is_user_present, + :last_autopark_error, + :locked, + :notifications_supported, + :odometer, + :parsed_calendar_supported, + :pf, + :pr, + :remote_start, + :remote_start_enabled, + :remote_start_supported, + :rt, + :fd_window, + :fp_window, + :rd_window, + :rp_window, + :sentry_mode, + :sentry_mode_available, + :smart_summon_available, + :software_update, + :summon_standby_mode_enabled, + :sun_roof_percent_open, + :sun_roof_state, + :timestamp, + :valet_mode, + :valet_pin_needed, + :vehicle_name + ] + + defmodule SoftwareUpdate do + defstruct [ + :download_perc, + :expected_duration_sec, + :install_perc, + :scheduled_time_ms, + :status, + :version + ] + end + + def result(vehicle_state) when is_map(vehicle_state) do + %__MODULE__{ + api_version: vehicle_state["api_version"], + autopark_state_v3: vehicle_state["autopark_state_v3"], + autopark_style: vehicle_state["autopark_style"], + calendar_supported: vehicle_state["calendar_supported"], + car_version: vehicle_state["car_version"], + center_display_state: vehicle_state["center_display_state"], + df: vehicle_state["df"], + dr: vehicle_state["dr"], + ft: vehicle_state["ft"], + homelink_device_count: vehicle_state["homelink_device_count"], + homelink_nearby: vehicle_state["homelink_nearby"], + is_user_present: vehicle_state["is_user_present"], + last_autopark_error: vehicle_state["last_autopark_error"], + locked: vehicle_state["locked"], + notifications_supported: vehicle_state["notifications_supported"], + odometer: vehicle_state["odometer"], + parsed_calendar_supported: vehicle_state["parsed_calendar_supported"], + pf: vehicle_state["pf"], + pr: vehicle_state["pr"], + remote_start: vehicle_state["remote_start"], + remote_start_enabled: vehicle_state["remote_start_enabled"], + remote_start_supported: vehicle_state["remote_start_supported"], + rt: vehicle_state["rt"], + software_update: %SoftwareUpdate{ + download_perc: vehicle_state["software_update"]["download_perc"], + expected_duration_sec: vehicle_state["software_update"]["expected_duration_sec"], + install_perc: vehicle_state["software_update"]["install_perc"], + scheduled_time_ms: vehicle_state["software_update"]["scheduled_time_ms"], + status: vehicle_state["software_update"]["status"], + version: vehicle_state["software_update"]["version"] + }, + summon_standby_mode_enabled: vehicle_state["summon_standby_mode_enabled"], + sun_roof_percent_open: vehicle_state["sun_roof_percent_open"], + sun_roof_state: vehicle_state["sun_roof_state"], + timestamp: vehicle_state["timestamp"], + valet_mode: vehicle_state["valet_mode"], + fd_window: vehicle_state["fd_window"], + fp_window: vehicle_state["fp_window"], + rd_window: vehicle_state["rd_window"], + rp_window: vehicle_state["rp_window"], + sentry_mode: vehicle_state["sentry_mode"], + sentry_mode_available: vehicle_state["sentry_mode_available"], + smart_summon_available: vehicle_state["smart_summon_available"], + valet_pin_needed: vehicle_state["valet_pin_needed"], + vehicle_name: vehicle_state["vehicle_name"] + } + end + end +end diff --git a/lib/teslamate/api.ex b/lib/teslamate/api.ex index 87890049..2915e28d 100644 --- a/lib/teslamate/api.ex +++ b/lib/teslamate/api.ex @@ -7,12 +7,15 @@ defmodule TeslaMate.Api do alias TeslaMate.Auth alias TeslaMate.Vehicles + alias Mojito.Response + import Core.Dependency, only: [call: 3, call: 2] defstruct auth: nil, deps: %{}, refs: %{} alias __MODULE__, as: State @name __MODULE__ + @timeout 65_000 # API @@ -23,15 +26,15 @@ defmodule TeslaMate.Api do ## State def list_vehicles(name \\ @name) do - GenServer.call(name, :list, 35_000) + GenServer.call(name, :list, @timeout) end def get_vehicle(name \\ @name, id) do - GenServer.call(name, {:get, id}, 35_000) + GenServer.call(name, {:get, id}, @timeout) end def get_vehicle_with_state(name \\ @name, id) do - GenServer.call(name, {:get_with_state, id}, 35_000) + GenServer.call(name, {:get_with_state, id}, @timeout) end ## Internals @@ -71,7 +74,7 @@ defmodule TeslaMate.Api do :ok = call(state.deps.vehicles, :restart) {:reply, :ok, %State{state | auth: auth}, {:continue, :schedule_refresh}} - {:error, %TeslaApi.Error{error: reason}} -> + {:error, %TeslaApi.Error{reason: reason}} -> {:reply, {:error, reason}, state} end end @@ -103,55 +106,28 @@ defmodule TeslaMate.Api do end @impl true - def handle_info({ref, {:list, result}}, %State{refs: refs} = state) do + def handle_info({ref, {cmd, result}}, %State{refs: refs} = state) + when cmd in [:list, :get, :get_with_state] do {reply, state} = case result do - {:error, %TeslaApi.Error{env: %Tesla.Env{status: 401}}} -> + {:error, %TeslaApi.Error{reason: :unauthorized}} -> {{:error, :not_signed_in}, %State{state | auth: nil}} - {:error, %TeslaApi.Error{error: reason, env: %Tesla.Env{status: status, body: body}}} -> + {:error, %TeslaApi.Error{reason: reason, env: %Response{status_code: status, body: body}}} -> Logger.error("TeslaApi.Error / #{status} – #{inspect(body, pretty: true)}") {{:error, reason}, state} - {:error, %TeslaApi.Error{error: reason, message: _msg}} -> + {:error, %TeslaApi.Error{reason: reason, message: msg}} -> + if is_binary(msg) and msg != "", do: Logger.warn("TeslaApi.Error / #{msg}") {{:error, reason}, state} - {:ok, vehicles} -> + {:ok, vehicles} when is_list(vehicles) -> vehicles = vehicles |> Task.async_stream(&preload_vehicle(&1, state), timeout: 32_500) |> Enum.map(fn {:ok, vehicle} -> vehicle end) {{:ok, vehicles}, state} - end - - {from, refs} = Map.pop(refs, ref) - GenServer.reply(from, reply) - - {:noreply, %State{state | refs: refs}} - end - - def handle_info({ref, {_cmd, result}}, %State{refs: refs} = state) do - {reply, state} = - case result do - {:error, %TeslaApi.Error{env: %Tesla.Env{status: 401}}} -> - {{:error, :not_signed_in}, %State{state | auth: nil}} - - {:error, %TeslaApi.Error{env: %Tesla.Env{status: 404, body: %{"error" => "not_found"}}}} -> - {{:error, :vehicle_not_found}, state} - - {:error, - %TeslaApi.Error{ - env: %Tesla.Env{status: 405, body: %{"error" => "vehicle is curently in service"}} - }} -> - {{:error, :in_service}, state} - - {:error, %TeslaApi.Error{error: reason, env: %Tesla.Env{status: status, body: body}}} -> - Logger.error("TeslaApi.Error / #{status} – #{inspect(body, pretty: true)}") - {{:error, reason}, state} - - {:error, %TeslaApi.Error{error: reason, message: _msg}} -> - {{:error, reason}, state} {:ok, %TeslaApi.Vehicle{} = vehicle} -> {{:ok, vehicle}, state} @@ -171,8 +147,8 @@ defmodule TeslaMate.Api do :ok = call(state.deps.auth, :save, [auth]) {:noreply, %State{state | auth: auth}, {:continue, :schedule_refresh}} - {:error, %TeslaApi.Error{error: error, message: reason, env: _}} -> - {:stop, {error, reason}} + {:error, %TeslaApi.Error{reason: reason, message: message}} -> + {:stop, {reason, message}} end end @@ -184,11 +160,12 @@ defmodule TeslaMate.Api do with %Tokens{access: access, refresh: refresh} <- call(state.deps.auth, :get_tokens), api_auth = %TeslaApi.Auth{token: access, refresh_token: refresh}, {:ok, %TeslaApi.Auth{} = auth} <- call(state.deps.tesla_api_auth, :refresh, [api_auth]) do + Logger.info("Refreshed api tokens") :ok = call(state.deps.auth, :save, [auth]) {:noreply, %State{state | auth: auth}, {:continue, :schedule_refresh}} else nil -> - Logger.info("Please sign in.") + Logger.info("Please sign in") {:noreply, state} {:error, %TeslaApi.Error{} = error} -> @@ -204,6 +181,7 @@ defmodule TeslaMate.Api do |> round() |> :timer.seconds() + Logger.info("Scheduling token refresh in #{round(ms / (24 * 60 * 60 * 1000))}d") Process.send_after(self(), :refresh_auth, ms) {:noreply, state} diff --git a/lib/teslamate/vehicles.ex b/lib/teslamate/vehicles.ex index 7caf2f86..97dfb064 100644 --- a/lib/teslamate/vehicles.ex +++ b/lib/teslamate/vehicles.ex @@ -94,7 +94,7 @@ defmodule TeslaMate.Vehicles do end defp create_or_update!(%TeslaApi.Vehicle{} = vehicle) do - Logger.info("Found '#{vehicle.display_name}'") + unless is_nil(name = vehicle.display_name), do: Logger.info("Starting logger for '#{name}'") {:ok, car} = with nil <- Log.get_car_by(vin: vehicle.vin), diff --git a/lib/teslamate/vehicles/vehicle.ex b/lib/teslamate/vehicles/vehicle.ex index 74faf297..e77251be 100644 --- a/lib/teslamate/vehicles/vehicle.ex +++ b/lib/teslamate/vehicles/vehicle.ex @@ -265,7 +265,7 @@ defmodule TeslaMate.Vehicles.Vehicle do Logger.warn("Error / connection closed", car_id: data.car.id) {:keep_state_and_data, schedule_fetch(5)} - {:error, :in_service} -> + {:error, :vehicle_in_service} -> Logger.info("Vehicle is currently in service", car_id: data.car.id) {:keep_state_and_data, schedule_fetch(60)} diff --git a/lib/teslamate_web/live/car_live/summary.ex b/lib/teslamate_web/live/car_live/summary.ex index 75561b17..92919fca 100644 --- a/lib/teslamate_web/live/car_live/summary.ex +++ b/lib/teslamate_web/live/car_live/summary.ex @@ -99,12 +99,13 @@ defmodule TeslaMateWeb.CarLive.Summary do defp translate_error(:preconditioning), do: gettext("Preconditioning") defp translate_error(:user_present), do: gettext("Driver present") defp translate_error(:update_in_progress), do: gettext("Update in progress") - defp translate_error(:unknown), do: gettext("An error occurred") defp translate_error(:timeout), do: gettext("Timeout") defp translate_error(:sleep_mode_disabled_at_location), do: gettext("Sleep Mode is disabled at current location") + defp translate_error(_other), do: gettext("An error occurred") + defp cancel_timer(nil), do: :ok defp cancel_timer(ref) when is_reference(ref), do: Process.cancel_timer(ref) diff --git a/mix.exs b/mix.exs index 721223ef..172b444e 100644 --- a/mix.exs +++ b/mix.exs @@ -46,8 +46,6 @@ defmodule TeslaMate.MixProject do {:gettext, "~> 0.11"}, {:jason, "~> 1.0"}, {:plug_cowboy, "~> 2.0"}, - # Custom - {:tesla_api, github: "adriankumpf/tesla_api", branch: "v10"}, {:gen_state_machine, "~> 2.0"}, {:ecto_enum, "~> 1.0"}, {:phoenix_live_view, "~> 0.1"}, diff --git a/mix.lock b/mix.lock index b61407f3..d1cb9c85 100644 --- a/mix.lock +++ b/mix.lock @@ -47,7 +47,6 @@ "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"}, "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm"}, "tesla": {:hex, :tesla, "1.3.0", "f35d72f029e608f9cdc6f6d6fcc7c66cf6d6512a70cfef9206b21b8bd0203a30", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 0.4", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, - "tesla_api": {:git, "https://github.com/adriankumpf/tesla_api.git", "da8941cd4b93530d1a059116cd047a30a9132b81", [branch: "v10"]}, "tortoise": {:hex, :tortoise, "0.9.4", "3bca5f4475f80a5bd45aab644ddb3364dd0956b67f4202dcef6ed20e045ea3a6", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm"}, diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index 4bdcab5a..74cc3bb8 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -405,16 +405,16 @@ msgid "Sleep Mode Enabled" msgstr "Schlafmodus aktiviert" #, elixir-format -#: lib/teslamate_web/live/car_live/summary.ex:106 +#: lib/teslamate_web/live/car_live/summary.ex:105 msgid "Sleep Mode is disabled at current location" msgstr "Schlafmodus ist am aktuellen Ort deaktiviert" #, elixir-format -#: lib/teslamate_web/live/car_live/summary.ex:102 +#: lib/teslamate_web/live/car_live/summary.ex:107 msgid "An error occurred" msgstr "Ein Fehler ist aufgetreten" #, elixir-format -#: lib/teslamate_web/live/car_live/summary.ex:103 +#: lib/teslamate_web/live/car_live/summary.ex:102 msgid "Timeout" msgstr "" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index b1beddd3..ca5d72af 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -404,16 +404,16 @@ msgid "Sleep Mode Enabled" msgstr "" #, elixir-format -#: lib/teslamate_web/live/car_live/summary.ex:106 +#: lib/teslamate_web/live/car_live/summary.ex:105 msgid "Sleep Mode is disabled at current location" msgstr "" #, elixir-format -#: lib/teslamate_web/live/car_live/summary.ex:102 +#: lib/teslamate_web/live/car_live/summary.ex:107 msgid "An error occurred" msgstr "" #, elixir-format -#: lib/teslamate_web/live/car_live/summary.ex:103 +#: lib/teslamate_web/live/car_live/summary.ex:102 msgid "Timeout" msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index a2e60fd4..1c35c07b 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -405,16 +405,16 @@ msgid "Sleep Mode Enabled" msgstr "" #, elixir-format -#: lib/teslamate_web/live/car_live/summary.ex:106 +#: lib/teslamate_web/live/car_live/summary.ex:105 msgid "Sleep Mode is disabled at current location" msgstr "" #, elixir-format -#: lib/teslamate_web/live/car_live/summary.ex:102 +#: lib/teslamate_web/live/car_live/summary.ex:107 msgid "An error occurred" msgstr "" #, elixir-format -#: lib/teslamate_web/live/car_live/summary.ex:103 +#: lib/teslamate_web/live/car_live/summary.ex:102 msgid "Timeout" msgstr "" diff --git a/test/support/mocks/tesla_api.ex b/test/support/mocks/tesla_api.ex index ba834322..40034002 100644 --- a/test/support/mocks/tesla_api.ex +++ b/test/support/mocks/tesla_api.ex @@ -27,7 +27,7 @@ defmodule TeslaApi.AuthMock do %State{pid: pid} = state ) do send(pid, {TeslaApi.AuthMock, event}) - {:reply, {:error, %TeslaApi.Error{error: :induced_error}}, state} + {:reply, {:error, %TeslaApi.Error{reason: :induced_error, message: "foo"}}, state} end def handle_call(event, _from, %State{pid: pid} = state) do diff --git a/test/teslamate/api_errors_test.exs b/test/teslamate/api_errors_test.exs index 43f0dc2a..9a2a79cc 100644 --- a/test/teslamate/api_errors_test.exs +++ b/test/teslamate/api_errors_test.exs @@ -9,7 +9,9 @@ defmodule TeslaMate.ApiErrorsTest do @valid_credentials %Credentials{email: "teslamate", password: "foo"} test "sign_in", %{test: name} do - login = fn _email, _password -> {:error, %TeslaApi.Error{error: :unauthorized}} end + login = fn _email, _password -> + {:error, %TeslaApi.Error{reason: :unauthorized, env: %Mojito.Response{}}} + end with_mock TeslaApi.Auth, login: login do :ok = start_real_api(name) @@ -21,9 +23,15 @@ defmodule TeslaMate.ApiErrorsTest do vehicle_mock = {TeslaApi.Vehicle, [], [ - list: fn _ -> {:error, %TeslaApi.Error{env: %Tesla.Env{status: 401}}} end, - get: fn _, _ -> {:error, %TeslaApi.Error{env: %Tesla.Env{status: 401}}} end, - get_with_state: fn _, _ -> {:error, %TeslaApi.Error{env: %Tesla.Env{status: 401}}} end + list: fn _ -> + {:error, %TeslaApi.Error{reason: :unauthorized, env: %Mojito.Response{}}} + end, + get: fn _, _ -> + {:error, %TeslaApi.Error{reason: :unauthorized, env: %Mojito.Response{}}} + end, + get_with_state: fn _, _ -> + {:error, %TeslaApi.Error{reason: :unauthorized, env: %Mojito.Response{}}} + end ]} with_mocks [auth_mock(), vehicle_mock] do @@ -40,8 +48,9 @@ defmodule TeslaMate.ApiErrorsTest do end end + @tag :capture_log test ":vehicle_not_found", %{test: name} do - api_error = %TeslaApi.Error{env: %Tesla.Env{status: 404, body: %{"error" => "not_found"}}} + api_error = %TeslaApi.Error{reason: :vehicle_not_found, env: %Mojito.Response{}} vehicle_mock = {TeslaApi.Vehicle, [], @@ -61,7 +70,11 @@ defmodule TeslaMate.ApiErrorsTest do @tag :capture_log test "other error witn Env", %{test: name} do - api_error = %TeslaApi.Error{error: :unkown, env: %Tesla.Env{status: 503, body: ""}} + api_error = %TeslaApi.Error{ + reason: :unknown, + message: "", + env: %Mojito.Response{status_code: 503, body: ""} + } vehicle_mock = {TeslaApi.Vehicle, [], @@ -75,14 +88,15 @@ defmodule TeslaMate.ApiErrorsTest do :ok = start_real_api(name) assert :ok = Api.sign_in(@valid_credentials) - assert {:error, :unkown} = Api.list_vehicles() - assert {:error, :unkown} = Api.get_vehicle(0) - assert {:error, :unkown} = Api.get_vehicle_with_state(0) + assert {:error, :unknown} = Api.list_vehicles() + assert {:error, :unknown} = Api.get_vehicle(0) + assert {:error, :unknown} = Api.get_vehicle_with_state(0) end end + @tag :capture_log test "other error witnout Env", %{test: name} do - api_error = %TeslaApi.Error{error: :closed, message: "foo"} + api_error = %TeslaApi.Error{reason: :closed, message: "foo"} vehicle_mock = {TeslaApi.Vehicle, [], diff --git a/test/teslamate/vehicles/vehicle_test.exs b/test/teslamate/vehicles/vehicle_test.exs index 008324b7..3f4f6481 100644 --- a/test/teslamate/vehicles/vehicle_test.exs +++ b/test/teslamate/vehicles/vehicle_test.exs @@ -3,10 +3,10 @@ defmodule TeslaMate.Vehicles.VehicleTest do describe "starting" do @tag :capture_log - test "handles unkown and faulty states", %{test: name} do + test "handles unknown and faulty states", %{test: name} do events = [ {:ok, %TeslaApi.Vehicle{state: "unknown"}}, - {:error, %TeslaApi.Error{message: "boom"}} + {:error, %TeslaApi.Error{reason: :boom, message: "boom"}} ] :ok = start_vehicle(name, events) @@ -237,11 +237,11 @@ defmodule TeslaMate.Vehicles.VehicleTest do events = [ {:ok, online_event()}, {:ok, online_event()}, - {:error, :in_service}, - {:error, :in_service}, - {:error, :in_service}, - {:error, :in_service}, - {:error, :in_service}, + {:error, :vehicle_in_service}, + {:error, :vehicle_in_service}, + {:error, :vehicle_in_service}, + {:error, :vehicle_in_service}, + {:error, :vehicle_in_service}, {:ok, online_event()}, {:ok, online_event()} ] diff --git a/test/teslamate_web/controllers/car_controller_test.exs b/test/teslamate_web/controllers/car_controller_test.exs index b6363208..3d5e4ee2 100644 --- a/test/teslamate_web/controllers/car_controller_test.exs +++ b/test/teslamate_web/controllers/car_controller_test.exs @@ -321,7 +321,7 @@ defmodule TeslaMateWeb.CarControllerTest do @tag :signed_in test "renders current vehicle stats [:unavailable]", %{conn: conn} do events = [ - {:error, :unkown} + {:error, :unknown} ] :ok = start_vehicles(events)