Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle sleep behavior of MCU2 upgraded cars #4453

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 211 additions & 22 deletions lib/teslamate/vehicles/vehicle.ex
Original file line number Diff line number Diff line change
@@ -24,7 +24,12 @@ defmodule TeslaMate.Vehicles.Vehicle do
deps: %{},
task: nil,
import?: false,
stream_pid: nil
stream_pid: nil,
# fake_online_state is introduced because older cars upgraded to MCU2 have a little wakeup every hour to check subsystems.
# They report online but if vehicle_data is requested they wake up completely (cars clicks) and is awake for
# 15 minutes instead of a 2-3 minutes. The only difference (known at this moment) is that stream reports power=nil
# in subsystem online and reports power as a number when it is a real online.
fake_online_state: 0
end

@asleep_interval 30
@@ -333,18 +338,97 @@ defmodule TeslaMate.Vehicles.Vehicle do

# Handle fetch of vehicle/id (non-vehicle_data)
{%Vehicle{}, %Data{}} ->
Logger.warning("Discarded incomplete fetch result", car_id: data.car.id)
{:keep_state, data, [broadcast_fetch(false), schedule_fetch(data)]}
state =
case state do
s when is_tuple(s) -> elem(s, 0)
s when is_atom(s) -> s
end

# We stay in internal offline state, even though fetch result says online (its only non-vehicle_data)
# We connect to stream to check if power is a number and thereby a real online
case {data.car.settings, state, data} do
{%CarSettings{use_streaming_api: true}, state, %Data{stream_pid: nil}}
when state in [:asleep, :offline] ->
Logger.info("Vehicle online, connect stream to check for real online",
car_id: data.car.id
)

{:ok, pid} = connect_stream(data)

{:keep_state, %Data{data | stream_pid: pid, fake_online_state: 1},
[broadcast_fetch(false), schedule_fetch(@asleep_interval, data)]}

{%CarSettings{use_streaming_api: true}, state, %Data{stream_pid: pid}}
when state in [:asleep, :offline] and is_pid(pid) ->
case data do
%Data{fake_online_state: 1} ->
# Under normal circumstances stream always give data within @asleep_interval (30s)
# otherwise detect it here and allow vehicle_data in next fetch
Logger.info("Stream connected, but nothing received, allow real online",
car_id: data.car.id
)

# fetch now and go through regular :start -> :online by setting fake_online_state=3
{:keep_state, %Data{data | fake_online_state: 3},
[broadcast_fetch(false), schedule_fetch(0, data)]}

%Data{fake_online_state: 0} ->
Logger.warning(
"Stream connected, but fake_online_state is 0, shouldnt be possible, allow real online",
car_id: data.car.id
)

{:keep_state, %Data{data | fake_online_state: 3},
[broadcast_fetch(false), schedule_fetch(0, data)]}

%Data{} ->
{:keep_state, data,
[broadcast_fetch(false), schedule_fetch(@asleep_interval, data)]}
end

# Handle startup and vehicle in online
{%CarSettings{use_streaming_api: true}, state, %Data{}}
when state in [:start] ->
Logger.info("Vehicle online at startup, connect stream to check for real online",
car_id: data.car.id
)

data =
with %Data{last_response: nil} <- data do
{last_response, geofence} = restore_last_known_values(vehicle, data)
%Data{data | last_response: last_response, geofence: geofence}
end

{:ok, pid} = connect_stream(data)

{:next_state, {:offline, @asleep_interval},
%Data{data | stream_pid: pid, fake_online_state: 1},
[broadcast_fetch(false), schedule_fetch(@asleep_interval, data)]}

{%CarSettings{use_streaming_api: true}, _state, %Data{}} ->
{:keep_state, data,
[broadcast_fetch(false), schedule_fetch(@asleep_interval, data)]}

{%CarSettings{}, _state, %Data{}} ->
# when not using stream api the fetch is done differently, and
# %Vehicle{state: "online"} will always get vehicle_data which is handled above
Logger.warning("Discarded incomplete fetch result", car_id: data.car.id)
{:keep_state, data, [broadcast_fetch(false), schedule_fetch(data)]}
end
end

{:ok, %Vehicle{state: state} = vehicle} when state in ["offline", "asleep"] ->
# disconnect stream in case we started it to detect real online
# (in that case we won't go through Start / :offline or Start / :asleep)
:ok = disconnect_stream(data)

