Skip to content

Commit

Permalink
Publish extra active_route fields to mqtt output
Browse files Browse the repository at this point in the history
This refactors the existing code to make it more maintainable.

Nil values are published as "nil" string. This ensures that they will
get published, and ensures that MQTT doesn't drop the retained data.

Fixes #3748
  • Loading branch information
brianmay committed Mar 30, 2024
1 parent cd9c9c8 commit 3e05f68
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 45 deletions.
158 changes: 114 additions & 44 deletions lib/teslamate/mqtt/pubsub/vehicle_subscriber.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,9 @@ defmodule TeslaMate.Mqtt.PubSub.VehicleSubscriber do

alias TeslaMate.Mqtt.Publisher
alias TeslaMate.Vehicles.Vehicle.Summary
alias TeslaMate.Locations.GeoFence
alias TeslaMate.Vehicles

defstruct [:car_id, :last_summary, :deps, :namespace]
defstruct [:car_id, :last_values, :deps, :namespace]
alias __MODULE__, as: State

def child_spec(arg) do
Expand Down Expand Up @@ -38,28 +37,34 @@ defmodule TeslaMate.Mqtt.PubSub.VehicleSubscriber do
{:ok, %State{car_id: car_id, namespace: namespace, deps: deps}}
end

@impl true
def handle_info(summary, %State{last_summary: summary} = state) do
{:noreply, state}
end

@always_published ~w(charge_energy_added charger_actual_current charger_phases
charger_power charger_voltage scheduled_charging_start_time
time_to_full_charge shift_state geofence trim_badging)a

@impl true
def handle_info(%Summary{} = summary, state) do
summary
|> Map.from_struct()
|> Map.drop([:car])
values =
summary
|> Map.from_struct()
|> Map.drop([:car])
|> add_car_latitude_longitude(summary)
|> add_geofence(summary)
|> add_active_route(summary)

publish_values(values, state)
{:noreply, %State{state | last_values: values}}
end

defp publish_values(values, %State{last_values: values}) do
nil
end

defp publish_values(values, state) do
values
|> Stream.reject(&match?({_key, :unknown}, &1))
|> Stream.filter(fn {key, value} ->
(key in @always_published or value != nil) and
(state.last_summary == nil or Map.get(state.last_summary, key) != value)
end)
|> Stream.map(fn
{key = :geofence, %GeoFence{name: name}} -> {key, name}
{key = :geofence, nil} -> {key, Application.get_env(:teslamate, :default_geofence)}
{key, val} -> {key, val}
(state.last_values == nil or Map.get(state.last_values, key) != value)
end)
|> Task.async_stream(&publish(&1, state),
max_concurrency: 10,
Expand All @@ -73,38 +78,103 @@ defmodule TeslaMate.Mqtt.PubSub.VehicleSubscriber do
_ok ->
nil
end)
end

if state.last_summary == nil or
state.last_summary.latitude != summary.latitude or
state.last_summary.longitude != summary.longitude do
lat_lng =
case {summary.latitude, summary.longitude} do
{nil, _} -> nil
{_, nil} -> nil
{%Decimal{} = lat, %Decimal{} = lon} -> {Decimal.to_float(lat), Decimal.to_float(lon)}
{lat, lon} -> {lat, lon}
end

case lat_lng do
nil ->
nil

{lat, lon} ->
location =
%{
latitude: lat,
longitude: lon
}
|> Jason.encode!()

case publish({"location", location}, state) do
:ok -> nil
{:error, reason} -> Logger.warning("Failed to publish location: #{inspect(reason)}")
end
defp add_car_latitude_longitude(map, %Summary{} = summary) do
lat_lng =
case {summary.latitude, summary.longitude} do
{nil, _} -> nil
{_, nil} -> nil
{%Decimal{} = lat, %Decimal{} = lon} -> {Decimal.to_float(lat), Decimal.to_float(lon)}
{lat, lon} -> {lat, lon}
end

case lat_lng do
nil ->
map

{lat, lon} ->
location =
%{
latitude: lat,
longitude: lon
}
|> Jason.encode!()

Map.put(map, :location, location)
end
end

