esphome 2025.10.0b1__py3-none-any.whl → 2025.10.0b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. esphome/__main__.py +7 -0
  2. esphome/components/canbus/canbus.h +3 -3
  3. esphome/components/dashboard_import/dashboard_import.cpp +1 -1
  4. esphome/components/dashboard_import/dashboard_import.h +1 -1
  5. esphome/components/esp32/__init__.py +9 -8
  6. esphome/components/esp32_ble/__init__.py +9 -1
  7. esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +0 -4
  8. esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +0 -4
  9. esphome/components/esp32_improv/esp32_improv_component.cpp +33 -21
  10. esphome/components/esp32_improv/esp32_improv_component.h +1 -0
  11. esphome/components/esphome/ota/ota_esphome.cpp +1 -1
  12. esphome/components/json/json_util.cpp +8 -2
  13. esphome/components/mcp23xxx_base/mcp23xxx_base.h +3 -3
  14. esphome/components/mdns/__init__.py +76 -15
  15. esphome/components/mdns/mdns_component.cpp +52 -57
  16. esphome/components/mdns/mdns_component.h +12 -1
  17. esphome/components/mdns/mdns_esp32.cpp +3 -9
  18. esphome/components/mdns/mdns_esp8266.cpp +1 -1
  19. esphome/components/mdns/mdns_libretiny.cpp +1 -2
  20. esphome/components/mdns/mdns_rp2040.cpp +1 -2
  21. esphome/components/opentherm/opentherm.cpp +5 -5
  22. esphome/components/opentherm/opentherm.h +3 -3
  23. esphome/components/openthread/openthread.cpp +5 -3
  24. esphome/components/uart/__init__.py +1 -1
  25. esphome/components/usb_host/__init__.py +10 -1
  26. esphome/components/usb_host/usb_host.h +24 -18
  27. esphome/components/usb_host/usb_host_client.cpp +18 -39
  28. esphome/components/wifi/wifi_component.cpp +3 -2
  29. esphome/const.py +1 -1
  30. esphome/core/__init__.py +2 -0
  31. esphome/core/defines.h +2 -0
  32. esphome/core/entity_helpers.py +9 -6
  33. esphome/espota2.py +1 -1
  34. esphome/pins.py +2 -2
  35. esphome/platformio_api.py +31 -0
  36. {esphome-2025.10.0b1.dist-info → esphome-2025.10.0b2.dist-info}/METADATA +3 -3
  37. {esphome-2025.10.0b1.dist-info → esphome-2025.10.0b2.dist-info}/RECORD +41 -41
  38. {esphome-2025.10.0b1.dist-info → esphome-2025.10.0b2.dist-info}/WHEEL +0 -0
  39. {esphome-2025.10.0b1.dist-info → esphome-2025.10.0b2.dist-info}/entry_points.txt +0 -0
  40. {esphome-2025.10.0b1.dist-info → esphome-2025.10.0b2.dist-info}/licenses/LICENSE +0 -0
  41. {esphome-2025.10.0b1.dist-info → esphome-2025.10.0b2.dist-info}/top_level.txt +0 -0
esphome/__main__.py CHANGED
@@ -1002,6 +1002,12 @@ def parse_args(argv):
1002
1002
  action="append",
1003
1003
  default=[],
1004
1004
  )
1005
+ options_parser.add_argument(
1006
+ "--testing-mode",
1007
+ help="Enable testing mode (disables validation checks for grouped component testing)",
1008
+ action="store_true",
1009
+ default=False,
1010
+ )
1005
1011
 
