Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0b84024
feat(mqtt): add opt-in Home Assistant MQTT discovery
brianmay Jul 23, 2026
c903d37
docs: fix malformed environment variables table
brianmay Aug 11, 2026
e8fe146
docs: reword MQTT_HOME_ASSISTANT_DISCOVERY_URL wording
brianmay Aug 11, 2026
6ef232c
feat(mqtt): validate MQTT_HOME_ASSISTANT_DISCOVERY_PREFIX
brianmay Aug 11, 2026
5fb2a45
feat(mqtt): clear Home Assistant discovery configs on disable
brianmay Aug 11, 2026
a7b170b
feat(mqtt): clear Home Assistant discovery configs for removed vehicl…
brianmay Aug 12, 2026
ec4718a
feat(mqtt): make discovery entity IDs match the manual mqtt_sensors.yaml
brianmay Aug 12, 2026
4f3df33
fix(mqtt): use state_class total_increasing for charge_energy_added d…
brianmay Aug 12, 2026
a937565
feat(mqtt): add charging_state discovery entity
brianmay Aug 12, 2026
16e53cf
test(mqtt): use plain ExUnit.Case in home_assistant_test
brianmay Aug 12, 2026
2c1ef9a
feat(mqtt): scope discovery topics and unique_ids by MQTT namespace
brianmay Aug 12, 2026
f409fbe
feat(vehicles): add Summary type definition
brianmay Aug 12, 2026
3a8d45c
docs(mqtt): simplify discovered entities note
brianmay Aug 12, 2026
a3cc5a7
feat(nix): add Home Assistant MQTT discovery options
brianmay Aug 12, 2026
7292328
fix(mqtt): scope startup discovery cleanup by MQTT namespace
JakobLichterfeld Aug 14, 2026
b085f9b
refactor(test): extract shared drain_discovery_configs helper
JakobLichterfeld Aug 14, 2026
2db6d99
docs(mqtt): explain startup discovery cleanup timing
JakobLichterfeld Aug 14, 2026
b67f32b
feat(mqtt): reject MQTT wildcards in MQTT_NAMESPACE
JakobLichterfeld Aug 14, 2026
75da857
docs: update changelog
JakobLichterfeld Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

Existing charging processes are recomputed once during the upgrade migration: previously empty or zero `charge_energy_used` values (short or mixed AC sessions) gain values, the first start after the upgrade can take a few minutes longer on databases with years of history and slow HW, and charge costs are deliberately not changed retroactively.

### Note for manual Home Assistant configurations

