diff --git a/config/dev.exs b/config/dev.exs index 514e2e00..5a165620 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -73,3 +73,7 @@ config :tesla_mate, TeslaMate.Repo, database: "tesla_mate_dev", hostname: "localhost", pool_size: 10 + +config :tesla_mate, :tesla_auth, + username: System.get_env("USERNAME"), + password: System.get_env("PASSWOrD") diff --git a/lib/tesla_mate/api.ex b/lib/tesla_mate/api.ex new file mode 100644 index 00000000..c791a6da --- /dev/null +++ b/lib/tesla_mate/api.ex @@ -0,0 +1,97 @@ +defmodule TeslaMate.Api do + use GenServer + + require Logger + import Core.Dependency, only: [call: 3] + alias TeslaApi.{Auth, Error, Vehicle} + + defstruct auth: nil, + deps: %{} + + alias __MODULE__, as: State + + @name __MODULE__ + + if Mix.env() === :prod do + # API + + def start_link(opts) do + GenServer.start_link(__MODULE__, opts, name: Keyword.get(opts, :name, @name)) + end + + def list_vehicles(name \\ @name) do + GenServer.call(name, :list_vehicles) + end + + def charge_state(name \\ @name, vehicle_id) do + GenServer.call(name, {:charge_state, vehicle_id}) + end + + # Callbacks + + @impl true + def init(opts) do + username = Keyword.fetch!(opts, :username) + password = Keyword.fetch!(opts, :password) + + case Auth.login(username, password) do + %Error{error: error, message: reason, env: _env} -> + Logger.error("Login failed: #{inspect({error, reason})}") + {:stop, {error, reason}} + + %Auth{} = auth -> + Logger.info("Login successful") + {:ok, %State{auth: auth}, {:continue, :schedule_refresh}} + end + end + + @impl true + def handle_call({:charge_state, vehicle_id}, _from, state) do + case Vehicle.State.charge_state(state.auth, vehicle_id) do + %Vehicle.State.Charge{} = charge_state -> {:reply, charge_state, state} + %Error{message: reason} -> {:reply, {:error, reason}, state} + end + end + + def handle_call(:list_vehicles, _from, state) do + case Vehicle.list(state.auth) do + vehicles when is_list(vehicles) -> {:reply, {:ok, vehicles}, state} + %Error{message: reason} -> {:reply, {:error, reason}, state} + end + end + + @impl true + def handle_info(:refresh_auth, %State{auth: auth} = state) do + case Auth.refresh(auth) do + %Auth{} = auth -> {:noreply, %State{state | auth: auth}, {:continue, :schedule_refresh}} + %Error{error: error, message: reason, env: _} -> {:stop, {error, reason}} + end + end + + @impl true + def handle_continue(:schedule_refresh, state) do + Process.send_after(self(), :refresh_auth, :timer.hours(24 * 30)) + {:noreply, state} + end + else + # API + + def start_link(opts) do + GenServer.start_link(__MODULE__, opts, name: Keyword.get(opts, :name, @name)) + end + + def charge_state(vehicle_id) do + {:ok, %Vehicle.State.Charge{}} + end + + def list_vehicles do + {:ok, + [%Vehicle{vehicle_id: 0, display_name: "Tesla!M3", option_codes: ["MDL3", "BT37", "DV4W"]}]} + end + + @impl true + def init(opts) do + {:ok, nil} + end + end +end diff --git a/lib/tesla_mate/application.ex b/lib/tesla_mate/application.ex index 2d5386a8..d48f2e4b 100644 --- a/lib/tesla_mate/application.ex +++ b/lib/tesla_mate/application.ex @@ -6,20 +6,14 @@ defmodule TeslaMate.Application do use Application def start(_type, _args) do - # List all child processes to be supervised children = [ - # Start the Ecto repository TeslaMate.Repo, - # Start the endpoint when the application starts - TeslaMateWeb.Endpoint - # Starts a worker by calling: TeslaMate.Worker.start_link(arg) - # {TeslaMate.Worker, arg}, + TeslaMateWeb.Endpoint, + {TeslaMate.Api, auth()}, + TeslaMate.Vehicles ] - # See https://hexdocs.pm/elixir/Supervisor.html - # for other strategies and supported options - opts = [strategy: :one_for_one, name: TeslaMate.Supervisor] - Supervisor.start_link(children, opts) + Supervisor.start_link(children, strategy: :one_for_one, name: TeslaMate.Supervisor) end # Tell Phoenix to update the endpoint configuration @@ -28,4 +22,8 @@ defmodule TeslaMate.Application do TeslaMateWeb.Endpoint.config_change(changed, removed) :ok end + + defp auth do + Application.get_env(:tesla_mate, :tesla_auth) + end end diff --git a/lib/tesla_mate/dependency.ex b/lib/tesla_mate/dependency.ex new file mode 100644 index 00000000..0b65860e --- /dev/null +++ b/lib/tesla_mate/dependency.ex @@ -0,0 +1,4 @@ +defmodule Core.Dependency do + def call({m, a}, fun, args), do: apply(m, fun, [a] ++ args) + def call(m, f, a), do: apply(m, f, a) +end diff --git a/lib/tesla_mate/vehicles.ex b/lib/tesla_mate/vehicles.ex new file mode 100644 index 00000000..45011a9c --- /dev/null +++ b/lib/tesla_mate/vehicles.ex @@ -0,0 +1,28 @@ +defmodule TeslaMate.Vehicles do + use Supervisor + + alias __MODULE__.Vehicle + + @name __MODULE__ + + def start_link(opts) do + Supervisor.start_link(__MODULE__, opts, name: @name) + end + + @impl true + def init(opts) do + vehicles = + Keyword.get_lazy(opts, :vehicles, fn -> + {:ok, vehicles} = TeslaMate.Api.list_vehicles() + vehicles + end) + + children = + vehicles + |> Enum.map(fn %TeslaApi.Vehicle{} = vehicle -> + {Vehicle, vehicle} + end) + + Supervisor.init(children, strategy: :one_for_one, max_restarts: 5, max_seconds: 60) + end +end diff --git a/lib/tesla_mate/vehicles/vehicle.ex b/lib/tesla_mate/vehicles/vehicle.ex new file mode 100644 index 00000000..a9bca424 --- /dev/null +++ b/lib/tesla_mate/vehicles/vehicle.ex @@ -0,0 +1,35 @@ +defmodule TeslaMate.Vehicles.Vehicle do + use GenServer + + require Logger + + alias __MODULE__.Identification + + # import Core.Dependency, only: [call: 3] + + defstruct id: nil, + properties: nil, + deps: %{} + + alias __MODULE__, as: State + + def start_link(opts) do + GenServer.start_link(__MODULE__, opts, []) + end + + @impl true + def init(%TeslaApi.Vehicle{} = vehicle) do + properties = Identification.properties(vehicle) + + Logger.info("Found Vehicle '#{vehicle.display_name}' [#{inspect(properties)}]") + + {:ok, %State{id: vehicle.vehicle_id, properties: properties}, {:continue, :init}} + end + + @impl true + def handle_continue(:init, state) do + {:noreply, state} + end + + # Private +end diff --git a/lib/tesla_mate/vehicles/vehicle/identification.ex b/lib/tesla_mate/vehicles/vehicle/identification.ex new file mode 100644 index 00000000..5dec6dac --- /dev/null +++ b/lib/tesla_mate/vehicles/vehicle/identification.ex @@ -0,0 +1,62 @@ +defmodule TeslaMate.Vehicles.Vehicle.Identification do + def properties(%TeslaApi.Vehicle{} = vehicle) do + performance = vehicle.option_codes |> Enum.find(false, &is_performance/1) + battery = vehicle.option_codes |> Enum.find_value(&which_battery/1) + model = vehicle.option_codes |> Enum.find_value(&which_model/1) + awd = vehicle.option_codes |> Enum.member?("DV4W") + + {name, efficiency} = get_efficiency(model, battery, performance, awd) + + %{ + name: name, + model: model, + battery: battery, + awd: awd, + performance: performance, + efficiency: efficiency + } + end + + defp which_model("MDLS"), do: "MS" + defp which_model("MS01"), do: "MS" + defp which_model("MS02"), do: "MS" + defp which_model("MS03"), do: "MS" + defp which_model("MDLX"), do: "MX" + defp which_model("MDL3"), do: "M3" + defp which_model(______), do: false + + defp is_performance("PX01"), do: true + defp is_performance("P85D"), do: true + defp is_performance("PX6D"), do: true + defp is_performance("X024"), do: true + defp is_performance("PBT8"), do: true + defp is_performance(______), do: nil + + defp which_battery("BT" <> _ = battery), do: battery + defp which_battery(_), do: nil + + defp get_efficiency("MS", "BTX5", _, true), do: {"S 75D", 0.186} + defp get_efficiency("MS", "BTX5", _, false), do: {"S 75", 0.185} + defp get_efficiency("MS", "BTX4", true, _), do: {"S P90D", 0.200} + defp get_efficiency("MS", "BTX4", false, _), do: {"S 90D", 0.189} + defp get_efficiency("MS", "BTX6", true, _), do: {"S P100D", 0.200} + defp get_efficiency("MS", "BTX6", false, _), do: {"S 100D", 0.189} + defp get_efficiency("MS", "BTX8", _, true), do: {"S 75D (85kWh)", 0.186} + defp get_efficiency("MS", "BTX8", _, false), do: {"S 75 (85kWh)", 0.185} + defp get_efficiency("MS", "BT85", _, _), do: {"S 85 ?", 0.200} + defp get_efficiency("MS", "BT70", _, _), do: {"S 70 ?", 0.200} + defp get_efficiency("MS", "BT60", _, _), do: {"S 60 ?", 0.200} + defp get_efficiency("MS", _, _, _), do: {"S ???", 0.200} + + defp get_efficiency("MX", "BTX5", _, _), do: {"X 75D", 0.208} + defp get_efficiency("MX", "BTX4", true, _), do: {"X 90D", 0.208} + defp get_efficiency("MX", "BTX4", false, _), do: {"X P90D", 0.217} + defp get_efficiency("MX", "BTX6", true, _), do: {"X P100D", 0.226} + defp get_efficiency("MX", "BTX6", false, _), do: {"X 100D", 0.208} + defp get_efficiency("MX", _, _, _), do: {"X ???", 0.208} + + defp get_efficiency("M3", "BT37", _, _), do: {"M3 LR", 0.153} + defp get_efficiency("M3", _, _, _), do: {"M3 ???", 0.153} + + defp get_efficiency(_, _, _, _), do: {"???", nil} +end diff --git a/mix.exs b/mix.exs index 58a9fc77..ca20daaf 100644 --- a/mix.exs +++ b/mix.exs @@ -33,6 +33,7 @@ defmodule TeslaMate.MixProject do # Type `mix help deps` for examples and options. defp deps do [ + # Default {:phoenix, "~> 1.4.3"}, {:phoenix_pubsub, "~> 1.1"}, {:phoenix_ecto, "~> 4.0"}, @@ -43,6 +44,8 @@ defmodule TeslaMate.MixProject do {:gettext, "~> 0.11"}, {:jason, "~> 1.0"}, {:plug_cowboy, "~> 2.0"}, + # Custom + {:tesla_api, git: "https://github.com/mgwidmann/tesla_api"} ] end diff --git a/mix.lock b/mix.lock new file mode 100644 index 00000000..a768eabf --- /dev/null +++ b/mix.lock @@ -0,0 +1,34 @@ +%{ + "certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, + "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, + "cowboy": {:hex, :cowboy, "2.6.1", "f2e06f757c337b3b311f9437e6e072b678fcd71545a7b2865bdaa154d078593f", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, + "cowlib": {:hex, :cowlib, "2.7.0", "3ef16e77562f9855a2605900cedb15c1462d76fb1be6a32fc3ae91973ee543d2", [:rebar3], [], "hexpm"}, + "db_connection": {:hex, :db_connection, "2.0.6", "bde2f85d047969c5b5800cb8f4b3ed6316c8cb11487afedac4aa5f93fd39abfa", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, + "decimal": {:hex, :decimal, "1.7.0", "30d6b52c88541f9a66637359ddf85016df9eb266170d53105f02e4a67e00c5aa", [:mix], [], "hexpm"}, + "ecto": {:hex, :ecto, "3.0.8", "9eb6a1fcfc593e6619d45ef51afe607f1554c21ca188a1cd48eecc27223567f1", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, + "ecto_sql": {:hex, :ecto_sql, "3.0.5", "7e44172b4f7aca4469f38d7f6a3da394dbf43a1bcf0ca975e958cb957becd74e", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.6", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, + "file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], [], "hexpm"}, + "gettext": {:hex, :gettext, "0.16.1", "e2130b25eebcbe02bb343b119a07ae2c7e28bd4b146c4a154da2ffb2b3507af2", [:mix], [], "hexpm"}, + "hackney": {:hex, :hackney, "1.14.3", "b5f6f5dcc4f1fba340762738759209e21914516df6be440d85772542d4a5e412", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, + "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, + "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, + "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, + "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, + "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, + "phoenix": {:hex, :phoenix, "1.4.3", "8eed4a64ff1e12372cd634724bddd69185938f52c18e1396ebac76375d85677d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "phoenix_html": {:hex, :phoenix_html, "2.13.2", "f5d27c9b10ce881a60177d2b5227314fc60881e6b66b41dfe3349db6ed06cf57", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.0", "3bb31a9fbd40ffe8652e60c8660dffd72dd231efcdf49b744fb75b9ef7db5dd2", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"}, + "plug": {:hex, :plug, "1.7.2", "d7b7db7fbd755e8283b6c0a50be71ec0a3d67d9213d74422d9372effc8e87fd1", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.0.2", "6055f16868cc4882b24b6e1d63d2bada94fb4978413377a3b32ac16c18dffba2", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, + "postgrex": {:hex, :postgrex, "0.14.1", "63247d4a5ad6b9de57a0bac5d807e1c32d41e39c04b8a4156a26c63bcd8a2e49", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, + "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, + "telemetry": {:hex, :telemetry, "0.3.0", "099a7f3ce31e4780f971b4630a3c22ec66d22208bc090fe33a2a3a6a67754a73", [:rebar3], [], "hexpm"}, + "tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, 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]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, + "tesla_api": {:git, "https://github.com/mgwidmann/tesla_api", "15001942824940b808b6d67672d2b47bf2297623", []}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, +}