Include customized :tesla_api package

This commit is contained in:
Adrian Kumpf
2019-12-13 18:25:26 +01:00
parent e25b965304
commit d301ac86f6
19 changed files with 672 additions and 75 deletions

20
coveralls.json Normal file
View File

@@ -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"
]
}

81
lib/tesla_api.ex Normal file
View File

@@ -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

78
lib/tesla_api/auth.ex Normal file
View File

@@ -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

4
lib/tesla_api/error.ex Normal file
View File

@@ -0,0 +1,4 @@
defmodule TeslaApi.Error do
@enforce_keys [:reason]
defstruct [:reason, :message, :env]
end

59
lib/tesla_api/vehicle.ex Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -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}

View File

@@ -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),

View File

@@ -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)}

View File

@@ -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)

View File

@@ -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"},

View File

@@ -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"},

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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 ""

View File

@@ -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

View File

@@ -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, [],

View File

@@ -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()}
]

View File

@@ -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)