esphome 2025.2.2__py3-none-any.whl → 2025.3.0__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 (148) hide show
  1. esphome/__main__.py +9 -1
  2. esphome/components/api/api_connection.cpp +426 -70
  3. esphome/components/api/api_connection.h +117 -25
  4. esphome/components/api/api_pb2.cpp +33 -0
  5. esphome/components/api/api_pb2.h +4 -0
  6. esphome/components/api/api_server.cpp +2 -2
  7. esphome/components/api/list_entities.cpp +76 -22
  8. esphome/components/api/list_entities.h +1 -0
  9. esphome/components/api/subscribe_state.h +2 -0
  10. esphome/components/audio/__init__.py +1 -1
  11. esphome/components/audio/audio_decoder.cpp +43 -11
  12. esphome/components/audio/audio_reader.cpp +2 -2
  13. esphome/components/audio/audio_resampler.cpp +4 -2
  14. esphome/components/audio/audio_transfer_buffer.cpp +19 -9
  15. esphome/components/audio/audio_transfer_buffer.h +7 -2
  16. esphome/components/bluetooth_proxy/bluetooth_proxy.h +8 -0
  17. esphome/components/bmp085/bmp085.cpp +1 -1
  18. esphome/components/chsc6x/__init__.py +2 -0
  19. esphome/components/chsc6x/chsc6x_touchscreen.cpp +47 -0
  20. esphome/components/chsc6x/chsc6x_touchscreen.h +34 -0
  21. esphome/components/chsc6x/touchscreen.py +33 -0
  22. esphome/components/climate/__init__.py +0 -1
  23. esphome/components/cst816/binary_sensor/__init__.py +2 -25
  24. esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +3 -14
  25. esphome/components/cst816/touchscreen/cst816_touchscreen.h +0 -4
  26. esphome/components/esp32_ble_beacon/__init__.py +3 -1
  27. esphome/components/esp8266/gpio.py +1 -2
  28. esphome/components/font/__init__.py +198 -215
  29. esphome/components/font/font.cpp +4 -4
  30. esphome/components/font/font.h +1 -0
  31. esphome/components/graph/graph.cpp +4 -0
  32. esphome/components/graph/graph.h +4 -0
  33. esphome/components/haier/climate.py +11 -10
  34. esphome/components/hbridge/switch/hbridge_switch.cpp +2 -2
  35. esphome/components/heatpumpir/climate.py +2 -1
  36. esphome/components/heatpumpir/heatpumpir.cpp +1 -0
  37. esphome/components/heatpumpir/heatpumpir.h +1 -0
  38. esphome/components/i2c/__init__.py +6 -6
  39. esphome/components/i2c/i2c_bus_esp_idf.cpp +6 -2
  40. esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +1 -1
  41. esphome/components/ili9xxx/display.py +1 -0
  42. esphome/components/ili9xxx/ili9xxx_display.h +5 -0
  43. esphome/components/ili9xxx/ili9xxx_init.h +59 -0
  44. esphome/components/ld2450/__init__.py +51 -0
  45. esphome/components/ld2450/binary_sensor.py +47 -0
  46. esphome/components/ld2450/button/__init__.py +45 -0
  47. esphome/components/ld2450/button/reset_button.cpp +9 -0
  48. esphome/components/ld2450/button/reset_button.h +18 -0
  49. esphome/components/ld2450/button/restart_button.cpp +9 -0
  50. esphome/components/ld2450/button/restart_button.h +18 -0
  51. esphome/components/ld2450/ld2450.cpp +876 -0
  52. esphome/components/ld2450/ld2450.h +234 -0
  53. esphome/components/ld2450/number/__init__.py +121 -0
  54. esphome/components/ld2450/number/presence_timeout_number.cpp +12 -0
  55. esphome/components/ld2450/number/presence_timeout_number.h +18 -0
  56. esphome/components/ld2450/number/zone_coordinate_number.cpp +14 -0
  57. esphome/components/ld2450/number/zone_coordinate_number.h +19 -0
  58. esphome/components/ld2450/select/__init__.py +56 -0
  59. esphome/components/ld2450/select/baud_rate_select.cpp +12 -0
  60. esphome/components/ld2450/select/baud_rate_select.h +18 -0
  61. esphome/components/ld2450/select/zone_type_select.cpp +12 -0
  62. esphome/components/ld2450/select/zone_type_select.h +18 -0
  63. esphome/components/ld2450/sensor.py +156 -0
  64. esphome/components/ld2450/switch/__init__.py +45 -0
  65. esphome/components/ld2450/switch/bluetooth_switch.cpp +12 -0
  66. esphome/components/ld2450/switch/bluetooth_switch.h +18 -0
  67. esphome/components/ld2450/switch/multi_target_switch.cpp +12 -0
  68. esphome/components/ld2450/switch/multi_target_switch.h +18 -0
  69. esphome/components/ld2450/text_sensor.py +62 -0
  70. esphome/components/lvgl/defines.py +0 -2
  71. esphome/components/lvgl/font.cpp +1 -1
  72. esphome/components/lvgl/lvgl_esphome.cpp +27 -19
  73. esphome/components/lvgl/widgets/img.py +1 -3
  74. esphome/components/mcp2515/mcp2515.cpp +1 -0
  75. esphome/components/mdns/__init__.py +1 -1
  76. esphome/components/mixer/speaker/mixer_speaker.cpp +6 -1
  77. esphome/components/mixer/speaker/mixer_speaker.h +2 -0
  78. esphome/components/mlx90393/sensor.py +53 -33
  79. esphome/components/mlx90393/sensor_mlx90393.cpp +4 -0
  80. esphome/components/mlx90393/sensor_mlx90393.h +8 -3
  81. esphome/components/mqtt/__init__.py +2 -2
  82. esphome/components/msa3xx/__init__.py +189 -0
  83. esphome/components/msa3xx/binary_sensor.py +40 -0
  84. esphome/components/msa3xx/msa3xx.cpp +417 -0
  85. esphome/components/msa3xx/msa3xx.h +311 -0
  86. esphome/components/msa3xx/sensor.py +42 -0
  87. esphome/components/msa3xx/text_sensor.py +38 -0
  88. esphome/components/nfc/binary_sensor/__init__.py +4 -4
  89. esphome/components/opentherm/binary_sensor/__init__.py +4 -4
  90. esphome/components/opentherm/generate.py +6 -6
  91. esphome/components/opentherm/sensor/__init__.py +5 -6
  92. esphome/components/packages/__init__.py +35 -11
  93. esphome/components/pn532/binary_sensor.py +4 -4
  94. esphome/components/rc522/binary_sensor.py +4 -4
  95. esphome/components/resampler/speaker/resampler_speaker.h +2 -0
  96. esphome/components/socket/bsd_sockets_impl.cpp +1 -0
  97. esphome/components/socket/lwip_sockets_impl.cpp +1 -0
  98. esphome/components/socket/socket.h +3 -1
  99. esphome/components/speaker/speaker.h +2 -2
  100. esphome/components/ssd1306_base/__init__.py +7 -7
  101. esphome/components/thermostat/climate.py +1 -1
  102. esphome/components/tmp1075/tmp1075.cpp +7 -11
  103. esphome/components/tmp1075/tmp1075.h +1 -2
  104. esphome/components/tormatic/__init__.py +1 -0
  105. esphome/components/tormatic/cover.py +47 -0
  106. esphome/components/tormatic/tormatic_cover.cpp +355 -0
  107. esphome/components/tormatic/tormatic_cover.h +60 -0
  108. esphome/components/tormatic/tormatic_protocol.h +211 -0
  109. esphome/components/touchscreen/binary_sensor/__init__.py +3 -0
  110. esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp +7 -1
  111. esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h +3 -1
  112. esphome/components/touchscreen/touchscreen.cpp +3 -4
  113. esphome/components/udp/udp_component.h +4 -1
  114. esphome/components/web_server/list_entities.cpp +70 -66
  115. esphome/components/web_server/list_entities.h +43 -22
  116. esphome/components/web_server/web_server.cpp +345 -68
  117. esphome/components/web_server/web_server.h +138 -6
  118. esphome/components/web_server_base/__init__.py +1 -1
  119. esphome/components/web_server_idf/__init__.py +2 -0
  120. esphome/components/web_server_idf/web_server_idf.cpp +177 -30
  121. esphome/components/web_server_idf/web_server_idf.h +53 -4
  122. esphome/config_validation.py +23 -125
  123. esphome/const.py +5 -1
  124. esphome/core/config.py +15 -6
  125. esphome/core/defines.h +1 -1
  126. esphome/core/helpers.h +24 -3
  127. esphome/core/time.cpp +1 -0
  128. esphome/cpp_generator.py +3 -3
  129. esphome/dashboard/core.py +30 -21
  130. esphome/dashboard/dns.py +7 -1
  131. esphome/dashboard/entries.py +83 -16
  132. esphome/dashboard/settings.py +0 -4
  133. esphome/dashboard/status/mdns.py +43 -14
  134. esphome/dashboard/status/mqtt.py +22 -9
  135. esphome/dashboard/status/ping.py +54 -10
  136. esphome/dashboard/web_server.py +56 -24
  137. esphome/storage_json.py +4 -0
  138. esphome/wizard.py +13 -17
  139. esphome/writer.py +1 -3
  140. esphome/yaml_util.py +36 -33
  141. esphome/zeroconf.py +9 -21
  142. {esphome-2025.2.2.dist-info → esphome-2025.3.0.dist-info}/METADATA +7 -7
  143. {esphome-2025.2.2.dist-info → esphome-2025.3.0.dist-info}/RECORD +147 -107
  144. esphome/components/cst816/binary_sensor/cst816_button.h +0 -27
  145. {esphome-2025.2.2.dist-info → esphome-2025.3.0.dist-info}/LICENSE +0 -0
  146. {esphome-2025.2.2.dist-info → esphome-2025.3.0.dist-info}/WHEEL +0 -0
  147. {esphome-2025.2.2.dist-info → esphome-2025.3.0.dist-info}/entry_points.txt +0 -0
  148. {esphome-2025.2.2.dist-info → esphome-2025.3.0.dist-info}/top_level.txt +0 -0