1006
1012
  parser = argparse.ArgumentParser(
1007
1013
  description=f"ESPHome {const.__version__}", parents=[options_parser]
@@ -1260,6 +1266,7 @@ def run_esphome(argv):
1260
1266
 
1261
1267
  args = parse_args(argv)
1262
1268
  CORE.dashboard = args.dashboard
1269
+ CORE.testing_mode = args.testing_mode
1263
1270
 
1264
1271
  # Create address cache from command-line arguments
1265
1272
  CORE.address_cache = AddressCache.from_cli_args(
@@ -105,9 +105,9 @@ class Canbus : public Component {
105
105
  CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
106
106
  callback_manager_{};
107
107
 
108
- virtual bool setup_internal();
109
- virtual Error send_message(struct CanFrame *frame);
110
- virtual Error read_message(struct CanFrame *frame);
108
+ virtual bool setup_internal() = 0;
109
+ virtual Error send_message(struct CanFrame *frame) = 0;
110
+ virtual Error read_message(struct CanFrame *frame) = 0;
111
111
  };
112
112
 
113
113
  template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public Parented<Canbus> {
@@ -5,7 +5,7 @@ namespace dashboard_import {
5
5
 
6
6
  static std::string g_package_import_url; // NOLINT
7
7
 
8
- std::string get_package_import_url() { return g_package_import_url; }
8
+ const std::string &get_package_import_url() { return g_package_import_url; }
9
9
  void set_package_import_url(std::string url) { g_package_import_url = std::move(url); }
10
10
 
11
11
  } // namespace dashboard_import
@@ -5,7 +5,7 @@
5
5
  namespace esphome {
6
6
  namespace dashboard_import {
7
7
 
8
- std::string get_package_import_url();
8
+ const std::string &get_package_import_url();
9
9
  void set_package_import_url(std::string url);
10
10
 
11
11
  } // namespace dashboard_import
@@ -314,11 +314,12 @@ def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
314
314
  # - https://github.com/espressif/arduino-esp32/releases
315
315
  ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
316
316
  "recommended": cv.Version(3, 2, 1),
317
- "latest": cv.Version(3, 3, 1),
318
- "dev": cv.Version(3, 3, 1),
317
+ "latest": cv.Version(3, 3, 2),
318
+ "dev": cv.Version(3, 3, 2),
319
319
  }
320
320
  ARDUINO_PLATFORM_VERSION_LOOKUP = {
321
- cv.Version(3, 3, 1): cv.Version(55, 3, 31),
321
+ cv.Version(3, 3, 2): cv.Version(55, 3, 31, "1"),
322
+ cv.Version(3, 3, 1): cv.Version(55, 3, 31, "1"),
322
323
  cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"),
323
324
  cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"),
324
325
  cv.Version(3, 2, 0): cv.Version(54, 3, 20),
@@ -336,8 +337,8 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
336
337
  "dev": cv.Version(5, 5, 1),
337
338
  }
338
339
  ESP_IDF_PLATFORM_VERSION_LOOKUP = {
339
- cv.Version(5, 5, 1): cv.Version(55, 3, 31),
340
- cv.Version(5, 5, 0): cv.Version(55, 3, 31),
340
+ cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"),
341
+ cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"),
341
342
  cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
342
343
  cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
343
344
  cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"),
@@ -352,8 +353,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
352
353
  # - https://github.com/pioarduino/platform-espressif32/releases
353
354
  PLATFORM_VERSION_LOOKUP = {
354
355
  "recommended": cv.Version(54, 3, 21, "2"),
355
- "latest": cv.Version(55, 3, 31),
356
- "dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
356
+ "latest": cv.Version(55, 3, 31, "1"),
357
+ "dev": cv.Version(55, 3, 31, "1"),
357
358
  }
358
359
 
359
360
 
@@ -645,6 +646,7 @@ def _show_framework_migration_message(name: str, variant: str) -> None:
645
646
  + "Why change? ESP-IDF offers:\n"
646
647
  + color(AnsiFore.GREEN, " ✨ Up to 40% smaller binaries\n")
647
648
  + color(AnsiFore.GREEN, " 🚀 Better performance and optimization\n")
649
+ + color(AnsiFore.GREEN, " ⚡ 2-3x faster compile times\n")
648
650
  + color(AnsiFore.GREEN, " 📦 Custom-built firmware for your exact needs\n")
649
651
  + color(
650
652
  AnsiFore.GREEN,
@@ -652,7 +654,6 @@ def _show_framework_migration_message(name: str, variant: str) -> None:
652
654
  )
653
655
  + "\n"
654
656
  + "Trade-offs:\n"
655
- + color(AnsiFore.YELLOW, " ⏱️ Compile times are ~25% longer\n")
656
657
  + color(AnsiFore.YELLOW, " 🔄 Some components need migration\n")
657
658
  + "\n"
658
659
  + "What should I do?\n"
@@ -285,6 +285,10 @@ def consume_connection_slots(
285
285
 
286
286
  def validate_connection_slots(max_connections: int) -> None:
287
287
  """Validate that BLE connection slots don't exceed the configured maximum."""
288
+ # Skip validation in testing mode to allow component grouping
289
+ if CORE.testing_mode:
290
+ return
291
+
288
292
  ble_data = CORE.data.get(KEY_ESP32_BLE, {})
289
293
  used_slots = ble_data.get(KEY_USED_CONNECTION_SLOTS, [])
290
294
  num_used = len(used_slots)
@@ -332,12 +336,16 @@ def final_validation(config):
332
336
 
333
337
  # Check if BLE Server is needed
334
338
  has_ble_server = "esp32_ble_server" in full_config
335
- add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server)
336
339
 
337
340
  # Check if BLE Client is needed (via esp32_ble_tracker or esp32_ble_client)
338
341
  has_ble_client = (
339
342
  "esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config
340
343
  )
344
+
345
+ # ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled
346
+ # This is an internal dependency in the Bluedroid stack (tested ESP-IDF 5.4.2-5.5.1)
347
+ # See: https://github.com/espressif/esp-idf/issues/17724
348
+ add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client)
341
349
  add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client)
342
350
 
343
351
  # Handle max_connections: check for deprecated location in esp32_ble_tracker
@@ -14,10 +14,6 @@
14
14
  #include "esphome/core/hal.h"
15
15
  #include "esphome/core/helpers.h"
16
16
 
17
- #ifdef USE_ARDUINO
18
- #include <esp32-hal-bt.h>
19
- #endif
20
-
21
17
  namespace esphome {
22
18
  namespace esp32_ble_beacon {
23
19
 
@@ -25,10 +25,6 @@
25
25
  #include <esp_coexist.h>
26
26
  #endif
27
27
 
28
- #ifdef USE_ARDUINO
29
- #include <esp32-hal-bt.h>
30
- #endif
31
-
32
28
  #define MBEDTLS_AES_ALT
33
29
  #include <aes_alt.h>
34
30
 
@@ -143,6 +143,7 @@ void ESP32ImprovComponent::loop() {
143
143
  #else
144
144
  this->set_state_(improv::STATE_AUTHORIZED);
145
145
  #endif
146
+ this->check_wifi_connection_();
146
147
  break;
147
148
  }
148
149
  case improv::STATE_AUTHORIZED: {
@@ -156,31 +157,12 @@ void ESP32ImprovComponent::loop() {
156
157
  if (!this->check_identify_()) {
157
158
  this->set_status_indicator_state_((now % 1000) < 500);
158
159
  }
160
+ this->check_wifi_connection_();
159
161
  break;
160
162
  }
161
163
  case improv::STATE_PROVISIONING: {
162
164
  this->set_status_indicator_state_((now % 200) < 100);
163
- if (wifi::global_wifi_component->is_connected()) {
164
- wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
165
- this->connecting_sta_.get_password());
166
- this->connecting_sta_ = {};
167
- this->cancel_timeout("wifi-connect-timeout");
168
- this->set_state_(improv::STATE_PROVISIONED);
169
-
170
- std::vector<std::string> urls = {ESPHOME_MY_LINK};
171
- #ifdef USE_WEBSERVER
172
- for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
173
- if (ip.is_ip4()) {
174
- std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
175
- urls.push_back(webserver_url);
176
- break;
177
- }
178
- }
179
- #endif
180
- std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
181
- this->send_response_(data);
182
- this->stop();
183
- }
165
+ this->check_wifi_connection_();
184
166
  break;
185
167
  }
186
168
  case improv::STATE_PROVISIONED: {
@@ -392,6 +374,36 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() {
392
374
  wifi::global_wifi_component->clear_sta();
393
375
  }
394
376
 
377
+ void ESP32ImprovComponent::check_wifi_connection_() {
378
+ if (!wifi::global_wifi_component->is_connected()) {
379
+ return;
380
+ }
381
+
382
+ if (this->state_ == improv::STATE_PROVISIONING) {
383
+ wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password());
384
+ this->connecting_sta_ = {};
385
+ this->cancel_timeout("wifi-connect-timeout");
386
+
387
+ std::vector<std::string> urls = {ESPHOME_MY_LINK};
388
+ #ifdef USE_WEBSERVER
389
+ for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
390
+ if (ip.is_ip4()) {
391
+ std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
392
+ urls.push_back(webserver_url);
393
+ break;
394
+ }
395
+ }
396
+ #endif
397
+ std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
398
+ this->send_response_(data);
399
+ } else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
400
+ ESP_LOGD(TAG, "WiFi provisioned externally");
401
+ }
402
+
403
+ this->set_state_(improv::STATE_PROVISIONED);
404
+ this->stop();
405
+ }
406
+
395
407
  void ESP32ImprovComponent::advertise_service_data_() {
396
408
  uint8_t service_data[IMPROV_SERVICE_DATA_SIZE] = {};
397
409
  service_data[0] = IMPROV_PROTOCOL_ID_1; // PR
@@ -111,6 +111,7 @@ class ESP32ImprovComponent : public Component {
111
111
  void send_response_(std::vector<uint8_t> &response);
112
112
  void process_incoming_data_();
113
113
  void on_wifi_connect_timeout_();
114
+ void check_wifi_connection_();
114
115
  bool check_identify_();
115
116
  void advertise_service_data_();
116
117
  #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
@@ -29,7 +29,7 @@ namespace esphome {
29
29
  static const char *const TAG = "esphome.ota";
30
30
  static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
31
31
  static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size for OTA data transfer
32
- static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 10000; // milliseconds for initial handshake
32
+ static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
33
33
  static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
34
34
 
35
35
  #ifdef USE_OTA_PASSWORD
@@ -8,6 +8,13 @@ namespace json {
8
8
 
9
9
  static const char *const TAG = "json";
10
10
 
11
+ #ifdef USE_PSRAM
12
+ // Global allocator that outlives all JsonDocuments returned by parse_json()
13
+ // This prevents dangling pointer issues when JsonDocuments are returned from functions
14
+ // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - Must be mutable for ArduinoJson::Allocator
15
+ static SpiRamAllocator global_json_allocator;
16
+ #endif
17
+
11
18
  std::string build_json(const json_build_t &f) {
12
19
  // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
13
20
  JsonBuilder builder;
@@ -33,8 +40,7 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
33
40
  return JsonObject(); // return unbound object
34
41
  }
35
42
  #ifdef USE_PSRAM
36
- auto doc_allocator = SpiRamAllocator();
37
- JsonDocument json_document(&doc_allocator);
43
+ JsonDocument json_document(&global_json_allocator);
38
44
  #else
39
45
  JsonDocument json_document;
40
46
  #endif
@@ -21,11 +21,11 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander:
21
21
 
22
22
  protected:
23
23
  // read a given register
24
- virtual bool read_reg(uint8_t reg, uint8_t *value);
24
+ virtual bool read_reg(uint8_t reg, uint8_t *value) = 0;
25
25
  // write a value to a given register
26
- virtual bool write_reg(uint8_t reg, uint8_t value);
26
+ virtual bool write_reg(uint8_t reg, uint8_t value) = 0;
27
27
  // update registers with given pin value.
28
- virtual void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a);
28
+ virtual void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a) = 0;
29
29
 
30
30
  bool open_drain_ints_;
31
31
  };
@@ -11,7 +11,7 @@ from esphome.const import (
11
11
  CONF_SERVICES,
12
12
  PlatformFramework,
13
13
  )
14
- from esphome.core import CORE, coroutine_with_priority
14
+ from esphome.core import CORE, Lambda, coroutine_with_priority
15
15
  from esphome.coroutine import CoroPriority
16
16
 
17
17
  CODEOWNERS = ["@esphome/core"]
@@ -58,17 +58,64 @@ CONFIG_SCHEMA = cv.All(
58
58
  )
59
59
 
60
60
 
61
- def mdns_txt_record(key: str, value: str):
62
- return cg.StructInitializer(
63
- MDNSTXTRecord,
64
- ("key", cg.RawExpression(f"MDNS_STR({cg.safe_exp(key)})")),
65
- ("value", value),
61
+ def mdns_txt_record(key: str, value: str) -> cg.RawExpression:
62
+ """Create a mDNS TXT record.
63
+
64
+ Public API for external components. Do not remove.
65
+
66
+ Args:
67
+ key: The TXT record key
68
+ value: The TXT record value (static string only)
69
+
70
+ Returns:
71
+ A RawExpression representing a MDNSTXTRecord struct
72
+ """
73
+ return cg.RawExpression(
74
+ f"{{MDNS_STR({cg.safe_exp(key)}), MDNS_STR({cg.safe_exp(value)})}}"
66
75
  )
67
76
 
68
77
 
78
+ async def _mdns_txt_record_templated(
79
+ mdns_comp: cg.Pvariable, key: str, value: Lambda | str
80
+ ) -> cg.RawExpression:
81
+ """Create a mDNS TXT record with support for templated values.
82
+
83
+ Internal helper function.
84
+
85
+ Args:
86
+ mdns_comp: The MDNSComponent instance (from cg.get_variable())
87
+ key: The TXT record key
88
+ value: The TXT record value (can be a static string or a lambda template)
89
+
90
+ Returns:
91
+ A RawExpression representing a MDNSTXTRecord struct
92
+ """
93
+ if not cg.is_template(value):
94
+ # It's a static string - use directly in flash, no need to store in vector
95
+ return mdns_txt_record(key, value)
96
+ # It's a lambda - evaluate and store using helper
97
+ templated_value = await cg.templatable(value, [], cg.std_string)
98
+ safe_key = cg.safe_exp(key)
99
+ dynamic_call = f"{mdns_comp}->add_dynamic_txt_value(({templated_value})())"
100
+ return cg.RawExpression(f"{{MDNS_STR({safe_key}), MDNS_STR({dynamic_call})}}")
101
+
102
+
69
103
  def mdns_service(
70
- service: str, proto: str, port: int, txt_records: list[dict[str, str]]
71
- ):
104
+ service: str, proto: str, port: int, txt_records: list[cg.RawExpression]
105
+ ) -> cg.StructInitializer:
106
+ """Create a mDNS service.
107
+
108
+ Public API for external components. Do not remove.
109
+
110
+ Args:
111
+ service: Service name (e.g., "_http")
112
+ proto: Protocol (e.g., "_tcp" or "_udp")
113
+ port: Port number
114
+ txt_records: List of MDNSTXTRecord expressions
115
+
116
+ Returns:
117
+ A StructInitializer representing a MDNSService struct
118
+ """
72
119
  return cg.StructInitializer(
73
120
  MDNSService,
74
121
  ("service_type", cg.RawExpression(f"MDNS_STR({cg.safe_exp(service)})")),
@@ -107,23 +154,37 @@ async def to_code(config):
107
154
  # Ensure at least 1 service (fallback service)
108
155
  cg.add_define("MDNS_SERVICE_COUNT", max(1, service_count))
109
156
 
157
+ # Calculate compile-time dynamic TXT value count
158
+ # Dynamic values are those that cannot be stored in flash at compile time
159
+ dynamic_txt_count = 0
160
+ if "api" in CORE.config:
161
+ # Always: get_mac_address()
162
+ dynamic_txt_count += 1
163
+ # User-provided templatable TXT values (only lambdas, not static strings)
164
+ dynamic_txt_count += sum(
165
+ 1
166
+ for service in config[CONF_SERVICES]
167
+ for txt_value in service[CONF_TXT].values()
168
+ if cg.is_template(txt_value)
169
+ )
170
+
171
+ # Ensure at least 1 to avoid zero-size array
172
+ cg.add_define("MDNS_DYNAMIC_TXT_COUNT", max(1, dynamic_txt_count))
173
+
110
174
  var = cg.new_Pvariable(config[CONF_ID])
111
175
  await cg.register_component(var, config)
112
176
 
113
177
  for service in config[CONF_SERVICES]:
114
- txt = [
115
- cg.StructInitializer(
116
- MDNSTXTRecord,
117
- ("key", cg.RawExpression(f"MDNS_STR({cg.safe_exp(txt_key)})")),
118
- ("value", await cg.templatable(txt_value, [], cg.std_string)),
119
- )
178
+ txt_records = [
179
+ await _mdns_txt_record_templated(var, txt_key, txt_value)
120
180
  for txt_key, txt_value in service[CONF_TXT].items()
121
181
  ]
182
+
122
183
  exp = mdns_service(
123
184
  service[CONF_SERVICE],
124
185
  service[CONF_PROTOCOL],
125
186
  await cg.templatable(service[CONF_PORT], [], cg.uint16),
126
- txt,
187
+ txt_records,
127
188
  )
128
189
 
129
190
  cg.add(var.add_extra_service(exp))
@@ -9,21 +9,9 @@
9
9
  #include <pgmspace.h>
10
10
  // Macro to define strings in PROGMEM on ESP8266, regular memory on other platforms
11
11
  #define MDNS_STATIC_CONST_CHAR(name, value) static const char name[] PROGMEM = value
12
- // Helper to convert PROGMEM string to std::string for TemplatableValue
13
- // Only define this function if we have services that will use it
14
- #if defined(USE_API) || defined(USE_PROMETHEUS) || defined(USE_WEBSERVER) || defined(USE_MDNS_EXTRA_SERVICES)
15
- static std::string mdns_str_value(PGM_P str) {
16
- char buf[64];
17
- strncpy_P(buf, str, sizeof(buf) - 1);
18
- buf[sizeof(buf) - 1] = '\0';
19
- return std::string(buf);
20
- }
21
- #define MDNS_STR_VALUE(name) mdns_str_value(name)
22
- #endif
23
12
  #else
24
13
  // On non-ESP8266 platforms, use regular const char*
25
14
  #define MDNS_STATIC_CONST_CHAR(name, value) static constexpr const char name[] = value
26
- #define MDNS_STR_VALUE(name) std::string(name)
27
15
  #endif
28
16
 
29
17
  #ifdef USE_API
@@ -43,30 +31,10 @@ static const char *const TAG = "mdns";
43
31
  #endif
44
32
 
45
33
  // Define all constant strings using the macro
46
- MDNS_STATIC_CONST_CHAR(SERVICE_ESPHOMELIB, "_esphomelib");
47
34
  MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp");
48
- MDNS_STATIC_CONST_CHAR(SERVICE_PROMETHEUS, "_prometheus-http");
49
- MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http");
50
-
51
- MDNS_STATIC_CONST_CHAR(TXT_FRIENDLY_NAME, "friendly_name");
52
- MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version");
53
- MDNS_STATIC_CONST_CHAR(TXT_MAC, "mac");
54
- MDNS_STATIC_CONST_CHAR(TXT_PLATFORM, "platform");
55
- MDNS_STATIC_CONST_CHAR(TXT_BOARD, "board");
56
- MDNS_STATIC_CONST_CHAR(TXT_NETWORK, "network");
57
- MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION, "api_encryption");
58
- MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION_SUPPORTED, "api_encryption_supported");
59
- MDNS_STATIC_CONST_CHAR(TXT_PROJECT_NAME, "project_name");
60
- MDNS_STATIC_CONST_CHAR(TXT_PROJECT_VERSION, "project_version");
61
- MDNS_STATIC_CONST_CHAR(TXT_PACKAGE_IMPORT_URL, "package_import_url");
62
-
63
- MDNS_STATIC_CONST_CHAR(PLATFORM_ESP8266, "ESP8266");
64
- MDNS_STATIC_CONST_CHAR(PLATFORM_ESP32, "ESP32");
65
- MDNS_STATIC_CONST_CHAR(PLATFORM_RP2040, "RP2040");
66
-
67
- MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
68
- MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
69
- MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread");
35
+
36
+ // Wrap build-time defines into flash storage
37
+ MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION);
70
38
 
71
39
  void MDNSComponent::compile_records_() {
72
40
  this->hostname_ = App.get_name();
@@ -75,6 +43,15 @@ void MDNSComponent::compile_records_() {
75
43
  // in mdns/__init__.py. If you add a new service here, update both locations.
76
44
 
77
45
  #ifdef USE_API
46
+ MDNS_STATIC_CONST_CHAR(SERVICE_ESPHOMELIB, "_esphomelib");
47
+ MDNS_STATIC_CONST_CHAR(TXT_FRIENDLY_NAME, "friendly_name");
48
+ MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version");
49
+ MDNS_STATIC_CONST_CHAR(TXT_MAC, "mac");
50
+ MDNS_STATIC_CONST_CHAR(TXT_PLATFORM, "platform");
51
+ MDNS_STATIC_CONST_CHAR(TXT_BOARD, "board");
52
+ MDNS_STATIC_CONST_CHAR(TXT_NETWORK, "network");
53
+ MDNS_STATIC_CONST_CHAR(VALUE_BOARD, ESPHOME_BOARD);
54
+
78
55
  if (api::global_api_server != nullptr) {
79
56
  auto &service = this->services_.emplace_next();
80
57
  service.service_type = MDNS_STR(SERVICE_ESPHOMELIB);
@@ -109,52 +86,66 @@ void MDNSComponent::compile_records_() {
109
86
  txt_records.reserve(txt_count);
110
87
 
111
88
  if (!friendly_name_empty) {
112
- txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), friendly_name});
89
+ txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), MDNS_STR(friendly_name.c_str())});
113
90
  }
