Skip to content

Commit 241d801

Browse files
committed
feat(mqtt): scope discovery topics and unique_ids by MQTT namespace
1 parent d05631d commit 241d801

3 files changed

Lines changed: 54 additions & 12 deletions

File tree

lib/teslamate/mqtt/pubsub/home_assistant.ex

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,27 @@ defmodule TeslaMate.Mqtt.PubSub.HomeAssistant do
2727
the given vehicle summary.
2828
2929
Each payload is published retained (QoS 1) to
30-
`<discovery_prefix>/<component>/<node>_<car_id>/<object_id>/config` where
31-
`node` is `#{@node}`. Returns `:ok` on success.
30+
`<discovery_prefix>/<component>/<node>/<object_id>/config` where `node` is
31+
`#{@node}_<car_id>` (with the `MQTT_NAMESPACE` inserted after `#{@node}`
32+
when set). Returns `:ok` on success.
3233
"""
3334
@spec publish(term(), publish_opts(), term()) :: :ok | {:error, term()}
3435
def publish(%Summary{} = summary, opts, publisher) do
3536
car_id = Keyword.fetch!(opts, :car_id)
3637
namespace = Keyword.get(opts, :namespace)
3738
base_url = Keyword.get(opts, :base_url)
3839
prefix = Keyword.get(opts, :discovery_prefix, @discovery_prefix)
39-
node = "#{@node}_#{car_id}"
40+
node = node(car_id, namespace)
4041

41-
device = device(summary, car_id, base_url)
42+
device = device(summary, car_id, base_url, namespace)
4243

4344
Enum.reduce_while(entities(), :ok, fn {component, object_id, config}, _acc ->
4445
topic = discovery_topic(prefix, component, node, object_id)
4546

4647
payload =
4748
config
4849
|> resolve_topics(car_id, namespace)
49-
|> Map.put(:unique_id, "teslamate_#{car_id}_#{object_id}")
50+
|> Map.put(:unique_id, "#{node}_#{object_id}")
5051
|> Map.put(:object_id, "tesla_#{object_id}")
5152
|> Map.put(:device, device)
5253
|> Jason.encode!()
@@ -65,8 +66,9 @@ defmodule TeslaMate.Mqtt.PubSub.HomeAssistant do
6566
"""
6667
@spec clear(pos_integer(), publish_opts(), term()) :: :ok | {:error, term()}
6768
def clear(car_id, opts, publisher) do
69+
namespace = Keyword.get(opts, :namespace)
6870
prefix = Keyword.get(opts, :discovery_prefix, @discovery_prefix)
69-
node = "#{@node}_#{car_id}"
71+
node = node(car_id, namespace)
7072

7173
Enum.reduce_while(entities(), :ok, fn {component, object_id, _config}, _acc ->
7274
topic = discovery_topic(prefix, component, node, object_id)
@@ -109,14 +111,29 @@ defmodule TeslaMate.Mqtt.PubSub.HomeAssistant do
109111
|> Enum.join("/")
110112
end
111113

112-
defp device(%Summary{} = summary, car_id, base_url) do
114+
defp node(car_id, namespace) do
115+
[@node, namespace, car_id]
116+
|> Enum.reject(&is_nil/1)
117+
|> Enum.join("_")
118+
end
119+
120+
defp device(%Summary{} = summary, car_id, base_url, namespace) do
113121
name = summary.display_name || car_name(summary) || "Tesla ##{car_id}"
114122
model = summary.model || "Tesla"
115123

116-
%{identifiers: ["teslamate_car_#{car_id}"], manufacturer: "Tesla", name: name, model: model}
124+
%{
125+
identifiers: [device_identifier(car_id, namespace)],
126+
manufacturer: "Tesla",
127+
name: name,
128+
model: model
129+
}
117130
|> maybe_put(:configuration_url, base_url)
118131
end
119132

133+
defp device_identifier(car_id, namespace) do
134+
[@node, namespace, "car", car_id] |> Enum.reject(&is_nil/1) |> Enum.join("_")
135+
end
136+
120137
defp car_name(%Summary{car: %Car{name: name}}) when is_binary(name) and name != "", do: name
121138
defp car_name(_), do: nil
122139

test/teslamate/mqtt/pubsub/home_assistant_test.exs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,28 @@ defmodule TeslaMate.Mqtt.PubSub.HomeAssistantTest do
9797
assert String.starts_with?(topic, "custom_prefix/")
9898
end
9999

100+
test "scopes node, unique_id and device by namespace", %{test: name} do
101+
publisher_name = start_publisher(name)
102+
103+
:ok =
104+
HomeAssistant.publish(
105+
@summary,
106+
[car_id: 0, namespace: "ns1"],
107+
{MqttPublisherMock, publisher_name}
108+
)
109+
110+
find_config("homeassistant/sensor/teslamate_ns1_0/speed/config", fn decoded ->
111+
assert decoded["object_id"] == "tesla_speed"
112+
assert decoded["unique_id"] == "teslamate_ns1_0_speed"
113+
assert decoded["state_topic"] == "teslamate/ns1/cars/0/speed"
114+
assert decoded["device"]["identifiers"] == ["teslamate_ns1_car_0"]
115+
end)
116+
117+
find_config("homeassistant/sensor/teslamate_ns1_0/display_name/config", fn decoded ->
118+
assert decoded["unique_id"] == "teslamate_ns1_0_display_name"
119+
end)
120+
end
121+
100122
test "locked binary sensor is inverted", %{test: name} do
101123
publisher_name = start_publisher(name)
102124

website/docs/integrations/home_assistant.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ Enable it with the following environment variables (see [Environment variables](
3636
| `MQTT_HOME_ASSISTANT_DISCOVERY_PREFIX` | Discovery topic prefix. Must match Home Assistant's `discovery_prefix` setting. | `homeassistant` |
3737

3838
When enabled, TeslaMate publishes a retained `config` payload per entity to
39-
`<discovery_prefix>/<component>/<node>_<car_id>/<object_id>/config` on the first
40-
vehicle summary, where `<node>` is `teslamate`. The device is grouped under the
41-
`teslamate_car_<car_id>` identifier, and the entity IDs match those produced by
42-
the manual `mqtt_sensors.yaml` below.
39+
`<discovery_prefix>/<component>/<node>/<object_id>/config` on the first
40+
vehicle summary, where `<node>` is `teslamate_<car_id>`. If
41+
`MQTT_NAMESPACE` is set, it is inserted after `teslamate_` (e.g.
42+
`teslamate_<namespace>_<car_id>`), so multiple TeslaMate instances sharing a
43+
broker do not collide on the same discovery topics. The device is grouped
44+
under the `teslamate_car_<car_id>` identifier (likewise namespace-scoped), and
45+
the entity IDs match those produced by the manual `mqtt_sensors.yaml` below.
4346

4447
:::note
4548

0 commit comments

Comments
 (0)