From 3e05f680c0ec2c1b0b5759b196596f685a30197f Mon Sep 17 00:00:00 2001 From: Brian May Date: Fri, 29 Mar 2024 10:27:42 +1100 Subject: [PATCH] Publish extra active_route fields to mqtt output 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 --- .../mqtt/pubsub/vehicle_subscriber.ex | 158 +++++++++++++----- lib/teslamate/vehicles/vehicle/summary.ex | 11 +- .../mqtt/pubsub/vehicle_subscriber_test.exs | 72 ++++++++ test/teslamate/vehicles/vehicle_sync_test.exs | 24 +++ 4 files changed, 220 insertions(+), 45 deletions(-) diff --git a/lib/teslamate/mqtt/pubsub/vehicle_subscriber.ex b/lib/teslamate/mqtt/pubsub/vehicle_subscriber.ex index c770e714c93..e1447158b16 100644 --- a/lib/teslamate/mqtt/pubsub/vehicle_subscriber.ex +++ b/lib/teslamate/mqtt/pubsub/vehicle_subscriber.ex @@ -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 @@ -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, @@ -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 diff --git a/lib/teslamate/vehicles/vehicle/summary.ex b/lib/teslamate/vehicles/vehicle/summary.ex index 1ead0a5becb..1d372451b8e 100644 --- a/lib/teslamate/vehicles/vehicle/summary.ex +++ b/lib/teslamate/vehicles/vehicle/summary.ex @@ -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 @@ -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]), diff --git a/test/teslamate/mqtt/pubsub/vehicle_subscriber_test.exs b/test/teslamate/mqtt/pubsub/vehicle_subscriber_test.exs index 93199a7a019..1f65628b52f 100644 --- a/test/teslamate/mqtt/pubsub/vehicle_subscriber_test.exs +++ b/test/teslamate/mqtt/pubsub/vehicle_subscriber_test.exs @@ -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 @@ -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 @@ -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 diff --git a/test/teslamate/vehicles/vehicle_sync_test.exs b/test/teslamate/vehicles/vehicle_sync_test.exs index 4550f6656e6..94ddc093994 100644 --- a/test/teslamate/vehicles/vehicle_sync_test.exs +++ b/test/teslamate/vehicles/vehicle_sync_test.exs @@ -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