114
- txt_records.push_back({MDNS_STR(TXT_VERSION), ESPHOME_VERSION});
115
- txt_records.push_back({MDNS_STR(TXT_MAC), get_mac_address()});
91
+ txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)});
92
+ txt_records.push_back({MDNS_STR(TXT_MAC), MDNS_STR(this->add_dynamic_txt_value(get_mac_address()))});
116
93
 
117
94
  #ifdef USE_ESP8266
118
- txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR_VALUE(PLATFORM_ESP8266)});
95
+ MDNS_STATIC_CONST_CHAR(PLATFORM_ESP8266, "ESP8266");
96
+ txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP8266)});
119
97
  #elif defined(USE_ESP32)
120
- txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR_VALUE(PLATFORM_ESP32)});
98
+ MDNS_STATIC_CONST_CHAR(PLATFORM_ESP32, "ESP32");
99
+ txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP32)});
121
100
  #elif defined(USE_RP2040)
122
- txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR_VALUE(PLATFORM_RP2040)});
101
+ MDNS_STATIC_CONST_CHAR(PLATFORM_RP2040, "RP2040");
102
+ txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_RP2040)});
123
103
  #elif defined(USE_LIBRETINY)
124
- txt_records.push_back({MDNS_STR(TXT_PLATFORM), lt_cpu_get_model_name()});
104
+ txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(lt_cpu_get_model_name())});
125
105
  #endif
