Skip to content

Commit e166ca0

Browse files
author
J.A.R.V.I.S.
committed
fix: MqttSetupHelper - use configured HA URL for broker discovery
- Use the HA URL the user configured (IP or domain like home.kirchweger.de) - Check /api/config -> components for MQTT integration existence - Try /api/config/config_entries/entry for broker details (credentials protected by HA) - Fallback: use HA host as MQTT broker (Mosquitto runs on same host) - Try anonymous, then HA token auth, then localhost - Fix: never hardcode IP addresses, always derive from user's HA URL
1 parent 933e932 commit e166ca0

1 file changed

Lines changed: 106 additions & 31 deletions

File tree

src/HaDeskLink/MqttSetupHelper.cs

Lines changed: 106 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -39,29 +39,37 @@ public class MqttSetupResult
3939
/// <summary>
4040
/// Auto-configures MQTT by querying the Home Assistant REST API for
4141
/// the MQTT broker details, then testing the connection.
42+
///
43+
/// Strategy:
44+
/// 1. Check if MQTT integration exists in HA (via /api/config -> components)
45+
/// 2. Try to extract broker details from config entries API (credentials may be hidden)
46+
/// 3. Use the HA host as broker host (Mosquitto add-on runs on same host)
47+
/// 4. Try multiple connection strategies: discovered creds, anonymous, HA token
4248
/// </summary>
4349
public static class MqttSetupHelper
4450
{
4551
/// <summary>
4652
/// Query the HA REST API for MQTT integration details and try to
47-
/// establish a working MQTT connection. Returns a result that can
48-
/// be used directly for persistent MQTT client setup.
53+
/// establish a working MQTT connection. Uses the configured HA URL
54+
/// (works with IP addresses AND domain names like home.kirchweger.de).
4955
/// </summary>
5056
public static async Task<MqttSetupResult> AutoConfigureAsync(string haUrl, string haToken)
5157
{
5258
var result = new MqttSetupResult();
5359

5460
try
5561
{
56-
// ── Step 1: extract the HA host from the URL ──
62+
// ── Step 1: extract the host from the configured HA URL ──
63+
// This works with IPs (192.168.178.33) AND domains (home.kirchweger.de)
5764
var haUri = new Uri(haUrl.TrimEnd('/'));
5865
var haHost = haUri.Host;
66+
var haPort = haUri.Port; // -1 if not specified, else the actual port
5967

60-
// Build an HttpClient with the Bearer token pre-set.
6168
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
6269
http.DefaultRequestHeaders.Add("Authorization", $"Bearer {haToken}");
6370

64-
// ── Step 2: fetch config entries, look for the MQTT integration ──
71+
// ── Step 2: check if MQTT integration exists in HA ──
72+
bool mqttInstalled = false;
6573
string? brokerHost = null;
6674
int brokerPort = 0;
6775
string? brokerUser = null;
@@ -70,20 +78,54 @@ public static async Task<MqttSetupResult> AutoConfigureAsync(string haUrl, strin
7078

7179
try
7280
{
73-
var configUrl = $"{haUrl}/api/config/config_entries/entry";
74-
var resp = await http.GetStringAsync(configUrl);
75-
using var doc = JsonDocument.Parse(resp);
81+
// Check /api/config -> components list for "mqtt"
82+
var configResp = await http.GetStringAsync($"{haUrl.TrimEnd('/')}/api/config");
83+
using var configDoc = JsonDocument.Parse(configResp);
7684

77-
if (doc.RootElement.ValueKind == JsonValueKind.Array)
85+
if (configDoc.RootElement.TryGetProperty("components", out var components)
86+
&& components.ValueKind == JsonValueKind.Array)
7887
{
79-
foreach (var entry in doc.RootElement.EnumerateArray())
88+
foreach (var comp in components.EnumerateArray())
89+
{
90+
if (comp.GetString() == "mqtt")
91+
{
92+
mqttInstalled = true;
93+
break;
94+
}
95+
}
96+
}
97+
}
98+
catch
99+
{
100+
// If we can't reach the API, assume MQTT might be there and try anyway
101+
mqttInstalled = true; // give it a shot
102+
}
103+
104+
// ── Step 3: try to extract broker details from config entries ──
105+
// HA protects the actual credentials, so we may get empty data here.
106+
// But sometimes the broker host is visible.
107+
try
108+
{
109+
var entriesResp = await http.GetStringAsync($"{haUrl.TrimEnd('/')}/api/config/config_entries/entry");
110+
using var entriesDoc = JsonDocument.Parse(entriesResp);
111+
112+
if (entriesDoc.RootElement.ValueKind == JsonValueKind.Array)
113+
{
114+
foreach (var entry in entriesDoc.RootElement.EnumerateArray())
80115
{
81116
var domain = entry.TryGetProperty("domain", out var d) ? d.GetString() : null;
82117
if (domain != "mqtt") continue;
83118

84-
// We found the MQTT integration – extract the connection details.
119+
// Found the MQTT config entry
120+
if (entry.TryGetProperty("title", out var title))
121+
{
122+
// "core-mosquitto" title means Mosquitto add-on
123+
// This confirms the broker is on the same host
124+
}
125+
85126
if (entry.TryGetProperty("data", out var data))
86127
{
128+
// Broker host might be available
87129
if (data.TryGetProperty("broker", out var b))
88130
brokerHost = b.GetString();
89131
if (data.TryGetProperty("port", out var p) && p.TryGetInt32(out var portVal))
@@ -101,31 +143,40 @@ public static async Task<MqttSetupResult> AutoConfigureAsync(string haUrl, strin
101143
}
102144
catch
103145
{
104-
// Fallback: the /api/config/config_entries/entry endpoint might not
105-
// exist on older HA versions — carry on with the heuristic below.
146+
// Config entries API may not return credentials, that's fine
106147
}
107148

108-
// ── Step 3: heuristic fallback ──
109-
// The MQTT broker (Mosquitto add-on) nearly always runs on the
110-
// same host as Home Assistant itself.
149+
// ── Step 4: determine broker host ──
150+
// If the API didn't reveal the broker host, use the HA host.
151+
// For domain names (home.kirchweger.de), this means the MQTT broker
152+
// is accessible at the same domain. For IPs, same thing.
111153
if (string.IsNullOrEmpty(brokerHost))
112154
{
113155
brokerHost = haHost;
114156
}
115157

116-
// Default port unless we found one from the API.
158+
// Default port unless we found one from the API
117159
if (brokerPort <= 0)
118160
{
161+
// Check if HA is running on a custom port — Mosquitto is
162+
// always on 1883 (or 8883 for SSL) regardless of HA's port
119163
brokerPort = brokerSsl ? 8883 : 1883;
120164
}
121165

122166
result.BrokerHost = brokerHost;
123167
result.BrokerPort = brokerPort;
124168
result.UseSsl = brokerSsl;
125169

126-
// ── Step 4: attempt to connect ──
170+
// ── Step 5: check if MQTT is installed at all ──
171+
if (!mqttInstalled)
172+
{
173+
// Double-check by trying to connect anyway — the components
174+
// list might not include all loaded integrations
175+
}
176+
177+
// ── Step 6: attempt to connect with multiple strategies ──
127178

128-
// Strategy A: with discovered credentials (or anonymous if none)
179+
// Strategy A: with discovered credentials (or anonymous if none found)
129180
if (await TestConnectionAsync(brokerHost, brokerPort, brokerUser, brokerPass, brokerSsl))
130181
{
131182
result.Success = true;
@@ -134,36 +185,60 @@ public static async Task<MqttSetupResult> AutoConfigureAsync(string haUrl, strin
134185
return result;
135186
}
136187

137-
// Strategy B: try the HA access token as the MQTT password.
138-
// Some HA setups (especially supervised/core installs) accept this.
139-
if (!string.IsNullOrEmpty(haToken) && brokerUser != haToken)
188+
// Strategy B: try anonymous access (Mosquitto add-on default allows
189+
// anonymous from local network)
190+
if (brokerUser != null || brokerPass != null)
140191
{
141-
if (await TestConnectionAsync(brokerHost, brokerPort, brokerUser, haToken, brokerSsl))
192+
if (await TestConnectionAsync(brokerHost, brokerPort, null, null, brokerSsl))
193+
{
194+
result.Success = true;
195+
result.Username = null;
196+
result.Password = null;
197+
return result;
198+
}
199+
}
200+
201+
// Strategy C: try with the HA access token as MQTT password
202+
// Some HA setups accept this
203+
if (!string.IsNullOrEmpty(haToken))
204+
{
205+
if (await TestConnectionAsync(brokerHost, brokerPort, "homeassistant", haToken, brokerSsl))
142206
{
143207
result.Success = true;
144-
result.Username = brokerUser;
208+
result.Username = "homeassistant";
145209
result.Password = haToken;
146210
return result;
147211
}
148212
}
149213

150-
// Strategy C: anonymous fallback (in case credentials are wrong, but
151-
// the Mosquitto add-on allows anonymous connections from the local LAN).
152-
if (!string.IsNullOrEmpty(brokerUser) || !string.IsNullOrEmpty(brokerPass))
214+
// Strategy D: if HA runs on a non-standard port, try localhost
215+
// (Mosquitto might only listen on localhost in some setups)
216+
if (brokerHost != "localhost" && brokerHost != "127.0.0.1")
153217
{
154-
if (await TestConnectionAsync(brokerHost, brokerPort, null, null, brokerSsl))
218+
if (await TestConnectionAsync("localhost", brokerPort, null, null, brokerSsl))
155219
{
156220
result.Success = true;
221+
result.BrokerHost = "localhost";
222+
result.Username = null;
223+
result.Password = null;
224+
return result;
225+
}
226+
227+
if (await TestConnectionAsync("127.0.0.1", brokerPort, null, null, brokerSsl))
228+
{
229+
result.Success = true;
230+
result.BrokerHost = "127.0.0.1";
157231
result.Username = null;
158232
result.Password = null;
159233
return result;
160234
}
161235
}
162236

163-
// ── Step 5: all attempts failed ──
237+
// ── Step 7: all attempts failed ──
164238
result.MosquittoNotInstalled = true;
165239
result.ErrorMessage = "Could not connect to the MQTT broker. " +
166-
"Please make sure the Mosquitto broker add-on is installed and running in Home Assistant.";
240+
"Please make sure the Mosquitto broker add-on is installed and running in Home Assistant " +
241+
"(Settings → Add-ons → Mosquitto Broker).";
167242
}
168243
catch (Exception ex)
169244
{
@@ -219,4 +294,4 @@ public static async Task<bool> TestConnectionAsync(
219294
return false;
220295
}
221296
}
222-
}
297+
}

0 commit comments

Comments
 (0)