Skip to content

Commit

Permalink
feat: handle sleep behavior of MCU2 upgraded cars
Browse files Browse the repository at this point in the history
changes from @micves from #3262
  • Loading branch information
JakobLichterfeld authored and brianmay committed Jan 3, 2025
1 parent 458c594 commit d2999d2
Showing 1 changed file with 249 additions and 27 deletions.
276 changes: 249 additions & 27 deletions lib/teslamate/vehicles/vehicle.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -65,7 +70,6 @@ defmodule TeslaMate.Vehicles.Vehicle do
with str when is_binary(str) <- type do
case String.downcase(str) do
"models" <> _ -> "S"
"models2" <> _ -> "S"
"model3" <> _ -> "3"
"modelx" <> _ -> "X"
"modely" <> _ -> "Y"
Expand All @@ -79,7 +83,6 @@ defmodule TeslaMate.Vehicles.Vehicle do
case {model, trim_badging, type} do
{"S", "100D", "lychee"} -> "LR"
{"S", "P100D", "lychee"} -> "Plaid"
{"S", "100D", "models2"} -> "LR+"
{"3", "P74D", _} -> "LR AWD Performance"
{"3", "74D", _} -> "LR AWD"
{"3", "74", _} -> "LR"
Expand Down Expand Up @@ -333,18 +336,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}}}
Expand Down Expand Up @@ -437,6 +519,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
Expand All @@ -463,8 +611,21 @@ defmodule TeslaMate.Vehicles.Vehicle do
[broadcast_summary(), schedule_fetch(0, data)]}

%Stream.Data{shift_state: nil, power: power} when is_number(power) and power < 0 ->
Logger.info("Charging detected: #{power} kW", car_id: data.car.id)
{:keep_state_and_data, schedule_fetch(0, data)}
vehicle = merge(data.last_response, stream_data, time: true)

# Only detect as charging if we are not doing something else while plugged in.
# In case we are doing both charging and other thing a normal fetch will discover it later
case {vehicle} do
{%Vehicle{climate_state: %Climate{is_preconditioning: true}}} ->
:keep_state_and_data

{%Vehicle{climate_state: %Climate{climate_keeper_mode: "dog"}}} ->
:keep_state_and_data

{%Vehicle{}} ->
Logger.info("Online / Charging detected: #{power} kW", car_id: data.car.id)
{:keep_state_and_data, schedule_fetch(0, data)}
end

%Stream.Data{} ->
Logger.debug(inspect(stream_data), car_id: data.car.id)
Expand Down Expand Up @@ -519,7 +680,19 @@ defmodule TeslaMate.Vehicles.Vehicle do
%Stream.Data{shift_state: s, power: power}
when s in [nil, "P"] and is_number(power) and power < 0 ->
Logger.info("Suspended / Charging detected: #{power} kW", car_id: data.car.id)
{:next_state, prev_state, data, schedule_fetch(0, data)}
{:next_state, prev_state, %Data{data | last_used: DateTime.utc_now()},
schedule_fetch(0, data)}

%Stream.Data{shift_state: s, power: power}
when s in [nil, "P"] and is_number(power) and power > 0 ->
Logger.info("Suspended / Usage detected: #{power} kW", car_id: data.car.id)

# update power to be used in can_fall_asleep / try_to_suspend
vehicle = merge(data.last_response, stream_data, time: true)

{:next_state, prev_state,
%Data{data | last_response: vehicle, last_used: DateTime.utc_now()},
schedule_fetch(0, data)}

%Stream.Data{} ->
Logger.debug(inspect(stream_data), car_id: data.car.id)
Expand Down Expand Up @@ -726,7 +899,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

Expand All @@ -739,7 +912,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

Expand Down Expand Up @@ -1237,23 +1410,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

Expand Down Expand Up @@ -1433,6 +1645,12 @@ defmodule TeslaMate.Vehicles.Vehicle do
{:keep_state, %Data{data | last_used: DateTime.utc_now()},
[broadcast_summary(), schedule_fetch(default_interval() * i, data)]}

{:error, :power_usage} ->
if suspend?, do: Logger.warning("Power usage ...", car_id: car.id)

{:keep_state, %Data{data | last_used: DateTime.utc_now()},
[broadcast_summary(), schedule_fetch(default_interval() * i, data)]}

{:error, :unlocked} ->
if suspend?, do: Logger.warning("Unlocked ...", car_id: car.id)

Expand Down Expand Up @@ -1501,6 +1719,10 @@ defmodule TeslaMate.Vehicles.Vehicle do
%CarSettings{req_not_unlocked: true}} ->
{:error, :unlocked}

{%Vehicle{drive_state: %Drive{power: power}}, _}
when is_number(power) and power > 0 ->
{:error, :power_usage}

{%Vehicle{}, %CarSettings{}} ->
:ok
end
Expand Down

0 comments on commit d2999d2

Please sign in to comment.