126
106
 
127
- txt_records.push_back({MDNS_STR(TXT_BOARD), ESPHOME_BOARD});
107
+ txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(VALUE_BOARD)});
128
108
 
129
109
  #if defined(USE_WIFI)
130
- txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR_VALUE(NETWORK_WIFI)});
110
+ MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
111
+ txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)});
131
112
  #elif defined(USE_ETHERNET)
132
- txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR_VALUE(NETWORK_ETHERNET)});
113
+ MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
114
+ txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_ETHERNET)});
133
115
  #elif defined(USE_OPENTHREAD)
134
- txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR_VALUE(NETWORK_THREAD)});
116
+ MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread");
117
+ txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_THREAD)});
135
118
  #endif
136
119
 
137
120
  #ifdef USE_API_NOISE
121
+ MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION, "api_encryption");
122
+ MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION_SUPPORTED, "api_encryption_supported");
138
123
  MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256");
139
- if (api::global_api_server->get_noise_ctx()->has_psk()) {
140
- txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION), MDNS_STR_VALUE(NOISE_ENCRYPTION)});
141
- } else {
142
- txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION_SUPPORTED), MDNS_STR_VALUE(NOISE_ENCRYPTION)});
143
- }
124
+ bool has_psk = api::global_api_server->get_noise_ctx()->has_psk();
125
+ const char *encryption_key = has_psk ? TXT_API_ENCRYPTION : TXT_API_ENCRYPTION_SUPPORTED;
126
+ txt_records.push_back({MDNS_STR(encryption_key), MDNS_STR(NOISE_ENCRYPTION)});
144
127
  #endif
