ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
mdns_component.cpp
Go to the documentation of this file.
2#ifdef USE_MDNS
4#include "esphome/core/log.h"
6#include "mdns_component.h"
7
8#ifdef USE_ESP8266
9#include <pgmspace.h>
10// Macro to define strings in PROGMEM on ESP8266, regular memory on other platforms
11#define MDNS_STATIC_CONST_CHAR(name, value) static const char name[] PROGMEM = value
12#else
13// On non-ESP8266 platforms, use regular const char*
14#define MDNS_STATIC_CONST_CHAR(name, value) static constexpr const char name[] = value
15#endif
16
17#ifdef USE_API
19#endif
20#ifdef USE_DASHBOARD_IMPORT
22#endif
23#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
26#endif
27
28namespace esphome::mdns {
29
30ESPHOME_LOG_TAG(TAG, "mdns");
31
32#ifndef USE_WEBSERVER_PORT
33#define USE_WEBSERVER_PORT 80 // NOLINT
34#endif
35
36#ifndef USE_SENDSPIN_PORT
37#define USE_SENDSPIN_PORT 8928 // NOLINT
38#endif
39
40// Define all constant strings using the macro
41MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp");
42
43// Wrap build-time defines into flash storage
44MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION);
45
46#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
47MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
48MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
49
50// Same order as the default route in NetworkComponent::loop()
52 const bool ethernet = ethernet::global_eth_component->is_connected();
53 const bool wifi = wifi::global_wifi_component->is_connected();
54#ifdef USE_NETWORK_PRIMARY_INTERFACE_WIFI
55 if (wifi)
56 return NETWORK_WIFI;
57#endif
58 if (ethernet)
59 return NETWORK_ETHERNET;
60 if (wifi)
61 return NETWORK_WIFI;
62 return nullptr;
63}
64
65void MDNSComponent::update_network_txt_(const char *network) {
66 // With no interface connected, keep the last value instead of flapping to wifi
67 if (this->network_txt_ == nullptr || network == nullptr || network == this->network_txt_)
68 return;
69 if (!this->publish_network_txt_(network)) {
70 // The _esphomelib service is missing, so a retry would fail too
71 this->network_txt_ = nullptr;
72 return;
73 }
74 ESP_LOGD(TAG, "Network TXT: %s", network);
75 this->network_txt_ = network;
76#ifdef USE_MDNS_STORE_SERVICES
77 this->network_record_->value = MDNS_STR(network);
78#endif
79}
80
81#ifdef USE_MDNS_ACTIVE_INTERFACE_LISTENER
83 switch (interface) {
85 this->update_network_txt_(NETWORK_ETHERNET);
86 break;
88 this->update_network_txt_(NETWORK_WIFI);
89 break;
90 default:
91 break;
92 }
93}
94#endif
95#endif
96
98#ifdef USE_MDNS_STORE_SERVICES
99 auto &services = this->services_;
100#else
102 auto &services = services_storage;
103#endif
104
105#ifdef USE_MDNS_DEVICE_INFO_TXT
106#ifdef USE_MDNS_STORE_SERVICES
108 char *mac_ptr = this->mac_address_;
110 char *cfg_ptr = this->config_hash_str_;
111#else
112 char mac_address[MAC_ADDRESS_BUFFER_SIZE];
113 char config_hash_str[CONFIG_HASH_STR_SIZE];
114 get_mac_address_into_buffer(mac_address);
115 format_hex_to(config_hash_str, App.get_config_hash());
116 char *mac_ptr = mac_address;
117 char *cfg_ptr = config_hash_str;
118#endif
119#else
120 char *mac_ptr = nullptr;
121 char *cfg_ptr = nullptr;
122#endif
123
124 this->compile_records_(services, mac_ptr, cfg_ptr);
125 platform_register(this, services);
126}
127
129 const char *mac_address_buf, const char *config_hash_buf) {
130 // IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES
131 // in mdns/__init__.py. If you add a new service here, update both locations.
132
133#ifdef USE_MDNS_DEVICE_INFO_TXT
134 MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version");
135 MDNS_STATIC_CONST_CHAR(TXT_MAC, "mac");
136 MDNS_STATIC_CONST_CHAR(TXT_CONFIG_HASH, "config_hash");
137#endif
138
139#ifdef USE_API
140 MDNS_STATIC_CONST_CHAR(SERVICE_ESPHOMELIB, "_esphomelib");
141 MDNS_STATIC_CONST_CHAR(TXT_FRIENDLY_NAME, "friendly_name");
142 MDNS_STATIC_CONST_CHAR(TXT_PLATFORM, "platform");
143 MDNS_STATIC_CONST_CHAR(TXT_BOARD, "board");
144 MDNS_STATIC_CONST_CHAR(TXT_NETWORK, "network");
145 MDNS_STATIC_CONST_CHAR(VALUE_BOARD, ESPHOME_BOARD);
146
147 if (api::global_api_server != nullptr) {
148 auto &service = services.emplace_next();
149 service.service_type = MDNS_STR(SERVICE_ESPHOMELIB);
150 service.proto = MDNS_STR(SERVICE_TCP);
151 service.port = []() -> uint16_t { return api::global_api_server->get_port(); };
152
153 const auto &friendly_name = App.get_friendly_name();
154 bool friendly_name_empty = friendly_name.empty();
155
156 // Calculate exact capacity for txt_records
157 size_t txt_count = 4; // version, config_hash, mac, board (always present)
158 if (!friendly_name_empty) {
159 txt_count++; // friendly_name
160 }
161#if defined(USE_ESP8266) || defined(USE_ESP32) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_NRF52)
162 txt_count++; // platform
163#endif
164#if defined(USE_WIFI) || defined(USE_ETHERNET) || defined(USE_OPENTHREAD)
165 txt_count++; // network
166#endif
167#ifdef USE_API_NOISE
168 const bool api_has_psk = api::global_api_server->get_noise_ctx().has_psk();
169 txt_count++; // api_encryption or api_encryption_supported
170#ifndef USE_API_NOISE_PSK_FROM_YAML
171 if (!api_has_psk) {
172 txt_count++; // api_provisioning
173 }
174#endif
175#endif
176#ifdef USE_OTA_SIGNED_VERIFICATION
177 txt_count++; // ota_signed
178#endif
179#ifdef ESPHOME_PROJECT_NAME
180 txt_count += 2; // project_name and project_version
181#endif
182#ifdef USE_DASHBOARD_IMPORT
183 txt_count++; // package_import_url
184#endif
185
186 auto &txt_records = service.txt_records;
187 txt_records.init(txt_count);
188
189 if (!friendly_name_empty) {
190 txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), MDNS_STR(friendly_name.c_str())});
191 }
192 txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)});
193
194 // Config hash: passed from caller (either member buffer or stack buffer depending on USE_MDNS_STORE_SERVICES)
195 txt_records.push_back({MDNS_STR(TXT_CONFIG_HASH), MDNS_STR(config_hash_buf)});
196
197 // MAC address: passed from caller (either member buffer or stack buffer depending on USE_MDNS_STORE_SERVICES)
198 txt_records.push_back({MDNS_STR(TXT_MAC), MDNS_STR(mac_address_buf)});
199
200#ifdef USE_ESP8266
201 MDNS_STATIC_CONST_CHAR(PLATFORM_ESP8266, "ESP8266");
202 txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP8266)});
203#elif defined(USE_ESP32)
204 MDNS_STATIC_CONST_CHAR(PLATFORM_ESP32, "ESP32");
205 txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP32)});
206#elif defined(USE_RP2)
207 MDNS_STATIC_CONST_CHAR(PLATFORM_RP2, "RP2");
208 txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_RP2)});
209#elif defined(USE_LIBRETINY)
210 txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(lt_cpu_get_model_name())});
211#elif defined(USE_NRF52)
212 MDNS_STATIC_CONST_CHAR(PLATFORM_NRF52, "nRF52");
213 txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_NRF52)});
214#endif
215
216 txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(VALUE_BOARD)});
217
218#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
219 const char *network = this->connected_network_();
220 this->network_txt_ = network != nullptr ? network : NETWORK_WIFI;
221 txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(this->network_txt_)});
222#ifdef USE_MDNS_STORE_SERVICES
223 // services is services_ here, which never reallocates
224 this->network_record_ = &txt_records.back();
225#endif
226#elif defined(USE_WIFI)
227 MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
228 txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)});
229#elif defined(USE_ETHERNET)
230 MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
231 txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_ETHERNET)});
232#elif defined(USE_OPENTHREAD)
233 MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread");
234 txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_THREAD)});
235#endif
236
237#ifdef USE_API_NOISE
238 MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION, "api_encryption");
239 MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION_SUPPORTED, "api_encryption_supported");
240 MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256");
241 const char *encryption_key = api_has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED;
242 txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)});
243#ifndef USE_API_NOISE_PSK_FROM_YAML
244 if (!api_has_psk) {
245 // Unprovisioned device without a YAML key: advertise that the encryption
246 // key can be provisioned over a zero-PSK Noise connection. Gated on the
247 // YAML define so this survives the plaintext removal in 2027.2.0.
248 MDNS_STATIC_CONST_CHAR(TXT_API_PROVISIONING, "api_provisioning");
249 MDNS_STATIC_CONST_CHAR(VALUE_ZERO_PSK, "zero-psk");
250 txt_records.push_back({MDNS_STR(TXT_API_PROVISIONING), MDNS_STR(VALUE_ZERO_PSK)});
251 }
252#endif
253#endif
254
255#ifdef USE_OTA_SIGNED_VERIFICATION
256 // Signals that an unsigned OTA image is rejected; serial flash is unaffected.
257 MDNS_STATIC_CONST_CHAR(TXT_OTA_SIGNED, "ota_signed");
258 MDNS_STATIC_CONST_CHAR(VALUE_TRUE, "1");
259 txt_records.push_back({MDNS_STR(TXT_OTA_SIGNED), MDNS_STR(VALUE_TRUE)});
260#endif
261
262#ifdef ESPHOME_PROJECT_NAME
263 MDNS_STATIC_CONST_CHAR(TXT_PROJECT_NAME, "project_name");
264 MDNS_STATIC_CONST_CHAR(TXT_PROJECT_VERSION, "project_version");
265 MDNS_STATIC_CONST_CHAR(VALUE_PROJECT_NAME, ESPHOME_PROJECT_NAME);
266 MDNS_STATIC_CONST_CHAR(VALUE_PROJECT_VERSION, ESPHOME_PROJECT_VERSION);
267 txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), MDNS_STR(VALUE_PROJECT_NAME)});
268 txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), MDNS_STR(VALUE_PROJECT_VERSION)});
269#endif // ESPHOME_PROJECT_NAME
270
271#ifdef USE_DASHBOARD_IMPORT
272 MDNS_STATIC_CONST_CHAR(TXT_PACKAGE_IMPORT_URL, "package_import_url");
273 txt_records.push_back({MDNS_STR(TXT_PACKAGE_IMPORT_URL), MDNS_STR(dashboard_import::get_package_import_url())});
274#endif
275 }
276#endif // USE_API
277
278#ifdef USE_PROMETHEUS
279 MDNS_STATIC_CONST_CHAR(SERVICE_PROMETHEUS, "_prometheus-http");
280
281 auto &prom_service = services.emplace_next();
282 prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS);
283 prom_service.proto = MDNS_STR(SERVICE_TCP);
284 prom_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; };
285#endif
286
287#ifdef USE_SENDSPIN
288 MDNS_STATIC_CONST_CHAR(SERVICE_SENDSPIN, "_sendspin");
289 MDNS_STATIC_CONST_CHAR(TXT_SENDSPIN_PATH, "path");
290 MDNS_STATIC_CONST_CHAR(VALUE_SENDSPIN_PATH, "/sendspin");
291
292 auto &sendspin_service = services.emplace_next();
293 sendspin_service.service_type = MDNS_STR(SERVICE_SENDSPIN);
294 sendspin_service.proto = MDNS_STR(SERVICE_TCP);
295 sendspin_service.port = []() -> uint16_t { return USE_SENDSPIN_PORT; };
296 sendspin_service.txt_records = {{MDNS_STR(TXT_SENDSPIN_PATH), MDNS_STR(VALUE_SENDSPIN_PATH)}};
297#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
298 // Starts disabled; the sendspin hub enables it once its server is running
299 sendspin_service.enabled = false;
300#endif
301#endif
302
303#ifdef USE_WEBSERVER
304 MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http");
305
306 auto &web_service = services.emplace_next();
307 web_service.service_type = MDNS_STR(SERVICE_HTTP);
308 web_service.proto = MDNS_STR(SERVICE_TCP);
309 web_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; };
310#ifndef USE_API
311 // Without the native API there is no _esphomelib service, so publish the
312 // device info here for the device builder to discover.
313 web_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)},
314 {MDNS_STR(TXT_MAC), MDNS_STR(mac_address_buf)},
315 {MDNS_STR(TXT_CONFIG_HASH), MDNS_STR(config_hash_buf)}};
316#endif
317#endif
318
319#if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_SENDSPIN) && !defined(USE_WEBSERVER) && \
320 !defined(USE_MDNS_EXTRA_SERVICES)
321 MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http");
322
323 // Publish "http" service if not using native API or any other services
324 // This is just to have *some* mDNS service so that .local resolution works
325 auto &fallback_service = services.emplace_next();
326 fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
327 fallback_service.proto = MDNS_STR(SERVICE_TCP);
328 fallback_service.port = []() -> uint16_t { return USE_WEBSERVER_PORT; };
329 fallback_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)},
330 {MDNS_STR(TXT_MAC), MDNS_STR(mac_address_buf)},
331 {MDNS_STR(TXT_CONFIG_HASH), MDNS_STR(config_hash_buf)}};
332#endif
333}
334
336 ESP_LOGCONFIG(TAG,
337 "mDNS:\n"
338 " Hostname: %s",
339 App.get_name().c_str());
340#ifdef USE_MDNS_STORE_SERVICES
341 ESP_LOGV(TAG, " Services:");
342 for (const auto &service : this->services_) {
343 ESP_LOGV(TAG, " - %s, %s, %d", MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto),
344 service.port.value());
345 for (const auto &record : service.txt_records) {
346 ESP_LOGV(TAG, " TXT: %s = %s", MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
347 }
348 }
349#endif
350}
351
352} // namespace esphome::mdns
353#endif
const StringRef & get_name() const
Get the name of this Application set by pre_setup().
uint32_t get_config_hash()
Get the config hash as a 32-bit integer.
const StringRef & get_friendly_name() const
Get the friendly name of this Application set by pre_setup().
Minimal static vector - saves memory by avoiding std::vector overhead.
Definition helpers.h:286
constexpr const char * c_str() const
Definition string_ref.h:78
constexpr bool empty() const
Definition string_ref.h:81
uint16_t get_port() const
Definition api_server.h:62
noise::NoiseContext & get_noise_ctx()
Definition api_server.h:92
const char * connected_network_() const
The first connected interface in network: priority order ("ethernet" or "wifi"), else nullptr.
const char * network_txt_
Advertised value; nullptr until the _esphomelib service is registered, and after a failed publish.
void on_active_interface(network::NetworkInterface interface) override
char config_hash_str_[CONFIG_HASH_STR_SIZE]
Fixed buffer for config hash hex string (only needed when services are stored)
static constexpr size_t CONFIG_HASH_STR_SIZE
Size of buffer required for config hash hex string (8 hex chars + null terminator)
void compile_records_(StaticVector< MDNSService, MDNS_SERVICE_COUNT > &services, const char *mac_address_buf, const char *config_hash_buf)
void setup_buffers_and_register_(PlatformRegisterFn platform_register)
void update_network_txt_(const char *network)
Publish network as the "network" TXT value if it changed; nullptr keeps the current value.
char mac_address_[MAC_ADDRESS_BUFFER_SIZE]
Fixed buffer for MAC address (only needed when services are stored)
void(*)(MDNSComponent *, StaticVector< MDNSService, MDNS_SERVICE_COUNT > &) PlatformRegisterFn
Helper to set up services and MAC buffers, then call platform-specific registration.
MDNSTXTRecord * network_record_
The "network" record in services_, kept in step with network_txt_.
bool publish_network_txt_(const char *network)
Platform-specific: replace the "network" TXT value of the registered _esphomelib service.
StaticVector< MDNSService, MDNS_SERVICE_COUNT > services_
APIServer * global_api_server
EthernetComponent * global_eth_component
ESPHOME_LOG_TAG(TAG, "mdns")
MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp")
NetworkInterface
The interface holding the default route.
WiFiComponent * global_wifi_component
void get_mac_address_into_buffer(std::span< char, MAC_ADDRESS_BUFFER_SIZE > buf)
Get the device MAC address into the given buffer, in lowercase hex notation.
Definition helpers.cpp:848
char * format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length)
Format byte array as lowercase hex to buffer (base implementation).
Definition helpers.cpp:428
Application App
Global storage of Application pointer - only one Application can exist.