data =
with %Data{last_response: nil} <- data do
{last_response, geofence} = restore_last_known_values(vehicle, data)
%Data{data | last_response: last_response, geofence: geofence}
end

{:keep_state, data,
{:keep_state, %Data{data | fake_online_state: 0, stream_pid: nil},
[
broadcast_fetch(false),
{:next_event, :internal, {:update, {String.to_existing_atom(state), vehicle}}}
@@ -437,6 +521,72 @@ defmodule TeslaMate.Vehicles.Vehicle do

### Streaming API

#### sleep or offline
# stream is started in def handle_event(:info, {ref, fetch_result}, state, %Data{task: %Task{ref: ref}} = data)

def handle_event(:info, {:stream, %Stream.Data{} = stream_data}, {state, _}, data)
when state in [:asleep, :offline] do
case stream_data do
%Stream.Data{power: nil} ->
Logger.debug(inspect(stream_data), car_id: data.car.id)

# stay on stream, keep asking if online and see if a real one appears
# set to 2 to avoid triggering real online in fetch_result (fallback if stream doesn't work)
case data do
%Data{fake_online_state: 1} ->
Logger.info("Fake online: power is nil", car_id: data.car.id)
{:keep_state, %Data{data | fake_online_state: 2}}

%Data{fake_online_state: 0} ->
Logger.warning(
"Fake online: power is nil, but fake_online_state is 0, shouldnt be possible, allow real online",
car_id: data.car.id
)

{:keep_state, %Data{data | fake_online_state: 2}}

%Data{} ->
:keep_state_and_data
end

%Stream.Data{power: power} when is_number(power) ->
Logger.debug(inspect(stream_data), car_id: data.car.id)

case data do
%Data{fake_online_state: fake_online_state}
when is_number(fake_online_state) and fake_online_state in [1, 2] ->
Logger.info("Real online detected: power is a number", car_id: data.car.id)
# fetch now and go through regular :start -> :online by setting fake_online_state=3
{:keep_state, %Data{data | fake_online_state: 3}, schedule_fetch(0, data)}

%Data{fake_online_state: 0} ->
Logger.warning(
"Real online detected: power is a number, but fake_online_state is 0, shouldnt be possible, allow real online",
car_id: data.car.id
)

{:keep_state, %Data{data | fake_online_state: 3}, schedule_fetch(0, data)}

%Data{} ->
# fake_online_state already set to 3, dont fetch again to avoid 'Fetch already in progress ...'
:keep_state_and_data
end

%Stream.Data{} ->
Logger.debug(inspect(stream_data), car_id: data.car.id)
:keep_state_and_data
end
end

def handle_event(:info, {:stream, :inactive}, {state, _}, data)
when state in [:asleep, :offline] do
Logger.info("Stream :inactive in state #{inspect(state)}, seems to have been a fake online",
car_id: data.car.id
)

:keep_state_and_data
end

#### Online

def handle_event(:info, {:stream, %Stream.Data{} = stream_data}, :online, data) do
@@ -752,7 +902,7 @@ defmodule TeslaMate.Vehicles.Vehicle do
:ok = disconnect_stream(data)

{:next_state, {:asleep, asleep_interval()},
%Data{data | last_state_change: last_state_change, stream_pid: nil},
%Data{data | last_state_change: last_state_change, stream_pid: nil, fake_online_state: 0},
[broadcast_summary(), schedule_fetch(data)]}
end

@@ -765,7 +915,7 @@ defmodule TeslaMate.Vehicles.Vehicle do
:ok = disconnect_stream(data)

{:next_state, {:offline, asleep_interval()},
%Data{data | last_state_change: last_state_change, stream_pid: nil},
%Data{data | last_state_change: last_state_change, stream_pid: nil, fake_online_state: 0},
[broadcast_summary(), schedule_fetch(data)]}
end

@@ -1263,23 +1413,62 @@ defmodule TeslaMate.Vehicles.Vehicle do
end
end

defp fetch(%Data{car: car, deps: deps}, expected_state: expected_state) do
reachable? =
case expected_state do
:online -> true
{:driving, _, _} -> true
{:updating, _} -> true
{:charging, _} -> true
:start -> false
{:offline, _} -> false
{:asleep, _} -> false
{:suspended, _} -> false
end
defp fetch(%Data{car: car, deps: deps} = data, expected_state: expected_state) do
case car.settings do
%CarSettings{use_streaming_api: true} ->
allow_vehicle_data? =
case expected_state do
# will not go to real state :online unless a stream is received
# with power not nil in state :offline/:asleep or if use_streaming api is turned off
:online ->
true

if reachable? do
fetch_with_reachable_assumption(car.eid, deps)
else
fetch_with_unreachable_assumption(car.eid, deps)
{:driving, _, _} ->
true

{:updating, _} ->
true

{:charging, _} ->
true

:start ->
false

{state, _} when state in [:asleep, :offline] ->
case data do
%Data{fake_online_state: 3} -> true
%Data{} -> false
end

{:suspended, _} ->
false
end

if allow_vehicle_data? do
call(deps.api, :get_vehicle_with_state, [car.eid])
else
call(deps.api, :get_vehicle, [car.eid])
end

_ ->
reachable? =
case expected_state do
:online -> true
{:driving, _, _} -> true
{:updating, _} -> true
{:charging, _} -> true
:start -> false
{:offline, _} -> false
{:asleep, _} -> false
{:suspended, _} -> false
end

if reachable? do
fetch_with_reachable_assumption(car.eid, deps)
else
fetch_with_unreachable_assumption(car.eid, deps)
end
end
end


Unchanged files with check annotations Beta

refute_receive _
end
test "does not log updates <= current version", %{test: name} do

Check failure on line 446 in test/teslamate/vehicles/vehicle_test.exs

GitHub Actions / elixir_test / Test

test updates does not log updates <= current version (TeslaMate.Vehicles.VehicleTest)

Check failure on line 446 in test/teslamate/vehicles/vehicle_test.exs

GitHub Actions / elixir_test / Test

test updates does not log updates <= current version (TeslaMate.Vehicles.VehicleTest)
events = [
{:ok, online_event()},
{:ok, online_event(vehicle_state: %{car_version: "2019.40.10.7 ad132c7b057e"})},
colors: [enabled: false]
@tag :capture_log
test "starts a drive", %{test: name} do

Check failure on line 36 in test/teslamate/vehicles/vehicle/streaming_test.exs

GitHub Actions / elixir_test / Test

test driving starts a drive (TeslaMate.Vehicles.Vehicle.StreamingTest)
me = self()
now = DateTime.utc_now()
now_ts = DateTime.to_unix(now, :millisecond)
end
@tag :capture_log
test "discards stale stream data", %{test: name} do

Check failure on line 296 in test/teslamate/vehicles/vehicle/streaming_test.exs

GitHub Actions / elixir_test / Test

test driving discards stale stream data (TeslaMate.Vehicles.Vehicle.StreamingTest)
now = DateTime.utc_now()
events = [
describe "charging" do
@tag :capture_log
test "starts charging", %{test: name} do

Check failure on line 321 in test/teslamate/vehicles/vehicle/streaming_test.exs

GitHub Actions / elixir_test / Test

test charging starts charging (TeslaMate.Vehicles.Vehicle.StreamingTest)
me = self()
now = DateTime.utc_now()
now_ts = DateTime.to_unix(now, :millisecond)
end
@tag :capture_log
test "discards stale stream data when suspended", %{test: name} do

Check failure on line 423 in test/teslamate/vehicles/vehicle/streaming_test.exs

GitHub Actions / elixir_test / Test

test suspended discards stale stream data when suspended (TeslaMate.Vehicles.Vehicle.StreamingTest)
now = DateTime.utc_now()
now_ts = DateTime.to_unix(now, :millisecond)
refute_receive _
end
test "resumes logging when starting to charge", %{test: name} do

Check failure on line 498 in test/teslamate/vehicles/vehicle/streaming_test.exs

GitHub Actions / elixir_test / Test

test resumes logging when starting to charge (TeslaMate.Vehicles.Vehicle.StreamingTest)
me = self()
now = DateTime.utc_now()
now_ts = DateTime.to_unix(now, :millisecond)
end
describe "updating" do
test "disconnects stream", %{test: name} do

Check failure on line 545 in test/teslamate/vehicles/vehicle/streaming_test.exs

GitHub Actions / elixir_test / Test

test updating disconnects stream (TeslaMate.Vehicles.Vehicle.StreamingTest)
now = DateTime.utc_now()
now_ts = DateTime.to_unix(now, :millisecond)
@tag :signed_in
@tag :capture_log
test "shows tag if update is available ", %{conn: conn} do

Check failure on line 376 in test/teslamate_web/live/car_summary_live_test.exs

GitHub Actions / elixir_test / Test

test tags shows tag if update is available (TeslaMateWeb.CarLive.SummaryTest)
events = [
{:ok, online_event()},
{:ok,
alias TeslaMate.Vehicles.Vehicle.Summary
alias TeslaMate.Log.Drive
test "logs a full drive", %{test: name} do

Check failure on line 7 in test/teslamate/vehicles/vehicle/driving_test.exs

GitHub Actions / elixir_test / Test

test logs a full drive (TeslaMate.Vehicles.Vehicle.DrivingTest)

Check failure on line 7 in test/teslamate/vehicles/vehicle/driving_test.exs

GitHub Actions / elixir_test / Test

test logs a full drive (TeslaMate.Vehicles.Vehicle.DrivingTest)
now = DateTime.utc_now()
now_ts = DateTime.to_unix(now, :millisecond)
end
@tag :capture_log
test "handles a connection loss when driving", %{test: name} do

Check failure on line 50 in test/teslamate/vehicles/vehicle/driving_test.exs

GitHub Actions / elixir_test / Test

test handles a connection loss when driving (TeslaMate.Vehicles.Vehicle.DrivingTest)
now = DateTime.utc_now()
now_ts = DateTime.to_unix(now, :millisecond)
refute_received _
end
test "shift state P does not trigger driving state", %{test: name} do

Check failure on line 151 in test/teslamate/vehicles/vehicle/driving_test.exs

GitHub Actions / elixir_test / Test

test shift state P does not trigger driving state (TeslaMate.Vehicles.Vehicle.DrivingTest)
now = DateTime.utc_now()
now_ts = DateTime.to_unix(now, :millisecond)
end
@tag :capture_log
test "interprets a significant offline period while driving with SOC gains as charge session",

Check failure on line 227 in test/teslamate/vehicles/vehicle/driving_test.exs

GitHub Actions / elixir_test / Test

test when offline interprets a significant offline period while driving with SOC gains as charge session (TeslaMate.Vehicles.Vehicle.DrivingTest)
%{test: name} do
now = DateTime.utc_now()
now_ts = DateTime.to_unix(now, :millisecond)
end
@tag :capture_log
test "times out a drive when being offline for to long",

Check failure on line 312 in test/teslamate/vehicles/vehicle/driving_test.exs

GitHub Actions / elixir_test / Test

test when offline times out a drive when being offline for to long (TeslaMate.Vehicles.Vehicle.DrivingTest)
%{test: name} do
now_ts = DateTime.utc_now() |> DateTime.to_unix(:millisecond)
end
@tag :capture_log
test "logs a drive after a significant offline period while driving",

Check failure on line 376 in test/teslamate/vehicles/vehicle/driving_test.exs

GitHub Actions / elixir_test / Test

test when offline logs a drive after a significant offline period while driving (TeslaMate.Vehicles.Vehicle.DrivingTest)
%{test: name} do
now = DateTime.utc_now()
now_ts = DateTime.to_unix(now, :millisecond)
end
@tag :capture_log
test "continues a drive after a short offline period while driving",

Check failure on line 439 in test/teslamate/vehicles/vehicle/driving_test.exs

GitHub Actions / elixir_test / Test

test when offline continues a drive after a short offline period while driving (TeslaMate.Vehicles.Vehicle.DrivingTest)
%{test: name} do
now = DateTime.utc_now()
now_ts = DateTime.to_unix(now, :millisecond)
describe "geofencing" do
alias TeslaMate.Locations.GeoFence
test "changes geofence when enterling or leaving", %{test: name} do

Check failure on line 495 in test/teslamate/vehicles/vehicle/driving_test.exs

GitHub Actions / elixir_test / Test

test geofencing changes geofence when enterling or leaving (TeslaMate.Vehicles.Vehicle.DrivingTest)

Check failure on line 495 in test/teslamate/vehicles/vehicle/driving_test.exs

GitHub Actions / elixir_test / Test

test geofencing changes geofence when enterling or leaving (TeslaMate.Vehicles.Vehicle.DrivingTest)
ts = DateTime.utc_now() |> DateTime.to_unix(:millisecond)
drive_event = fn s, lat, lng ->
refute_receive _
end
test "handles a invalid charge data", %{test: name} do

Check failure on line 159 in test/teslamate/vehicles/vehicle/charging_test.exs

GitHub Actions / elixir_test / Test

test handles a invalid charge data (TeslaMate.Vehicles.Vehicle.ChargingTest)
now_ts = DateTime.utc_now() |> DateTime.to_unix(:millisecond)
events = [