145
128
 
146
129
  #ifdef ESPHOME_PROJECT_NAME
147
- txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), ESPHOME_PROJECT_NAME});
148
- txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), ESPHOME_PROJECT_VERSION});
130
+ MDNS_STATIC_CONST_CHAR(TXT_PROJECT_NAME, "project_name");
131
+ MDNS_STATIC_CONST_CHAR(TXT_PROJECT_VERSION, "project_version");
132
+ MDNS_STATIC_CONST_CHAR(VALUE_PROJECT_NAME, ESPHOME_PROJECT_NAME);
133
+ MDNS_STATIC_CONST_CHAR(VALUE_PROJECT_VERSION, ESPHOME_PROJECT_VERSION);
134
+ txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), MDNS_STR(VALUE_PROJECT_NAME)});
135
+ txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), MDNS_STR(VALUE_PROJECT_VERSION)});
149
136
  #endif // ESPHOME_PROJECT_NAME
150
137
 
151
138
  #ifdef USE_DASHBOARD_IMPORT
152
- txt_records.push_back({MDNS_STR(TXT_PACKAGE_IMPORT_URL), dashboard_import::get_package_import_url()});
139
+ MDNS_STATIC_CONST_CHAR(TXT_PACKAGE_IMPORT_URL, "package_import_url");
140
+ txt_records.push_back(
141
+ {MDNS_STR(TXT_PACKAGE_IMPORT_URL), MDNS_STR(dashboard_import::get_package_import_url().c_str())});
153
142
  #endif