defp add_geofence(map, %Summary{} = summary) do
# This overwrites the existing geofence value in map.
case summary.geofence do
nil ->
Map.put(map, :geofence, Application.get_env(:teslamate, :default_geofence))

geofence ->
Map.put(map, :geofence, geofence.name)
end
end

defp add_active_route(map, %Summary{active_route_destination: nil}) do
# This overwrites the existing values in map.
error =
%{
error: "No active route available"
}
|> Jason.encode!()

Map.merge(
map,
%{
active_route_destination: "nil",
active_route_latitude: "nil",
active_route_longitude: "nil",
active_route_energy_at_arrival: "nil",
active_route_miles_to_arrival: "nil",
active_route_minutes_to_arrival: "nil",
active_route_traffic_minutes_delay: "nil",
active_route_location: error,
active_route: error
}
)
end

{:noreply, %State{state | last_summary: summary}}
defp add_active_route(map, %Summary{} = summary) do
# This overwrites the existing values in map.
location =
%{
latitude: summary.active_route_latitude,
longitude: summary.active_route_longitude,
error: nil
}
|> Jason.encode!()

active_route =
%{
destination: summary.active_route_destination,
latitude: summary.active_route_latitude,
longitude: summary.active_route_longitude,
energy_at_arrival: summary.active_route_energy_at_arrival,
miles_to_arrival: summary.active_route_miles_to_arrival,
minutes_to_arrival: summary.active_route_minutes_to_arrival,
traffic_minutes_delay: summary.active_route_traffic_minutes_delay,
location: location,
error: nil
}
|> Jason.encode!()

Map.merge(map, %{
active_route_destination: summary.active_route_destination,
active_route_latitude: summary.active_route_latitude,
active_route_longitude: summary.active_route_longitude,
active_route_energy_at_arrival: summary.active_route_energy_at_arrival,
active_route_miles_to_arrival: summary.active_route_miles_to_arrival,
active_route_minutes_to_arrival: summary.active_route_minutes_to_arrival,
active_route_traffic_minutes_delay: summary.active_route_traffic_minutes_delay,
active_route_location: location,
active_route: active_route
})
end

defp publish({key, value}, %State{car_id: car_id, namespace: namespace, deps: deps}) do
Expand Down
11 changes: 10 additions & 1 deletion lib/teslamate/vehicles/vehicle/summary.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ defmodule TeslaMate.Vehicles.Vehicle.Summary do
model trim_badging exterior_color wheel_type spoiler_type trunk_open frunk_open elevation power
charge_current_request charge_current_request_max tpms_pressure_fl tpms_pressure_fr tpms_pressure_rl tpms_pressure_rr
tpms_soft_warning_fl tpms_soft_warning_fr tpms_soft_warning_rl tpms_soft_warning_rr climate_keeper_mode
active_route_destination active_route_latitude active_route_longitude
active_route_destination active_route_latitude active_route_longitude active_route_energy_at_arrival
active_route_miles_to_arrival active_route_minutes_to_arrival active_route_traffic_minutes_delay
)a

def into(nil, %{state: :start, healthy?: healthy?, car: car}) do
Expand Down Expand Up @@ -79,6 +80,14 @@ defmodule TeslaMate.Vehicles.Vehicle.Summary do
active_route_destination: get_in_struct(vehicle, [:drive_state, :active_route_destination]),
active_route_latitude: get_in_struct(vehicle, [:drive_state, :active_route_latitude]),
active_route_longitude: get_in_struct(vehicle, [:drive_state, :active_route_longitude]),
active_route_energy_at_arrival:
get_in_struct(vehicle, [:drive_state, :active_route_energy_at_arrival]),
active_route_miles_to_arrival:
get_in_struct(vehicle, [:drive_state, :active_route_miles_to_arrival]),
active_route_minutes_to_arrival:
get_in_struct(vehicle, [:drive_state, :active_route_minutes_to_arrival]),
active_route_traffic_minutes_delay:
get_in_struct(vehicle, [:drive_state, :active_route_traffic_minutes_delay]),
latitude: get_in_struct(vehicle, [:drive_state, :latitude]),
longitude: get_in_struct(vehicle, [:drive_state, :longitude]),
power: get_in_struct(vehicle, [:drive_state, :power]),
Expand Down
72 changes: 72 additions & 0 deletions test/teslamate/mqtt/pubsub/vehicle_subscriber_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,30 @@ defmodule TeslaMate.Mqtt.PubSub.VehicleSubscriberTest do
"longitude" => 41.129182
}