The documented manual [mqtt_sensors.yaml](https://docs.teslamate.org/docs/integrations/home_assistant#mqtt_sensorsyaml-mqtt-section-of-configurationyaml) now uses `state_class: total_increasing` for the `charge_energy_added` sensor (#5543). If you re-sync your manual YAML, Home Assistant will treat the per-charge resets as meter cycles, which changes the long-term statistics behavior (e.g. in the Energy dashboard).

### New features

- feat: add service mode to webview and reduce log when car is Unlocked at service mode (#5289 - @NirKli)
Expand All @@ -14,6 +18,7 @@ Existing charging processes are recomputed once during the upgrade migration: pr
- feat: link the software update icon to the notateslaapp release notes (#5490 - @NirKli)
- feat: add fullscreen mode to vehicle summary map (#5495 - @hakong)
- feat(web): expose VIN in car summary ( #5556 - @Helvio88, @magrathean-uk)
- feat(mqtt): add opt-in Home Assistant MQTT discovery (#5543 - @brianmay, @JakobLichterfeld)

### Improvements and bug fixes

Expand Down
23 changes: 19 additions & 4 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,20 @@ defmodule Util do
def validate_namespace!(""), do: nil

def validate_namespace!(ns) when is_binary(ns) do
case String.contains?(ns, "/") do
true -> raise "MQTT_NAMESPACE must not contain '/'"
false -> ns
cond do
String.contains?(ns, "/") -> raise "MQTT_NAMESPACE must not contain '/'"
String.contains?(ns, ["+", "#"]) -> raise "MQTT_NAMESPACE must not contain MQTT wildcards"
true -> ns
end
end

def validate_discovery_prefix!(nil), do: nil
def validate_discovery_prefix!(""), do: nil

def validate_discovery_prefix!(prefix) when is_binary(prefix) do
case String.contains?(prefix, ["+", "#"]) do
true -> raise "MQTT_HOME_ASSISTANT_DISCOVERY_PREFIX must not contain MQTT wildcards"
false -> prefix
end
end

Expand Down Expand Up @@ -181,7 +192,11 @@ if System.get_env("DISABLE_MQTT") != "true" or config_env() == :test do
tls: System.get_env("MQTT_TLS") == "true",
accept_invalid_certs: System.get_env("MQTT_TLS_ACCEPT_INVALID_CERTS") == "true",
namespace: System.get_env("MQTT_NAMESPACE") |> Util.validate_namespace!(),
ipv6: System.get_env("MQTT_IPV6") == "true"
ipv6: System.get_env("MQTT_IPV6") == "true",
discovery: System.get_env("MQTT_HOME_ASSISTANT_DISCOVERY") == "true",
discovery_base_url: System.get_env("MQTT_HOME_ASSISTANT_DISCOVERY_URL"),
discovery_prefix:
System.get_env("MQTT_HOME_ASSISTANT_DISCOVERY_PREFIX") |> Util.validate_discovery_prefix!()
end

if config_env() != :test do
Expand Down
9 changes: 8 additions & 1 deletion lib/teslamate/mqtt.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ defmodule TeslaMate.Mqtt do
children = [
{Tortoise311.Connection, connection_config(opts) ++ [client_id: client_id]},
{Publisher, client_id: client_id},
{PubSub, namespace: opts[:namespace]}
{PubSub,
[
namespace: opts[:namespace],
discovery: opts[:discovery],
discovery_base_url: opts[:discovery_base_url],
discovery_prefix: opts[:discovery_prefix]
]
|> Enum.reject(fn {_key, value} -> is_nil(value) end)}
]

Supervisor.init(children, strategy: :one_for_one)
Expand Down
48 changes: 46 additions & 2 deletions lib/teslamate/mqtt/pubsub.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
defmodule TeslaMate.Mqtt.PubSub do
use Supervisor

require Logger

alias __MODULE__.VehicleSubscriber
alias __MODULE__.HomeAssistant
alias TeslaMate.Log
alias TeslaMate.Mqtt.Publisher
alias TeslaMate.Vehicles
alias TeslaMate.Vehicles.Vehicle.Summary

# API

Expand All @@ -12,10 +18,48 @@ defmodule TeslaMate.Mqtt.PubSub do

@impl true
def init(opts) do
subscriber_opts =
opts
|> Keyword.take([:namespace, :discovery, :discovery_base_url, :discovery_prefix])

vehicles = Vehicles.list()

if Keyword.get(opts, :discovery, false) do
# Runs concurrently with the supervised children starting up, so it may
# fire before the MQTT connection is established. Failures are only
# logged; since the cleanup is idempotent and repeated on every start,
# a missed run is corrected on the next restart.
Task.start(fn -> clear_removed_vehicles(vehicles, opts) end)
end

children =
Vehicles.list()
|> Enum.map(&{VehicleSubscriber, Keyword.merge(opts, car_id: &1.car.id)})
vehicles
|> Enum.map(&{VehicleSubscriber, Keyword.merge(subscriber_opts, car_id: &1.car.id)})

Supervisor.init(children, strategy: :one_for_one)
end

@doc """
Clears Home Assistant discovery configs for cars that are no longer
tracked by a vehicle process, e.g. because they were removed from the
Tesla account or because logging was disabled, so their entities are
removed from Home Assistant.
"""
@spec clear_removed_vehicles([Summary.t()], keyword()) :: :ok
def clear_removed_vehicles(vehicles, opts) do
publisher = Keyword.get(opts, :deps_publisher, Publisher)
active_ids = Enum.map(vehicles, & &1.car.id)
clear_opts = Keyword.take(opts, [:namespace, :discovery_prefix])

Log.list_cars()
|> Enum.reject(&(&1.id in active_ids))
|> Enum.each(fn car ->
case HomeAssistant.clear(car.id, clear_opts, publisher) do
:ok -> :ok
{:error, reason} -> Logger.warning("MQTT HA discovery cleanup failed: #{inspect(reason)}")
end
end)

:ok
end
end
Loading
Loading