154
143
  }
155
144
  #endif // USE_API
156
145
 
157
146
  #ifdef USE_PROMETHEUS
147
+ MDNS_STATIC_CONST_CHAR(SERVICE_PROMETHEUS, "_prometheus-http");
148
+
158
149
  auto &prom_service = this->services_.emplace_next();
159
150
  prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS);
160
151
  prom_service.proto = MDNS_STR(SERVICE_TCP);
@@ -162,6 +153,8 @@ void MDNSComponent::compile_records_() {
162
153
  #endif
163
154
 
164
155
  #ifdef USE_WEBSERVER
156
+ MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http");
157
+
165
158
  auto &web_service = this->services_.emplace_next();
166
159
  web_service.service_type = MDNS_STR(SERVICE_HTTP);
167
160
  web_service.proto = MDNS_STR(SERVICE_TCP);
@@ -169,13 +162,16 @@ void MDNSComponent::compile_records_() {
169
162
  #endif
170
163
 
171
164
  #if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_WEBSERVER) && !defined(USE_MDNS_EXTRA_SERVICES)
165
+ MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http");
166
+ MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version");
167
+
172
168
  // Publish "http" service if not using native API or any other services
173
169
  // This is just to have *some* mDNS service so that .local resolution works
174
170
  auto &fallback_service = this->services_.emplace_next();
175
171
  fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
176
172
  fallback_service.proto = MDNS_STR(SERVICE_TCP);
177
173
  fallback_service.port = USE_WEBSERVER_PORT;
178
- fallback_service.txt_records.push_back({MDNS_STR(TXT_VERSION), ESPHOME_VERSION});
174
+ fallback_service.txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)});
179
175
  #endif