# Published as nil
for key <- [
:active_route_destination,
:active_route_longitude,
:active_route_latitude,
:active_route_energy_at_arrival,
:active_route_miles_to_arrival,
:active_route_minutes_to_arrival,
:active_route_traffic_minutes_delay
] do
topic = "teslamate/cars/0/#{key}"
assert_receive {MqttPublisherMock, {:publish, ^topic, "nil", [retain: true, qos: 1]}}
end

# Published as nil
for key <- [
:active_route_location,
:active_route
] do
topic = "teslamate/cars/0/#{key}"
assert_receive {MqttPublisherMock, {:publish, ^topic, data, [retain: true, qos: 1]}}
assert Jason.decode!(data) == %{"error" => "No active route available"}
end

refute_receive _
end

Expand Down Expand Up @@ -155,6 +179,30 @@ defmodule TeslaMate.Mqtt.PubSub.VehicleSubscriberTest do
assert_receive {MqttPublisherMock,
{:publish, "teslamate/cars/0/trim_badging", "", [retain: true, qos: 1]}}

# Published as nil
for key <- [
:active_route_destination,
:active_route_longitude,
:active_route_latitude,
:active_route_energy_at_arrival,
:active_route_miles_to_arrival,
:active_route_minutes_to_arrival,
:active_route_traffic_minutes_delay
] do
topic = "teslamate/cars/0/#{key}"
assert_receive {MqttPublisherMock, {:publish, ^topic, "nil", [retain: true, qos: 1]}}
end

# Published as nil
for key <- [
:active_route_location,
:active_route
] do
topic = "teslamate/cars/0/#{key}"
assert_receive {MqttPublisherMock, {:publish, ^topic, data, [retain: true, qos: 1]}}
assert Jason.decode!(data) == %{"error" => "No active route available"}
end

refute_receive _
end

Expand Down Expand Up @@ -219,6 +267,30 @@ defmodule TeslaMate.Mqtt.PubSub.VehicleSubscriberTest do
assert_receive {MqttPublisherMock, {:publish, ^topic, "", [retain: true, qos: 1]}}
end

# Published as nil
for key <- [
:active_route_destination,
:active_route_longitude,
:active_route_latitude,
:active_route_energy_at_arrival,
:active_route_miles_to_arrival,
:active_route_minutes_to_arrival,
:active_route_traffic_minutes_delay
] do
topic = "teslamate/account_0/cars/0/#{key}"
assert_receive {MqttPublisherMock, {:publish, ^topic, "nil", [retain: true, qos: 1]}}
end

# Published as nil
for key <- [
:active_route_location,
:active_route
] do
topic = "teslamate/account_0/cars/0/#{key}"
assert_receive {MqttPublisherMock, {:publish, ^topic, data, [retain: true, qos: 1]}}
assert Jason.decode!(data) == %{"error" => "No active route available"}
end

refute_receive _
end
end
24 changes: 24 additions & 0 deletions test/teslamate/vehicles/vehicle_sync_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,30 @@ defmodule TeslaMate.Vehicles.VehicleSyncTest do
"longitude" => 41.128817
}

# Published as nil
for key <- [
:active_route_destination,
:active_route_longitude,
:active_route_latitude,
:active_route_energy_at_arrival,
:active_route_miles_to_arrival,
:active_route_minutes_to_arrival,
:active_route_traffic_minutes_delay
] do
topic = "teslamate/cars/#{car.id}/#{key}"
assert_receive {MqttPublisherMock, {:publish, ^topic, "nil", [retain: true, qos: 1]}}
end

# Published as nil
for key <- [
:active_route_location,
:active_route
] do
topic = "teslamate/cars/#{car.id}/#{key}"
assert_receive {MqttPublisherMock, {:publish, ^topic, data, [retain: true, qos: 1]}}
assert Jason.decode!(data) == %{"error" => "No active route available"}
end

refute_receive _
end
end
Expand Down

0 comments on commit 3e05f68

Please sign in to comment.