From f9109702073f82c0a2514312a46b58bcc763d435 Mon Sep 17 00:00:00 2001 From: Jakob Lichterfeld Date: Mon, 23 Dec 2024 13:24:14 +0100 Subject: [PATCH] feat: handle sleep behavior of MCU2 upgraded cars changes from @micves from #3262 --- lib/teslamate/vehicles/vehicle.ex | 276 +++++++++++++++++++++++++++--- 1 file changed, 250 insertions(+), 26 deletions(-) diff --git a/lib/teslamate/vehicles/vehicle.ex b/lib/teslamate/vehicles/vehicle.ex index d32098b9a8..bf837527ba 100644 --- a/lib/teslamate/vehicles/vehicle.ex +++ b/lib/teslamate/vehicles/vehicle.ex @@ -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 @@ -79,7 +84,7 @@ defmodule TeslaMate.Vehicles.Vehicle do case {model, trim_badging, type} do {"S", "100D", "lychee"} -> "LR" {"S", "P100D", "lychee"} -> "Plaid" - {"S", "100D", "models2"} -> "LR+" + {"S", "100D", "models2"} -> "LR" {"3", "P74D", _} -> "LR AWD Performance" {"3", "74D", _} -> "LR AWD" {"3", "74", _} -> "LR" @@ -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 @@ -463,8 +613,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) @@ -519,7 +682,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) @@ -726,7 +901,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 @@ -739,7 +914,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 @@ -1237,23 +1412,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 @@ -1433,6 +1647,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) @@ -1501,6 +1721,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