Publish extra active_route fields to mqtt output (#3789)

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
This commit is contained in:
Brian May
2024-04-05 17:29:00 +11:00
committed by GitHub
parent dfb3a3cf51
commit c684252940
5 changed files with 268 additions and 114 deletions
+111 -43
View File
@@ -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,33 @@ 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 =
%{}
|> add_simple_values(summary)
|> 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 +77,102 @@ 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
@simple_values ~w(
display_name state since healthy latitude longitude heading battery_level charging_state usable_battery_level
ideal_battery_range_km est_battery_range_km rated_battery_range_km charge_energy_added
speed outside_temp inside_temp is_climate_on is_preconditioning locked sentry_mode
plugged_in scheduled_charging_start_time charge_limit_soc charger_power windows_open doors_open
odometer shift_state charge_port_door_open time_to_full_charge charger_phases
charger_actual_current charger_voltage version update_available update_version is_user_present
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
)a
case lat_lng do
nil ->
nil
defp add_simple_values(map, %Summary{} = summary) do
Map.merge(map, Map.take(summary, @simple_values))
end
{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
end
{:noreply, %State{state | last_summary: summary}}
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
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
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: error
}
)
end
defp add_active_route(map, %Summary{} = summary) do
location =
%{
latitude: summary.active_route_latitude,
longitude: summary.active_route_longitude
}
active_route =
%{
destination: summary.active_route_destination,
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: active_route
})
end
defp publish({key, value}, %State{car_id: car_id, namespace: namespace, deps: deps}) do
+10 -1
View File
@@ -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]),
@@ -103,6 +103,25 @@ defmodule TeslaMate.Mqtt.PubSub.VehicleSubscriberTest do
"longitude" => 41.129182
}
# Published as nil
for key <- [
:active_route_destination,
:active_route_longitude,
:active_route_latitude
] do
topic = "teslamate/cars/0/#{key}"
assert_receive {MqttPublisherMock, {:publish, ^topic, "nil", [retain: true, qos: 1]}}
end
# Published as nil
for key <- [
: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 +174,25 @@ 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
] do
topic = "teslamate/cars/0/#{key}"
assert_receive {MqttPublisherMock, {:publish, ^topic, "nil", [retain: true, qos: 1]}}
end
# Published as nil
for key <- [
: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 +257,25 @@ 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
] 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
] 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
@@ -157,6 +157,25 @@ defmodule TeslaMate.Vehicles.VehicleSyncTest do
"longitude" => 41.128817
}
# Published as nil
for key <- [
:active_route_destination,
:active_route_longitude,
:active_route_latitude
] 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
] 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
+71 -70
View File
@@ -9,76 +9,77 @@ The MQTT function within TeslaMate allows useful values to be published to an MQ
Vehicle data will be published to the following topics:
| Topic | Example | Description |
|--------------------------------------------------------|----------------------|---------------------------------------------------------------------------------------|
| `teslamate/cars/$car_id/display_name` | Blue Thunder | Vehicle Name |
| `teslamate/cars/$car_id/state` | asleep | Status of the vehicle (e.g. `online`, `asleep`, `charging`) |
| `teslamate/cars/$car_id/since` | 2019-02-29T23:00:07Z | Date of the last status change |
| `teslamate/cars/$car_id/healthy` | true | Health status of the logger for that vehicle |
| `teslamate/cars/$car_id/version` | 2019.32.12.2 | Software Version |
| `teslamate/cars/$car_id/update_available` | false | Indicates if a software update is available |
| `teslamate/cars/$car_id/update_version` | 2019.32.12.3 | Software version of the available update |
| | | |
| `teslamate/cars/$car_id/model` | 3 | Either "S", "3", "X" or "Y" |
| `teslamate/cars/$car_id/trim_badging` | P100D | Trim badging |
| `teslamate/cars/$car_id/exterior_color` | DeepBlue | The exterior color |
| `teslamate/cars/$car_id/wheel_type` | Pinwheel18 | The wheel type |
| `teslamate/cars/$car_id/spoiler_type` | None | The spoiler type |
| | | |
| `teslamate/cars/$car_id/geofence` | 🏡 Home | The name of the Geo-fence, if one exists at the current position |
| | | |
| `teslamate/cars/$car_id/latitude` | 35.278131 | Last reported car latitude |
| `teslamate/cars/$car_id/longitude` | 29.744801 | Last reported car longitude |
| `teslamate/cars/$car_id/location` | "latitude": 37.889544, "longitude: 41.128817 | Last reported car location |
| `teslamate/cars/$car_id/shift_state` | D | Current/Last Shift State (D/N/R/P) |
| `teslamate/cars/$car_id/power` | -9 | Current battery power in watts. Positive value on discharge, negative value on charge |
| `teslamate/cars/$car_id/speed` | 12 | Current Speed in km/h |
| `teslamate/cars/$car_id/heading` | 340 | Last reported car direction |
| `teslamate/cars/$car_id/elevation` | 70 | Current elevation above sea level in meters |
| | | |
| `teslamate/cars/$car_id/locked` | true | Indicates if the car is locked |
| `teslamate/cars/$car_id/sentry_mode` | false | Indicates if Sentry Mode is active |
| `teslamate/cars/$car_id/windows_open` | false | Indicates if any of the windows are open |
| `teslamate/cars/$car_id/doors_open` | false | Indicates if any of the doors are open |
| `teslamate/cars/$car_id/trunk_open` | false | Indicates if the trunk is open |
| `teslamate/cars/$car_id/frunk_open` | false | Indicates if the frunk is open |
| `teslamate/cars/$car_id/is_user_present` | false | Indicates if a user is present in the vehicle |
| | | |
| `teslamate/cars/$car_id/is_climate_on` | true | Indicates if the climate control is on |
| `teslamate/cars/$car_id/inside_temp` | 20.8 | Inside Temperature in °C |
| `teslamate/cars/$car_id/outside_temp` | 18.4 | Temperature in °C |
| `teslamate/cars/$car_id/is_preconditioning` | false | Indicates if the vehicle is being preconditioned |
| | | |
| `teslamate/cars/$car_id/odometer` | 1653 | Car odometer in km |
| `teslamate/cars/$car_id/est_battery_range_km` | 372.5 | Estimated Range in km |
| `teslamate/cars/$car_id/rated_battery_range_km` | 401.63 | Rated Range in km |
| `teslamate/cars/$car_id/ideal_battery_range_km` | 335.79 | Ideal Range in km |
| | | |
| `teslamate/cars/$car_id/battery_level` | 88 | Battery Level Percentage |
| `teslamate/cars/$car_id/usable_battery_level` | 85 | Usable battery level percentage |
| `teslamate/cars/$car_id/plugged_in` | true | If car is currently plugged into a charger |
| `teslamate/cars/$car_id/charge_energy_added` | 5.06 | Last added energy in kWh |
| `teslamate/cars/$car_id/charge_limit_soc` | 90 | Charge Limit Configured in Percentage |
| `teslamate/cars/$car_id/charge_port_door_open` | true | Indicates if the charger door is open |
| `teslamate/cars/$car_id/charger_actual_current` | 2.05 | Current amperage supplied by charger |
| `teslamate/cars/$car_id/charger_phases` | 3 | Number of charger power phases (1-3) |
| `teslamate/cars/$car_id/charger_power` | 48.9 | Charger Power |
| `teslamate/cars/$car_id/charger_voltage` | 240 | Charger Voltage |
| `teslamate/cars/$car_id/charge_current_request` | 40 | How many amps the car wants |
| `teslamate/cars/$car_id/charge_current_request_max` | 40 | How many amps the car can have |
| `teslamate/cars/$car_id/scheduled_charging_start_time` | 2019-02-29T23:00:07Z | Start time of the scheduled charge |
| `teslamate/cars/$car_id/time_to_full_charge` | 1.83 | Hours remaining to full charge |
| `teslamate/cars/$car_id/tpms_pressure_fl` | 2.9 | Tire pressure measure in BAR, front left tire |
| `teslamate/cars/$car_id/tpms_pressure_fr` | 2.8 | Tire pressure measure in BAR, front right tire |
| `teslamate/cars/$car_id/tpms_pressure_rl` | 2.9 | Tire pressure measure in BAR, rear left tire |
| `teslamate/cars/$car_id/tpms_pressure_rr` | 2.8 | Tire pressure measure in BAR, rear right tire |
| `teslamate/cars/$car_id/tpms_soft_warning_fl` | true | Indicates if the Tire pressure measure is soft warning, front left tire |
| `teslamate/cars/$car_id/tpms_soft_warning_fr` | false | Indicates if the Tire pressure measure is soft warning, front right tire | |
| `teslamate/cars/$car_id/tpms_soft_warning_rl` | false | Indicates if the Tire pressure measure is soft warning, rear left tire | |
| `teslamate/cars/$car_id/tpms_soft_warning_rr` | false | Indicates if the Tire pressure measure is soft warning, rear right tire |
| `teslamate/cars/$car_id/active_route_destination` | Home | Navigation destination name |
| `teslamate/cars/$car_id/active_route_latitude` | 35.278131 | Navigation destination latitude |
| `teslamate/cars/$car_id/active_route_longitude` | 29.744801 | Navigation destination longitude |
| Topic | Example | Description |
| ------------------------------------------------------ | -------------------------------------------- | ------------------------------------------------------------------------------------- |
| `teslamate/cars/$car_id/display_name` | Blue Thunder | Vehicle Name |
| `teslamate/cars/$car_id/state` | asleep | Status of the vehicle (e.g. `online`, `asleep`, `charging`) |
| `teslamate/cars/$car_id/since` | 2019-02-29T23:00:07Z | Date of the last status change |
| `teslamate/cars/$car_id/healthy` | true | Health status of the logger for that vehicle |
| `teslamate/cars/$car_id/version` | 2019.32.12.2 | Software Version |
| `teslamate/cars/$car_id/update_available` | false | Indicates if a software update is available |
| `teslamate/cars/$car_id/update_version` | 2019.32.12.3 | Software version of the available update |
| | | |
| `teslamate/cars/$car_id/model` | 3 | Either "S", "3", "X" or "Y" |
| `teslamate/cars/$car_id/trim_badging` | P100D | Trim badging |
| `teslamate/cars/$car_id/exterior_color` | DeepBlue | The exterior color |
| `teslamate/cars/$car_id/wheel_type` | Pinwheel18 | The wheel type |
| `teslamate/cars/$car_id/spoiler_type` | None | The spoiler type |
| | | |
| `teslamate/cars/$car_id/geofence` | 🏡 Home | The name of the Geo-fence, if one exists at the current position |
| | | |
| `teslamate/cars/$car_id/latitude` | 35.278131 | DEPRECATED: Last reported car latitude |
| `teslamate/cars/$car_id/longitude` | 29.744801 | DEPRECATED: Last reported car longitude |
| `teslamate/cars/$car_id/location` | "latitude": 37.889544, "longitude: 41.128817 | Last reported car location |
| `teslamate/cars/$car_id/shift_state` | D | Current/Last Shift State (D/N/R/P) |
| `teslamate/cars/$car_id/power` | -9 | Current battery power in watts. Positive value on discharge, negative value on charge |
| `teslamate/cars/$car_id/speed` | 12 | Current Speed in km/h |
| `teslamate/cars/$car_id/heading` | 340 | Last reported car direction |
| `teslamate/cars/$car_id/elevation` | 70 | Current elevation above sea level in meters |
| | | |
| `teslamate/cars/$car_id/locked` | true | Indicates if the car is locked |
| `teslamate/cars/$car_id/sentry_mode` | false | Indicates if Sentry Mode is active |
| `teslamate/cars/$car_id/windows_open` | false | Indicates if any of the windows are open |
| `teslamate/cars/$car_id/doors_open` | false | Indicates if any of the doors are open |
| `teslamate/cars/$car_id/trunk_open` | false | Indicates if the trunk is open |
| `teslamate/cars/$car_id/frunk_open` | false | Indicates if the frunk is open |
| `teslamate/cars/$car_id/is_user_present` | false | Indicates if a user is present in the vehicle |
| | | |
| `teslamate/cars/$car_id/is_climate_on` | true | Indicates if the climate control is on |
| `teslamate/cars/$car_id/inside_temp` | 20.8 | Inside Temperature in °C |
| `teslamate/cars/$car_id/outside_temp` | 18.4 | Temperature in °C |
| `teslamate/cars/$car_id/is_preconditioning` | false | Indicates if the vehicle is being preconditioned |
| | | |
| `teslamate/cars/$car_id/odometer` | 1653 | Car odometer in km |
| `teslamate/cars/$car_id/est_battery_range_km` | 372.5 | Estimated Range in km |
| `teslamate/cars/$car_id/rated_battery_range_km` | 401.63 | Rated Range in km |
| `teslamate/cars/$car_id/ideal_battery_range_km` | 335.79 | Ideal Range in km |
| | | |
| `teslamate/cars/$car_id/battery_level` | 88 | Battery Level Percentage |
| `teslamate/cars/$car_id/usable_battery_level` | 85 | Usable battery level percentage |
| `teslamate/cars/$car_id/plugged_in` | true | If car is currently plugged into a charger |
| `teslamate/cars/$car_id/charge_energy_added` | 5.06 | Last added energy in kWh |
| `teslamate/cars/$car_id/charge_limit_soc` | 90 | Charge Limit Configured in Percentage |
| `teslamate/cars/$car_id/charge_port_door_open` | true | Indicates if the charger door is open |
| `teslamate/cars/$car_id/charger_actual_current` | 2.05 | Current amperage supplied by charger |
| `teslamate/cars/$car_id/charger_phases` | 3 | Number of charger power phases (1-3) |
| `teslamate/cars/$car_id/charger_power` | 48.9 | Charger Power |
| `teslamate/cars/$car_id/charger_voltage` | 240 | Charger Voltage |
| `teslamate/cars/$car_id/charge_current_request` | 40 | How many amps the car wants |
| `teslamate/cars/$car_id/charge_current_request_max` | 40 | How many amps the car can have |
| `teslamate/cars/$car_id/scheduled_charging_start_time` | 2019-02-29T23:00:07Z | Start time of the scheduled charge |
| `teslamate/cars/$car_id/time_to_full_charge` | 1.83 | Hours remaining to full charge |
| `teslamate/cars/$car_id/tpms_pressure_fl` | 2.9 | Tire pressure measure in BAR, front left tire |
| `teslamate/cars/$car_id/tpms_pressure_fr` | 2.8 | Tire pressure measure in BAR, front right tire |
| `teslamate/cars/$car_id/tpms_pressure_rl` | 2.9 | Tire pressure measure in BAR, rear left tire |
| `teslamate/cars/$car_id/tpms_pressure_rr` | 2.8 | Tire pressure measure in BAR, rear right tire |
| `teslamate/cars/$car_id/tpms_soft_warning_fl` | true | Indicates if the Tire pressure measure is soft warning, front left tire |
| `teslamate/cars/$car_id/tpms_soft_warning_fr` | false | Indicates if the Tire pressure measure is soft warning, front right tire |
| `teslamate/cars/$car_id/tpms_soft_warning_rl` | false | Indicates if the Tire pressure measure is soft warning, rear left tire |
| `teslamate/cars/$car_id/tpms_soft_warning_rr` | false | Indicates if the Tire pressure measure is soft warning, rear right tire |
| `teslamate/cars/$car_id/active_route_destination` | Home | DEPRECATED: Navigation destination name (or "nil") |
| `teslamate/cars/$car_id/active_route_latitude` | 35.278131 | DEPRECATED: Navigation destination latitude (or "nil") |
| `teslamate/cars/$car_id/active_route_longitude` | 29.744801 | DEPRECATED: Navigation destination longitude (or "nil") |
| `teslamate/cars/$car_id/active_route` | | Navigation details (json blob) |
:::note
`$car_id` usually starts at 1