@@ -4,12 +4,18 @@
4
4
  #include <esp_http_server.h>
5
5
 
6
6
  #include <functional>
7
+ #include <list>
7
8
  #include <map>
8
9
  #include <set>
9
10
  #include <string>
11
+ #include <utility>
10
12
  #include <vector>
11
13
 
12
14
  namespace esphome {
15
+ namespace web_server {
16
+ class WebServer;
17
+ class ListEntitiesIterator;
18
+ }; // namespace web_server
13
19
  namespace web_server_idf {
14
20
 
15
21
  #define F(string_literal) (string_literal)
@@ -215,19 +221,58 @@ class AsyncWebHandler {
215
221
  };
216
222
 
217
223
  class AsyncEventSource;
224
+ class AsyncEventSourceResponse;
225
+
226
+ using message_generator_t = std::string(esphome::web_server::WebServer *, void *);
227
+
228
+ /*
229
+ This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function
230
+ that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for
231
+ the same component are backed up, and take up only 8 bytes of memory. The entry in the deferred queue (a
232
+ std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per
233
+ entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing
234
+ because of dedup) would take up only 0.8 kB.
235
+ */
236
+ struct DeferredEvent {
237
+ friend class AsyncEventSourceResponse;
238
+
239
+ protected:
240
+ void *source_;
241
+ message_generator_t *message_generator_;
242
+
243
+ public:
244
+ DeferredEvent(void *source, message_generator_t *message_generator)
245
+ : source_(source), message_generator_(message_generator) {}
246
+ bool operator==(const DeferredEvent &test) const {
247
+ return (source_ == test.source_ && message_generator_ == test.message_generator_);
248
+ }
249
+ } __attribute__((packed));
218
250
 
219
251
  class AsyncEventSourceResponse {
220
252
  friend class AsyncEventSource;
221
253
 
222
254
  public:
223
- void send(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
255
+ bool try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
256
+ void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
257
+ void loop();
224
258
 
225
259
  protected:
226
- AsyncEventSourceResponse(const AsyncWebServerRequest *request, AsyncEventSource *server);
260
+ AsyncEventSourceResponse(const AsyncWebServerRequest *request, esphome::web_server_idf::AsyncEventSource *server,
261
+ esphome::web_server::WebServer *ws);
262
+
263
+ void deq_push_back_with_dedup_(void *source, message_generator_t *message_generator);
264
+ void process_deferred_queue_();
265
+ void process_buffer_();
266
+
227
267
  static void destroy(void *p);
228
268
  AsyncEventSource *server_;
229
269
  httpd_handle_t hd_{};
230
270
  int fd_{};
271
+ std::vector<DeferredEvent> deferred_queue_;
272
+ esphome::web_server::WebServer *web_server_;
273
+ std::unique_ptr<esphome::web_server::ListEntitiesIterator> entities_iterator_;
274
+ std::string event_buffer_{""};
275
+ size_t event_bytes_sent_;
231
276
  };
232
277
 
233
278
  using AsyncEventSourceClient = AsyncEventSourceResponse;
@@ -237,7 +282,7 @@ class AsyncEventSource : public AsyncWebHandler {
237
282
  using connect_handler_t = std::function<void(AsyncEventSourceClient *)>;
238
283
 
239
284
  public:
240
- AsyncEventSource(std::string url) : url_(std::move(url)) {}
285
+ AsyncEventSource(std::string url, esphome::web_server::WebServer *ws) : url_(std::move(url)), web_server_(ws) {}
241
286
  ~AsyncEventSource() override;
242
287
 
243
288
  // NOLINTNEXTLINE(readability-identifier-naming)
@@ -249,7 +294,10 @@ class AsyncEventSource : public AsyncWebHandler {
249
294
  // NOLINTNEXTLINE(readability-identifier-naming)
250
295
  void onConnect(connect_handler_t cb) { this->on_connect_ = std::move(cb); }
251
296
 
252
- void send(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
297
+ void try_send_nodefer(const char *message, const char *event = nullptr, uint32_t id = 0, uint32_t reconnect = 0);
298
+ void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator);
299
+ void loop();
300
+ bool empty() { return this->count() == 0; }
253
301
 
254
302
  size_t count() const { return this->sessions_.size(); }
255
303
 
@@ -257,6 +305,7 @@ class AsyncEventSource : public AsyncWebHandler {
257
305
  std::string url_;
258
306
  std::set<AsyncEventSourceResponse *> sessions_;
259
307
  connect_handler_t on_connect_{};
308
+ esphome::web_server::WebServer *web_server_;
260
309
  };
261
310
 
262
311
  class DefaultHeaders {
@@ -1223,8 +1223,7 @@ def subscribe_topic(value):
1223
1223
  if index != len(value) - 1:
1224
1224
  # If there are multiple wildcards, this will also trigger
1225
1225
  raise Invalid(
1226
- "Multi-level wildcard must be the last "
1227
- "character in the topic filter."
1226
+ "Multi-level wildcard must be the last character in the topic filter."
1228
1227
  )
1229
1228
  if len(value) > 1 and value[index - 1] != "/":
1230
1229
  raise Invalid("Multi-level wildcard must be after a topic level separator.")
@@ -1642,140 +1641,39 @@ class GenerateID(Optional):
1642
1641
  super().__init__(key, default=lambda: None)
1643
1642
 
1644
1643
 
1645
- def _get_priority_default(*args):
1646
- for arg in args:
1647
- if arg is not vol.UNDEFINED:
1648
- return arg
1649
- return vol.UNDEFINED
1644
+ def _get_default_key(*args):
1645
+ return ["_".join([CORE.target_platform] + list(args))]
1650
1646
 
1651
1647
 
1652
1648
  class SplitDefault(Optional):
1653
1649
  """Mark this key to have a split default for ESP8266/ESP32."""
1654
1650
 
1655
- def __init__(
1656
- self,
1657
- key,
1658
- esp8266=vol.UNDEFINED,
1659
- esp32=vol.UNDEFINED,
1660
- esp32_arduino=vol.UNDEFINED,
1661
- esp32_idf=vol.UNDEFINED,
1662
- esp32_s2=vol.UNDEFINED,
1663
- esp32_s2_arduino=vol.UNDEFINED,
1664
- esp32_s2_idf=vol.UNDEFINED,
1665
- esp32_s3=vol.UNDEFINED,
1666
- esp32_s3_arduino=vol.UNDEFINED,
1667
- esp32_s3_idf=vol.UNDEFINED,
1668
- esp32_c3=vol.UNDEFINED,
1669
- esp32_c3_arduino=vol.UNDEFINED,
1670
- esp32_c3_idf=vol.UNDEFINED,
1671
- esp32_c6=vol.UNDEFINED,
1672
- esp32_c6_arduino=vol.UNDEFINED,
1673
- esp32_c6_idf=vol.UNDEFINED,
1674
- esp32_h2=vol.UNDEFINED,
1675
- esp32_h2_arduino=vol.UNDEFINED,
1676
- esp32_h2_idf=vol.UNDEFINED,
1677
- rp2040=vol.UNDEFINED,
1678
- bk72xx=vol.UNDEFINED,
1679
- rtl87xx=vol.UNDEFINED,
1680
- host=vol.UNDEFINED,
1681
- ):
1651
+ def __init__(self, key, **kwargs):
1682
1652
  super().__init__(key)
1683
- self._esp8266_default = vol.default_factory(esp8266)
1684
- self._esp32_arduino_default = vol.default_factory(
1685
- _get_priority_default(esp32_arduino, esp32)
1686
- )
1687
- self._esp32_idf_default = vol.default_factory(
1688
- _get_priority_default(esp32_idf, esp32)
1689
- )
1690
- self._esp32_s2_arduino_default = vol.default_factory(
1691
- _get_priority_default(esp32_s2_arduino, esp32_s2, esp32_arduino, esp32)
1692
- )
1693
- self._esp32_s2_idf_default = vol.default_factory(
1694
- _get_priority_default(esp32_s2_idf, esp32_s2, esp32_idf, esp32)
1695
- )
1696
- self._esp32_s3_arduino_default = vol.default_factory(
1697
- _get_priority_default(esp32_s3_arduino, esp32_s3, esp32_arduino, esp32)
1698
- )
1699
- self._esp32_s3_idf_default = vol.default_factory(
1700
- _get_priority_default(esp32_s3_idf, esp32_s3, esp32_idf, esp32)
1701
- )
1702
- self._esp32_c3_arduino_default = vol.default_factory(
1703
- _get_priority_default(esp32_c3_arduino, esp32_c3, esp32_arduino, esp32)
1704
- )
1705
- self._esp32_c3_idf_default = vol.default_factory(
1706
- _get_priority_default(esp32_c3_idf, esp32_c3, esp32_idf, esp32)
1707
- )
1708
- self._esp32_c6_arduino_default = vol.default_factory(
1709
- _get_priority_default(esp32_c6_arduino, esp32_c6, esp32_arduino, esp32)
1710
- )
1711
- self._esp32_c6_idf_default = vol.default_factory(
1712
- _get_priority_default(esp32_c6_idf, esp32_c6, esp32_idf, esp32)
1713
- )
1714
- self._esp32_h2_arduino_default = vol.default_factory(
1715
- _get_priority_default(esp32_h2_arduino, esp32_h2, esp32_arduino, esp32)
1716
- )
1717
- self._esp32_h2_idf_default = vol.default_factory(
1718
- _get_priority_default(esp32_h2_idf, esp32_h2, esp32_idf, esp32)
1719
- )
1720
- self._rp2040_default = vol.default_factory(rp2040)
1721
- self._bk72xx_default = vol.default_factory(bk72xx)
1722
- self._rtl87xx_default = vol.default_factory(rtl87xx)
1723
- self._host_default = vol.default_factory(host)
1653
+
1654
+ self._defaults = {}
1655
+
1656
+ for platform_key, value in kwargs.items():
1657
+ self._defaults[platform_key] = vol.default_factory(value)
1724
1658
 
1725
1659
  @property
1726
1660
  def default(self):
1727
- if CORE.is_esp8266:
1728
- return self._esp8266_default
1661
+ keys = []
1729
1662
  if CORE.is_esp32:
1730
1663
  from esphome.components.esp32 import get_esp32_variant
1731
- from esphome.components.esp32.const import (
1732
- VARIANT_ESP32C3,
1733
- VARIANT_ESP32C6,
1734
- VARIANT_ESP32H2,
1735
- VARIANT_ESP32S2,
1736
- VARIANT_ESP32S3,
1737
- )
1738
-
1739
- variant = get_esp32_variant()
1740
- if variant == VARIANT_ESP32S2:
1741
- if CORE.using_arduino:
1742
- return self._esp32_s2_arduino_default
1743
- if CORE.using_esp_idf:
1744
- return self._esp32_s2_idf_default
1745
- elif variant == VARIANT_ESP32S3:
1746
- if CORE.using_arduino:
1747
- return self._esp32_s3_arduino_default
1748
- if CORE.using_esp_idf:
1749
- return self._esp32_s3_idf_default
1750
- elif variant == VARIANT_ESP32C3:
1751
- if CORE.using_arduino:
1752
- return self._esp32_c3_arduino_default
1753
- if CORE.using_esp_idf:
1754
- return self._esp32_c3_idf_default
1755
- elif variant == VARIANT_ESP32C6:
1756
- if CORE.using_arduino:
1757
- return self._esp32_c6_arduino_default
1758
- if CORE.using_esp_idf:
1759
- return self._esp32_c6_idf_default
1760
- elif variant == VARIANT_ESP32H2:
1761
- if CORE.using_arduino:
1762
- return self._esp32_h2_arduino_default
1763
- if CORE.using_esp_idf:
1764
- return self._esp32_h2_idf_default
1765
- else:
1766
- if CORE.using_arduino:
1767
- return self._esp32_arduino_default
1768
- if CORE.using_esp_idf:
1769
- return self._esp32_idf_default
1770
- if CORE.is_rp2040:
1771
- return self._rp2040_default
1772
- if CORE.is_bk72xx:
1773
- return self._bk72xx_default
1774
- if CORE.is_rtl87xx:
1775
- return self._rtl87xx_default
1776
- if CORE.is_host:
1777
- return self._host_default
1778
- raise NotImplementedError
1664
+ from esphome.components.esp32.const import VARIANT_ESP32
1665
+
1666
+ variant = get_esp32_variant().replace(VARIANT_ESP32, "").lower()
1667
+ framework = CORE.target_framework.replace("esp-", "")
1668
+ if variant:
1669
+ keys += _get_default_key(variant, framework)
1670
+ keys += _get_default_key(variant)
1671
+ keys += _get_default_key(framework)
1672
+ keys += _get_default_key()
1673
+ for key in keys:
1674
+ if self._defaults.get(key) is not None:
1675
+ return self._defaults[key]
1676
+ return vol.default_factory(vol.UNDEFINED)
1779
1677
 
1780
1678
  @default.setter
1781
1679
  def default(self, value):
esphome/const.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Constants used by esphome."""
2
2
 
3
- __version__ = "2025.2.2"
3
+ __version__ = "2025.3.0"
4
4
 
5
5
  ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
6
6
  VALID_SUBSTITUTIONS_CHARACTERS = (
@@ -546,6 +546,9 @@ CONF_OFF_SPEED_CYCLE = "off_speed_cycle"
546
546
  CONF_OFFSET = "offset"
547
547
  CONF_OFFSET_HEIGHT = "offset_height"
548
548
  CONF_OFFSET_WIDTH = "offset_width"
549
+ CONF_OFFSET_X = "offset_x"
550
+ CONF_OFFSET_Y = "offset_y"
551
+ CONF_OFFSET_Z = "offset_z"
549
552
  CONF_ON = "on"
550
553
  CONF_ON_BLE_ADVERTISE = "on_ble_advertise"
551
554
  CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE = "on_ble_manufacturer_data_advertise"
@@ -927,6 +930,7 @@ CONF_VALUE = "value"
927
930
  CONF_VALUE_FONT = "value_font"
928
931
  CONF_VARIABLES = "variables"
929
932
  CONF_VARIANT = "variant"
933
+ CONF_VARS = "vars"
930
934
  CONF_VERSION = "version"
931
935
  CONF_VIBRATIONS = "vibrations"
932
936
  CONF_VISIBLE = "visible"
esphome/core/config.py CHANGED
@@ -1,5 +1,4 @@
1
1
  import logging
2
- import multiprocessing
3
2
  import os
4
3
  from pathlib import Path
5
4
 
@@ -94,10 +93,19 @@ def valid_project_name(value: str):
94
93
  return value
95
94
 
96
95
 
96
+ def get_usable_cpu_count() -> int:
97
+ """Return the number of CPUs that can be used for processes.
98
+ On Python 3.13+ this is the number of CPUs that can be used for processes.
99
+ On older Python versions this is the number of CPUs.
100
+ """
101
+ return (
102
+ os.process_cpu_count() if hasattr(os, "process_cpu_count") else os.cpu_count()
103
+ )
104
+
105
+
97
106
  if "ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT" in os.environ:
98
107
  _compile_process_limit_default = min(
99
- int(os.environ["ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT"]),
100
- multiprocessing.cpu_count(),
108
+ int(os.environ["ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT"]), get_usable_cpu_count()
101
109
  )
102
110
  else:
103
111
  _compile_process_limit_default = cv.UNDEFINED
@@ -156,7 +164,7 @@ CONFIG_SCHEMA = cv.All(
156
164
  ),
157
165
  cv.Optional(
158
166
  CONF_COMPILE_PROCESS_LIMIT, default=_compile_process_limit_default
159
- ): cv.int_range(min=1, max=multiprocessing.cpu_count()),
167
+ ): cv.int_range(min=1, max=get_usable_cpu_count()),
160
168
  }
161
169
  ),
162
170
  validate_hostname,
@@ -181,10 +189,11 @@ def _is_target_platform(name):
181
189
  from esphome.loader import get_component
182
190
 
183
191
  try:
184
- if get_component(name, True).is_target_platform:
185
- return True
192
+ return get_component(name, True).is_target_platform
186
193
  except KeyError:
187
194
  pass
195
+ except ImportError:
196
+ pass
188
197
  return False
189
198
 
190
199
 
esphome/core/defines.h CHANGED
@@ -71,7 +71,7 @@
71
71
  #define USE_OTA
72
72
  #define USE_OTA_PASSWORD
73
73
  #define USE_OTA_STATE_CALLBACK
74
- #define USE_OTA_VERSION 1
74
+ #define USE_OTA_VERSION 2
75
75
  #define USE_OUTPUT
76
76
  #define USE_POWER_SUPPLY
77
77
  #define USE_QR_CODE
esphome/core/helpers.h CHANGED
@@ -3,11 +3,11 @@
3
3
  #include <cmath>
4
4
  #include <cstring>
5
5
  #include <functional>
6
+ #include <limits>
6
7
  #include <memory>
7
8
  #include <string>
8
9
  #include <type_traits>
9
10
  #include <vector>
10
- #include <limits>
11
11
 
12
12
  #include "esphome/core/optional.h"
13
13
 
@@ -700,8 +700,10 @@ template<class T> class RAMAllocator {
700
700
  }
701
701
  template<class U> constexpr RAMAllocator(const RAMAllocator<U> &other) : flags_{other.flags_} {}
702
702
 
703
- T *allocate(size_t n) {
704
- size_t size = n * sizeof(T);
703
+ T *allocate(size_t n) { return this->allocate(n, sizeof(T)); }
704
+
705
+ T *allocate(size_t n, size_t manual_size) {
706
+ size_t size = n * manual_size;
705
707
  T *ptr = nullptr;
706
708
  #ifdef USE_ESP32
707
709
  if (this->flags_ & Flags::ALLOC_EXTERNAL) {
@@ -717,6 +719,25 @@ template<class T> class RAMAllocator {
717
719
  return ptr;
718
720
  }
719
721
 
722
+ T *reallocate(T *p, size_t n) { return this->reallocate(p, n, sizeof(T)); }
723
+
724
+ T *reallocate(T *p, size_t n, size_t manual_size) {
725
+ size_t size = n * sizeof(T);
726
+ T *ptr = nullptr;
727
+ #ifdef USE_ESP32
728
+ if (this->flags_ & Flags::ALLOC_EXTERNAL) {
729
+ ptr = static_cast<T *>(heap_caps_realloc(p, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
730
+ }
731
+ if (ptr == nullptr && this->flags_ & Flags::ALLOC_INTERNAL) {
732
+ ptr = static_cast<T *>(heap_caps_realloc(p, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
733
+ }
734
+ #else
735
+ // Ignore ALLOC_EXTERNAL/ALLOC_INTERNAL flags if external allocation is not supported
736
+ ptr = static_cast<T *>(realloc(p, size)); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc)
737
+ #endif
738
+ return ptr;
739
+ }
740
+
720
741
  void deallocate(T *p, size_t n) {
721
742
  free(p); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc)
722
743
  }
esphome/core/time.cpp CHANGED
@@ -197,6 +197,7 @@ void ESPTime::recalc_timestamp_local() {
197
197
  tm.tm_hour = this->hour;
198
198
  tm.tm_min = this->minute;
199
199
  tm.tm_sec = this->second;
200
+ tm.tm_isdst = -1;
200
201
 
201
202
  this->timestamp = mktime(&tm);
202
203
  }
esphome/cpp_generator.py CHANGED
@@ -506,9 +506,9 @@ def with_local_variable(id_: ID, rhs: SafeExpType, callback: Callable, *args) ->
506
506
  """
507
507
 
508
508
  # throw if the callback is async:
509
- assert not inspect.iscoroutinefunction(
510
- callback
511
- ), "with_local_variable() callback cannot be async!"
509
+ assert not inspect.iscoroutinefunction(callback), (
510
+ "with_local_variable() callback cannot be async!"
511
+ )
512
512
 
513
513
  CORE.add(RawStatement("{")) # output opening curly brace
514
514
  obj = variable(id_, rhs, None, True)
esphome/dashboard/core.py CHANGED
@@ -9,7 +9,7 @@ import json
9
9
  import logging
10
10
  from pathlib import Path
11
11
  import threading
12
- from typing import TYPE_CHECKING, Any, Callable
12
+ from typing import Any, Callable
13
13
 
14
14
  from esphome.storage_json import ignored_devices_storage_path
15
15
 
@@ -17,15 +17,15 @@ from ..zeroconf import DiscoveredImport
17
17
  from .dns import DNSCache
18
18
  from .entries import DashboardEntries
19
19
  from .settings import DashboardSettings
20
-
21
- if TYPE_CHECKING:
22
- from .status.mdns import MDNSStatus
23
-
20
+ from .status.mdns import MDNSStatus
21
+ from .status.ping import PingStatus
24
22
 
25
23
  _LOGGER = logging.getLogger(__name__)
26
24
 
27
25
  IGNORED_DEVICES_STORAGE_PATH = "ignored-devices.json"
28
26
 
27
+ MDNS_BOOTSTRAP_TIME = 7.5
28
+
29
29
 
30
30
  @dataclass
31
31
  class Event:
@@ -81,6 +81,7 @@ class ESPHomeDashboard:
81
81
  "dns_cache",
82
82
  "_background_tasks",
83
83
  "ignored_devices",
84
+ "_ping_status_task",
84
85
  )
85
86
 
86
87
  def __init__(self) -> None:
@@ -97,6 +98,7 @@ class ESPHomeDashboard:
97
98
  self.dns_cache = DNSCache()
98
99
  self._background_tasks: set[asyncio.Task] = set()
99
100
  self.ignored_devices: set[str] = set()
101
+ self._ping_status_task: asyncio.Task | None = None
100
102
 
101
103
  async def async_setup(self) -> None:
102
104
  """Setup the dashboard."""
@@ -121,41 +123,48 @@ class ESPHomeDashboard:
121
123
  {"ignored_devices": sorted(self.ignored_devices)}, indent=2, fp=f_handle
122
124
  )
123
125
 
126
+ def _async_start_ping_status(self, ping_status: PingStatus) -> None:
127
+ self._ping_status_task = asyncio.create_task(ping_status.async_run())
128
+
124
129
  async def async_run(self) -> None:
125
130
  """Run the dashboard."""
126
131
  settings = self.settings
127
132
  mdns_task: asyncio.Task | None = None
128
- ping_status_task: asyncio.Task | None = None
129
133
  await self.entries.async_update_entries()
130
134
 
131
- if settings.status_use_ping:
132
- from .status.ping import PingStatus
135
+ mdns_status = MDNSStatus(self)
136
+ ping_status = PingStatus(self)
137
+ start_ping_timer: asyncio.TimerHandle | None = None
133
138
 
134
- ping_status = PingStatus()
135
- ping_status_task = asyncio.create_task(ping_status.async_run())
136
- else:
137
- from .status.mdns import MDNSStatus
138
-
139
- mdns_status = MDNSStatus()
140
- await mdns_status.async_refresh_hosts()
141
- self.mdns_status = mdns_status
139
+ self.mdns_status = mdns_status
140
+ if mdns_status.async_setup():
142
141
  mdns_task = asyncio.create_task(mdns_status.async_run())
142
+ # Start ping MDNS_BOOTSTRAP_TIME seconds after startup to ensure
143
+ # MDNS has had a chance to resolve the devices
144
+ start_ping_timer = self.loop.call_later(
145
+ MDNS_BOOTSTRAP_TIME, self._async_start_ping_status, ping_status
146
+ )
147
+ else:
148
+ # If mDNS is not available, start the ping status immediately
149
+ self._async_start_ping_status(ping_status)
143
150
 
144
151
  if settings.status_use_mqtt:
145
152
  from .status.mqtt import MqttStatusThread
146
153
 
147
- status_thread_mqtt = MqttStatusThread()
154
+ status_thread_mqtt = MqttStatusThread(self)
148
155
  status_thread_mqtt.start()
149
156
 
150
- shutdown_event = asyncio.Event()
151
157
  try:
152
- await shutdown_event.wait()
158
+ await asyncio.Event().wait()
153
159
  finally:
154
160
  _LOGGER.info("Shutting down...")
155
161
  self.stop_event.set()
156
162
  self.ping_request.set()
157
- if ping_status_task:
158
- ping_status_task.cancel()
163
+ if start_ping_timer:
164
+ start_ping_timer.cancel()
165
+ if self._ping_status_task:
166
+ self._ping_status_task.cancel()
167
+ self._ping_status_task = None
159
168
  if mdns_task:
160
169
  mdns_task.cancel()
161
170
  if settings.status_use_mqtt:
esphome/dashboard/dns.py CHANGED
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ from contextlib import suppress
5
+ from ipaddress import ip_address
4
6
  import sys
5
7
 
6
8
  from icmplib import NameLookupError, async_resolve
@@ -10,11 +12,15 @@ if sys.version_info >= (3, 11):
10
12
  else:
11
13
  from async_timeout import timeout as async_timeout
12
14
 
15
+ RESOLVE_TIMEOUT = 3.0
16
+
13
17
 
14
18
  async def _async_resolve_wrapper(hostname: str) -> list[str] | Exception:
15
19
  """Wrap the icmplib async_resolve function."""
20
+ with suppress(ValueError):
21
+ return [str(ip_address(hostname))]
16
22
  try:
17
- async with async_timeout(2):
23
+ async with async_timeout(RESOLVE_TIMEOUT):
18
24
  return await async_resolve(hostname)
19
25
  except (asyncio.TimeoutError, NameLookupError, UnicodeError) as ex:
20
26
  return ex