180
176
  }
181
177
 
@@ -190,8 +186,7 @@ void MDNSComponent::dump_config() {
190
186
  ESP_LOGV(TAG, " - %s, %s, %d", MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto),
191
187
  const_cast<TemplatableValue<uint16_t> &>(service.port).value());
192
188
  for (const auto &record : service.txt_records) {
193
- ESP_LOGV(TAG, " TXT: %s = %s", MDNS_STR_ARG(record.key),
194
- const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
189
+ ESP_LOGV(TAG, " TXT: %s = %s", MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
195
190
  }
196
191
  }
197
192
  #endif
@@ -27,7 +27,7 @@ struct MDNSString;
27
27
 
28
28
  struct MDNSTXTRecord {
29
29
  const MDNSString *key;
30
- TemplatableValue<std::string> value;
30
+ const MDNSString *value;
31
31
  };
32
32
 
33
33
  struct MDNSService {
@@ -59,6 +59,17 @@ class MDNSComponent : public Component {
59
59
 
60
60
  void on_shutdown() override;
61
61
 
62
+ /// Add a dynamic TXT value and return pointer to it for use in MDNSTXTRecord
63
+ const char *add_dynamic_txt_value(const std::string &value) {
64
+ this->dynamic_txt_values_.push_back(value);
65
+ return this->dynamic_txt_values_[this->dynamic_txt_values_.size() - 1].c_str();
66
+ }
67
+
68
+ /// Storage for runtime-generated TXT values (MAC address, user lambdas)
69
+ /// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations.
70
+ /// Static/compile-time values (version, board, etc.) are stored directly in flash and don't use this.
71
+ StaticVector<std::string, MDNS_DYNAMIC_TXT_COUNT> dynamic_txt_values_;
72
+
62
73
  protected:
63
74
  StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{};
64
75
  std::string hostname_;