esphome 2025.2.2__py3-none-any.whl → 2025.3.0b1__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.
- esphome/__main__.py +9 -1
- esphome/components/api/api_connection.cpp +426 -70
- esphome/components/api/api_connection.h +117 -25
- esphome/components/api/api_pb2.cpp +9 -0
- esphome/components/api/api_pb2.h +1 -0
- esphome/components/api/api_server.cpp +2 -2
- esphome/components/api/list_entities.cpp +76 -22
- esphome/components/api/list_entities.h +1 -0
- esphome/components/api/subscribe_state.h +2 -0
- esphome/components/bluetooth_proxy/bluetooth_proxy.h +8 -0
- esphome/components/bmp085/bmp085.cpp +1 -1
- esphome/components/chsc6x/__init__.py +2 -0
- esphome/components/chsc6x/chsc6x_touchscreen.cpp +47 -0
- esphome/components/chsc6x/chsc6x_touchscreen.h +34 -0
- esphome/components/chsc6x/touchscreen.py +33 -0
- esphome/components/climate/__init__.py +0 -1
- esphome/components/cst816/binary_sensor/__init__.py +2 -25
- esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +3 -14
- esphome/components/cst816/touchscreen/cst816_touchscreen.h +0 -4
- esphome/components/esp32_ble_beacon/__init__.py +3 -1
- esphome/components/esp8266/gpio.py +1 -2
- esphome/components/font/__init__.py +185 -185
- esphome/components/font/font.cpp +4 -4
- esphome/components/font/font.h +1 -0
- esphome/components/haier/climate.py +11 -10
- esphome/components/hbridge/switch/hbridge_switch.cpp +2 -2
- esphome/components/heatpumpir/climate.py +2 -1
- esphome/components/heatpumpir/heatpumpir.cpp +1 -0
- esphome/components/heatpumpir/heatpumpir.h +1 -0
- esphome/components/i2c/__init__.py +6 -6
- esphome/components/i2c/i2c_bus_esp_idf.cpp +6 -2
- esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +1 -1
- esphome/components/ili9xxx/display.py +1 -0
- esphome/components/ili9xxx/ili9xxx_display.h +5 -0
- esphome/components/ili9xxx/ili9xxx_init.h +59 -0
- esphome/components/ld2450/__init__.py +51 -0
- esphome/components/ld2450/binary_sensor.py +47 -0
- esphome/components/ld2450/button/__init__.py +45 -0
- esphome/components/ld2450/button/reset_button.cpp +9 -0
- esphome/components/ld2450/button/reset_button.h +18 -0
- esphome/components/ld2450/button/restart_button.cpp +9 -0
- esphome/components/ld2450/button/restart_button.h +18 -0
- esphome/components/ld2450/ld2450.cpp +876 -0
- esphome/components/ld2450/ld2450.h +234 -0
- esphome/components/ld2450/number/__init__.py +121 -0
- esphome/components/ld2450/number/presence_timeout_number.cpp +12 -0
- esphome/components/ld2450/number/presence_timeout_number.h +18 -0
- esphome/components/ld2450/number/zone_coordinate_number.cpp +14 -0
- esphome/components/ld2450/number/zone_coordinate_number.h +19 -0
- esphome/components/ld2450/select/__init__.py +56 -0
- esphome/components/ld2450/select/baud_rate_select.cpp +12 -0
- esphome/components/ld2450/select/baud_rate_select.h +18 -0
- esphome/components/ld2450/select/zone_type_select.cpp +12 -0
- esphome/components/ld2450/select/zone_type_select.h +18 -0
- esphome/components/ld2450/sensor.py +156 -0
- esphome/components/ld2450/switch/__init__.py +45 -0
- esphome/components/ld2450/switch/bluetooth_switch.cpp +12 -0
- esphome/components/ld2450/switch/bluetooth_switch.h +18 -0
- esphome/components/ld2450/switch/multi_target_switch.cpp +12 -0
- esphome/components/ld2450/switch/multi_target_switch.h +18 -0
- esphome/components/ld2450/text_sensor.py +62 -0
- esphome/components/lvgl/defines.py +0 -2
- esphome/components/lvgl/font.cpp +1 -1
- esphome/components/lvgl/lvgl_esphome.cpp +27 -19
- esphome/components/lvgl/widgets/img.py +1 -3
- esphome/components/mcp2515/mcp2515.cpp +1 -0
- esphome/components/mlx90393/sensor.py +53 -33
- esphome/components/mlx90393/sensor_mlx90393.cpp +4 -0
- esphome/components/mlx90393/sensor_mlx90393.h +8 -3
- esphome/components/mqtt/__init__.py +2 -2
- esphome/components/msa3xx/__init__.py +189 -0
- esphome/components/msa3xx/binary_sensor.py +40 -0
- esphome/components/msa3xx/msa3xx.cpp +417 -0
- esphome/components/msa3xx/msa3xx.h +311 -0
- esphome/components/msa3xx/sensor.py +42 -0
- esphome/components/msa3xx/text_sensor.py +38 -0
- esphome/components/nfc/binary_sensor/__init__.py +4 -4
- esphome/components/opentherm/binary_sensor/__init__.py +4 -4
- esphome/components/opentherm/generate.py +6 -6
- esphome/components/opentherm/sensor/__init__.py +5 -6
- esphome/components/packages/__init__.py +35 -11
- esphome/components/pn532/binary_sensor.py +4 -4
- esphome/components/rc522/binary_sensor.py +4 -4
- esphome/components/socket/bsd_sockets_impl.cpp +1 -0
- esphome/components/socket/lwip_sockets_impl.cpp +1 -0
- esphome/components/socket/socket.h +3 -1
- esphome/components/ssd1306_base/__init__.py +7 -7
- esphome/components/thermostat/climate.py +1 -1
- esphome/components/tmp1075/tmp1075.cpp +7 -11
- esphome/components/tmp1075/tmp1075.h +1 -2
- esphome/components/tormatic/__init__.py +1 -0
- esphome/components/tormatic/cover.py +47 -0
- esphome/components/tormatic/tormatic_cover.cpp +355 -0
- esphome/components/tormatic/tormatic_cover.h +60 -0
- esphome/components/tormatic/tormatic_protocol.h +211 -0
- esphome/components/touchscreen/binary_sensor/__init__.py +3 -0
- esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp +7 -1
- esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h +3 -1
- esphome/components/touchscreen/touchscreen.cpp +3 -4
- esphome/components/udp/udp_component.h +4 -1
- esphome/components/web_server/list_entities.cpp +70 -66
- esphome/components/web_server/list_entities.h +43 -22
- esphome/components/web_server/web_server.cpp +345 -68
- esphome/components/web_server/web_server.h +138 -6
- esphome/components/web_server_base/__init__.py +1 -1
- esphome/components/web_server_idf/__init__.py +2 -0
- esphome/components/web_server_idf/web_server_idf.cpp +177 -30
- esphome/components/web_server_idf/web_server_idf.h +53 -4
- esphome/config_validation.py +23 -125
- esphome/const.py +5 -1
- esphome/core/config.py +12 -4
- esphome/core/defines.h +1 -1
- esphome/core/helpers.h +5 -3
- esphome/core/time.cpp +1 -0
- esphome/cpp_generator.py +3 -3
- esphome/dashboard/core.py +30 -21
- esphome/dashboard/dns.py +7 -1
- esphome/dashboard/entries.py +83 -16
- esphome/dashboard/settings.py +0 -4
- esphome/dashboard/status/mdns.py +43 -14
- esphome/dashboard/status/mqtt.py +22 -9
- esphome/dashboard/status/ping.py +54 -10
- esphome/dashboard/web_server.py +56 -24
- esphome/storage_json.py +4 -0
- esphome/wizard.py +13 -17
- esphome/writer.py +1 -3
- esphome/yaml_util.py +36 -33
- esphome/zeroconf.py +9 -21
- {esphome-2025.2.2.dist-info → esphome-2025.3.0b1.dist-info}/METADATA +5 -5
- {esphome-2025.2.2.dist-info → esphome-2025.3.0b1.dist-info}/RECORD +134 -94
- esphome/components/cst816/binary_sensor/cst816_button.h +0 -27
- {esphome-2025.2.2.dist-info → esphome-2025.3.0b1.dist-info}/LICENSE +0 -0
- {esphome-2025.2.2.dist-info → esphome-2025.3.0b1.dist-info}/WHEEL +0 -0
- {esphome-2025.2.2.dist-info → esphome-2025.3.0b1.dist-info}/entry_points.txt +0 -0
- {esphome-2025.2.2.dist-info → esphome-2025.3.0b1.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
|
-
|
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
|
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 {
|
esphome/config_validation.py
CHANGED
@@ -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
|
1646
|
-
|
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
|
-
|
1684
|
-
self.
|
1685
|
-
|
1686
|
-
)
|
1687
|
-
|
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
|
-
|
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
|
-
|
1733
|
-
|
1734
|
-
|
1735
|
-
|
1736
|
-
|
1737
|
-
|
1738
|
-
|
1739
|
-
|
1740
|
-
|
1741
|
-
|
1742
|
-
|
1743
|
-
|
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.
|
3
|
+
__version__ = "2025.3.0b1"
|
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=
|
167
|
+
): cv.int_range(min=1, max=get_usable_cpu_count()),
|
160
168
|
}
|
161
169
|
),
|
162
170
|
validate_hostname,
|
esphome/core/defines.h
CHANGED
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
|
-
|
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) {
|
esphome/core/time.cpp
CHANGED
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
|
-
)
|
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
|
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
|
-
|
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
|
-
|
132
|
-
|
135
|
+
mdns_status = MDNSStatus(self)
|
136
|
+
ping_status = PingStatus(self)
|
137
|
+
start_ping_timer: asyncio.TimerHandle | None = None
|
133
138
|
|
134
|
-
|
135
|
-
|
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
|
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
|
158
|
-
|
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(
|
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
|
esphome/dashboard/entries.py
CHANGED
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
import asyncio
|
4
4
|
from collections import defaultdict
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from functools import lru_cache
|
5
7
|
import logging
|
6
8
|
import os
|
7
9
|
from typing import TYPE_CHECKING, Any
|
@@ -27,37 +29,53 @@ _LOGGER = logging.getLogger(__name__)
|
|
27
29
|
|
28
30
|
DashboardCacheKeyType = tuple[int, int, float, int]
|
29
31
|
|
30
|
-
# Currently EntryState is a simple
|
31
|
-
# online/offline/unknown enum, but in the future
|
32
|
-
# it may be expanded to include more states
|
33
32
|
|
33
|
+
@dataclass(frozen=True)
|
34
|
+
class EntryState:
|
35
|
+
"""Represents the state of an entry."""
|
34
36
|
|
35
|
-
|
37
|
+
reachable: ReachableState
|
38
|
+
source: EntryStateSource
|
39
|
+
|
40
|
+
|
41
|
+
class EntryStateSource(StrEnum):
|
42
|
+
MDNS = "mdns"
|
43
|
+
PING = "ping"
|
44
|
+
MQTT = "mqtt"
|
45
|
+
UNKNOWN = "unknown"
|
46
|
+
|
47
|
+
|
48
|
+
class ReachableState(StrEnum):
|
36
49
|
ONLINE = "online"
|
37
50
|
OFFLINE = "offline"
|
51
|
+
DNS_FAILURE = "dns_failure"
|
38
52
|
UNKNOWN = "unknown"
|
39
53
|
|
40
54
|
|
41
|
-
|
42
|
-
True:
|
43
|
-
False:
|
44
|
-
None:
|
55
|
+
_BOOL_TO_REACHABLE_STATE = {
|
56
|
+
True: ReachableState.ONLINE,
|
57
|
+
False: ReachableState.OFFLINE,
|
58
|
+
None: ReachableState.UNKNOWN,
|
45
59
|
}
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
60
|
+
_REACHABLE_STATE_TO_BOOL = {
|
61
|
+
ReachableState.ONLINE: True,
|
62
|
+
ReachableState.OFFLINE: False,
|
63
|
+
ReachableState.DNS_FAILURE: False,
|
64
|
+
ReachableState.UNKNOWN: None,
|
50
65
|
}
|
51
66
|
|
67
|
+
UNKNOWN_STATE = EntryState(ReachableState.UNKNOWN, EntryStateSource.UNKNOWN)
|
52
68
|
|
53
|
-
|
69
|
+
|
70
|
+
@lru_cache # creating frozen dataclass instances is expensive, so we cache them
|
71
|
+
def bool_to_entry_state(value: bool | None, source: EntryStateSource) -> EntryState:
|
54
72
|
"""Convert a bool to an entry state."""
|
55
|
-
return
|
73
|
+
return EntryState(_BOOL_TO_REACHABLE_STATE[value], source)
|
56
74
|
|
57
75
|
|
58
76
|
def entry_state_to_bool(value: EntryState) -> bool | None:
|
59
77
|
"""Convert an entry state to a bool."""
|
60
|
-
return
|
78
|
+
return _REACHABLE_STATE_TO_BOOL[value.reachable]
|
61
79
|
|
62
80
|
|
63
81
|
class DashboardEntries:
|
@@ -119,6 +137,55 @@ class DashboardEntries:
|
|
119
137
|
"""Set the state for an entry."""
|
120
138
|
self.async_set_state(entry, state)
|
121
139
|
|
140
|
+
def set_state_if_online_or_source(
|
141
|
+
self, entry: DashboardEntry, state: EntryState
|
142
|
+
) -> None:
|
143
|
+
"""Set the state for an entry if its online or provided by the source or unknown."""
|
144
|
+
asyncio.run_coroutine_threadsafe(
|
145
|
+
self._async_set_state_if_online_or_source(entry, state), self._loop
|
146
|
+
).result()
|
147
|
+
|
148
|
+
async def _async_set_state_if_online_or_source(
|
149
|
+
self, entry: DashboardEntry, state: EntryState
|
150
|
+
) -> None:
|
151
|
+
"""Set the state for an entry if its online or provided by the source or unknown."""
|
152
|
+
self.async_set_state_if_online_or_source(entry, state)
|
153
|
+
|
154
|
+
def async_set_state_if_online_or_source(
|
155
|
+
self, entry: DashboardEntry, state: EntryState
|
156
|
+
) -> None:
|
157
|
+
"""Set the state for an entry if its online or provided by the source or unknown."""
|
158
|
+
if (
|
159
|
+
state.reachable is ReachableState.ONLINE
|
160
|
+
and entry.state.reachable is not ReachableState.ONLINE
|
161
|
+
) or entry.state.source in (
|
162
|
+
EntryStateSource.UNKNOWN,
|
163
|
+
state.source,
|
164
|
+
):
|
165
|
+
self.async_set_state(entry, state)
|
166
|
+
|
167
|
+
def set_state_if_source(self, entry: DashboardEntry, state: EntryState) -> None:
|
168
|
+
"""Set the state for an entry if provided by the source or unknown."""
|
169
|
+
asyncio.run_coroutine_threadsafe(
|
170
|
+
self._async_set_state_if_source(entry, state), self._loop
|
171
|
+
).result()
|
172
|
+
|
173
|
+
async def _async_set_state_if_source(
|
174
|
+
self, entry: DashboardEntry, state: EntryState
|
175
|
+
) -> None:
|
176
|
+
"""Set the state for an entry if rovided by the source or unknown."""
|
177
|
+
self.async_set_state_if_source(entry, state)
|
178
|
+
|
179
|
+
def async_set_state_if_source(
|
180
|
+
self, entry: DashboardEntry, state: EntryState
|
181
|
+
) -> None:
|
182
|
+
"""Set the state for an entry if provided by the source or unknown."""
|
183
|
+
if entry.state.source in (
|
184
|
+
EntryStateSource.UNKNOWN,
|
185
|
+
state.source,
|
186
|
+
):
|
187
|
+
self.async_set_state(entry, state)
|
188
|
+
|
122
189
|
def async_set_state(self, entry: DashboardEntry, state: EntryState) -> None:
|
123
190
|
"""Set the state for an entry."""
|
124
191
|
if entry.state == state:
|
@@ -269,7 +336,7 @@ class DashboardEntry:
|
|
269
336
|
self._storage_path = ext_storage_path(self.filename)
|
270
337
|
self.cache_key = cache_key
|
271
338
|
self.storage: StorageJSON | None = None
|
272
|
-
self.state =
|
339
|
+
self.state = UNKNOWN_STATE
|
273
340
|
self._to_dict: dict[str, Any] | None = None
|
274
341
|
|
275
342
|
def __repr__(self) -> str:
|