esphome 2025.2.1__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.
Files changed (140) 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 +9 -0
  5. esphome/components/api/api_pb2.h +1 -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/audio_reader.cpp +7 -7
  11. esphome/components/audio/audio_reader.h +1 -1
  12. esphome/components/bluetooth_proxy/bluetooth_proxy.h +8 -0
  13. esphome/components/bmp085/bmp085.cpp +1 -1
  14. esphome/components/chsc6x/__init__.py +2 -0
  15. esphome/components/chsc6x/chsc6x_touchscreen.cpp +47 -0
  16. esphome/components/chsc6x/chsc6x_touchscreen.h +34 -0
  17. esphome/components/chsc6x/touchscreen.py +33 -0
  18. esphome/components/climate/__init__.py +0 -1
  19. esphome/components/cst816/binary_sensor/__init__.py +2 -25
  20. esphome/components/cst816/touchscreen/cst816_touchscreen.cpp +3 -14
  21. esphome/components/cst816/touchscreen/cst816_touchscreen.h +0 -4
  22. esphome/components/esp32_ble_beacon/__init__.py +3 -1
  23. esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +2 -2
  24. esphome/components/esp8266/gpio.py +1 -2
  25. esphome/components/font/__init__.py +185 -185
  26. esphome/components/font/font.cpp +4 -4
  27. esphome/components/font/font.h +1 -0
  28. esphome/components/haier/climate.py +11 -10
  29. esphome/components/hbridge/switch/hbridge_switch.cpp +2 -2
  30. esphome/components/heatpumpir/climate.py +2 -1
  31. esphome/components/heatpumpir/heatpumpir.cpp +1 -0
  32. esphome/components/heatpumpir/heatpumpir.h +1 -0
  33. esphome/components/i2c/__init__.py +6 -6
  34. esphome/components/i2c/i2c_bus_esp_idf.cpp +6 -2
  35. esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +1 -1
  36. esphome/components/ili9xxx/display.py +1 -0
  37. esphome/components/ili9xxx/ili9xxx_display.h +5 -0
  38. esphome/components/ili9xxx/ili9xxx_init.h +59 -0
  39. esphome/components/ld2450/__init__.py +51 -0
  40. esphome/components/ld2450/binary_sensor.py +47 -0
  41. esphome/components/ld2450/button/__init__.py +45 -0
  42. esphome/components/ld2450/button/reset_button.cpp +9 -0
  43. esphome/components/ld2450/button/reset_button.h +18 -0
  44. esphome/components/ld2450/button/restart_button.cpp +9 -0
  45. esphome/components/ld2450/button/restart_button.h +18 -0
  46. esphome/components/ld2450/ld2450.cpp +876 -0
  47. esphome/components/ld2450/ld2450.h +234 -0
  48. esphome/components/ld2450/number/__init__.py +121 -0
  49. esphome/components/ld2450/number/presence_timeout_number.cpp +12 -0
  50. esphome/components/ld2450/number/presence_timeout_number.h +18 -0
  51. esphome/components/ld2450/number/zone_coordinate_number.cpp +14 -0
  52. esphome/components/ld2450/number/zone_coordinate_number.h +19 -0
  53. esphome/components/ld2450/select/__init__.py +56 -0
  54. esphome/components/ld2450/select/baud_rate_select.cpp +12 -0
  55. esphome/components/ld2450/select/baud_rate_select.h +18 -0
  56. esphome/components/ld2450/select/zone_type_select.cpp +12 -0
  57. esphome/components/ld2450/select/zone_type_select.h +18 -0
  58. esphome/components/ld2450/sensor.py +156 -0
  59. esphome/components/ld2450/switch/__init__.py +45 -0
  60. esphome/components/ld2450/switch/bluetooth_switch.cpp +12 -0
  61. esphome/components/ld2450/switch/bluetooth_switch.h +18 -0
  62. esphome/components/ld2450/switch/multi_target_switch.cpp +12 -0
  63. esphome/components/ld2450/switch/multi_target_switch.h +18 -0
  64. esphome/components/ld2450/text_sensor.py +62 -0
  65. esphome/components/ltr390/ltr390.cpp +7 -7
  66. esphome/components/ltr390/ltr390.h +0 -1
  67. esphome/components/lvgl/defines.py +0 -2
  68. esphome/components/lvgl/font.cpp +1 -1
  69. esphome/components/lvgl/lvgl_esphome.cpp +27 -19
  70. esphome/components/lvgl/widgets/img.py +1 -3
  71. esphome/components/mcp2515/mcp2515.cpp +1 -0
  72. esphome/components/mlx90393/sensor.py +53 -33
  73. esphome/components/mlx90393/sensor_mlx90393.cpp +4 -0
  74. esphome/components/mlx90393/sensor_mlx90393.h +8 -3
  75. esphome/components/mqtt/__init__.py +2 -2
  76. esphome/components/msa3xx/__init__.py +189 -0
  77. esphome/components/msa3xx/binary_sensor.py +40 -0
  78. esphome/components/msa3xx/msa3xx.cpp +417 -0
  79. esphome/components/msa3xx/msa3xx.h +311 -0
  80. esphome/components/msa3xx/sensor.py +42 -0
  81. esphome/components/msa3xx/text_sensor.py +38 -0
  82. esphome/components/nfc/binary_sensor/__init__.py +4 -4
  83. esphome/components/opentherm/binary_sensor/__init__.py +4 -4
  84. esphome/components/opentherm/generate.py +6 -6
  85. esphome/components/opentherm/sensor/__init__.py +5 -6
  86. esphome/components/packages/__init__.py +35 -11
  87. esphome/components/pn532/binary_sensor.py +4 -4
  88. esphome/components/rc522/binary_sensor.py +4 -4
  89. esphome/components/socket/bsd_sockets_impl.cpp +1 -0
  90. esphome/components/socket/lwip_sockets_impl.cpp +1 -0
  91. esphome/components/socket/socket.h +3 -1
  92. esphome/components/ssd1306_base/__init__.py +7 -7
  93. esphome/components/thermostat/climate.py +1 -1
  94. esphome/components/tmp1075/tmp1075.cpp +7 -11
  95. esphome/components/tmp1075/tmp1075.h +1 -2
  96. esphome/components/tormatic/__init__.py +1 -0
  97. esphome/components/tormatic/cover.py +47 -0
  98. esphome/components/tormatic/tormatic_cover.cpp +355 -0
  99. esphome/components/tormatic/tormatic_cover.h +60 -0
  100. esphome/components/tormatic/tormatic_protocol.h +211 -0
  101. esphome/components/touchscreen/binary_sensor/__init__.py +3 -0
  102. esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.cpp +7 -1
  103. esphome/components/touchscreen/binary_sensor/touchscreen_binary_sensor.h +3 -1
  104. esphome/components/touchscreen/touchscreen.cpp +3 -4
  105. esphome/components/udp/udp_component.h +4 -1
  106. esphome/components/web_server/list_entities.cpp +70 -66
  107. esphome/components/web_server/list_entities.h +43 -22
  108. esphome/components/web_server/web_server.cpp +345 -68
  109. esphome/components/web_server/web_server.h +138 -6
  110. esphome/components/web_server_base/__init__.py +1 -1
  111. esphome/components/web_server_idf/__init__.py +2 -0
  112. esphome/components/web_server_idf/web_server_idf.cpp +177 -30
  113. esphome/components/web_server_idf/web_server_idf.h +53 -4
  114. esphome/config_validation.py +23 -125
  115. esphome/const.py +5 -1
  116. esphome/core/config.py +12 -4
  117. esphome/core/defines.h +1 -1
  118. esphome/core/helpers.h +5 -3
  119. esphome/core/time.cpp +1 -0
  120. esphome/cpp_generator.py +3 -3
  121. esphome/dashboard/core.py +30 -21
  122. esphome/dashboard/dns.py +7 -1
  123. esphome/dashboard/entries.py +83 -16
  124. esphome/dashboard/settings.py +0 -4
  125. esphome/dashboard/status/mdns.py +43 -14
  126. esphome/dashboard/status/mqtt.py +22 -9
  127. esphome/dashboard/status/ping.py +54 -10
  128. esphome/dashboard/web_server.py +56 -24
  129. esphome/storage_json.py +4 -0
  130. esphome/wizard.py +13 -17
  131. esphome/writer.py +1 -3
  132. esphome/yaml_util.py +36 -33
  133. esphome/zeroconf.py +9 -21
  134. {esphome-2025.2.1.dist-info → esphome-2025.3.0b1.dist-info}/METADATA +5 -5
  135. {esphome-2025.2.1.dist-info → esphome-2025.3.0b1.dist-info}/RECORD +139 -99
  136. esphome/components/cst816/binary_sensor/cst816_button.h +0 -27
  137. {esphome-2025.2.1.dist-info → esphome-2025.3.0b1.dist-info}/LICENSE +0 -0
  138. {esphome-2025.2.1.dist-info → esphome-2025.3.0b1.dist-info}/WHEEL +0 -0
  139. {esphome-2025.2.1.dist-info → esphome-2025.3.0b1.dist-info}/entry_points.txt +0 -0
  140. {esphome-2025.2.1.dist-info → esphome-2025.3.0b1.dist-info}/top_level.txt +0 -0
@@ -54,10 +54,6 @@ class DashboardSettings:
54
54
  def relative_url(self) -> str:
55
55
  return os.getenv("ESPHOME_DASHBOARD_RELATIVE_URL") or "/"
56
56
 
57
- @property
58
- def status_use_ping(self):
59
- return get_bool_env("ESPHOME_DASHBOARD_USE_PING")
60
-
61
57
  @property
62
58
  def status_use_mqtt(self) -> bool:
63
59
  return get_bool_env("ESPHOME_DASHBOARD_USE_MQTT")
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import logging
5
+ import typing
4
6
 
5
7
  from esphome.zeroconf import (
6
8
  ESPHOME_SERVICE_TYPE,
@@ -11,20 +13,36 @@ from esphome.zeroconf import (
11
13
  )
12
14
 
13
15
  from ..const import SENTINEL
14
- from ..core import DASHBOARD
15
- from ..entries import DashboardEntry, bool_to_entry_state
16
+ from ..entries import DashboardEntry, EntryStateSource, bool_to_entry_state
17
+
18
+ if typing.TYPE_CHECKING:
19
+ from ..core import ESPHomeDashboard
20
+
21
+ _LOGGER = logging.getLogger(__name__)
16
22
 
17
23
 
18
24
  class MDNSStatus:
19
25
  """Class that updates the mdns status."""
20
26
 
21
- def __init__(self) -> None:
27
+ def __init__(self, dashboard: ESPHomeDashboard) -> None:
22
28
  """Initialize the MDNSStatus class."""
23
29
  super().__init__()
24
30
  self.aiozc: AsyncEsphomeZeroconf | None = None
25
31
  # This is the current mdns state for each host (True, False, None)
26
32
  self.host_mdns_state: dict[str, bool | None] = {}
27
33
  self._loop = asyncio.get_running_loop()
34
+ self.dashboard = dashboard
35
+
36
+ def async_setup(self) -> bool:
37
+ """Set up the MDNSStatus class."""
38
+ try:
39
+ self.aiozc = AsyncEsphomeZeroconf()
40
+ except OSError as e:
41
+ _LOGGER.warning(
42
+ "Failed to initialize zeroconf, will fallback to ping: %s", e
43
+ )
44
+ return False
45
+ return True
28
46
 
29
47
  async def async_resolve_host(self, host_name: str) -> list[str] | None:
30
48
  """Resolve a host name to an address in a thread-safe manner."""
@@ -32,9 +50,9 @@ class MDNSStatus:
32
50
  return await aiozc.async_resolve_host(host_name)
33
51
  return None
34
52
 
35
- async def async_refresh_hosts(self):
53
+ async def async_refresh_hosts(self) -> None:
36
54
  """Refresh the hosts to track."""
37
- dashboard = DASHBOARD
55
+ dashboard = self.dashboard
38
56
  host_mdns_state = self.host_mdns_state
39
57
  entries = dashboard.entries
40
58
  poll_names: dict[str, set[DashboardEntry]] = {}
@@ -49,7 +67,7 @@ class MDNSStatus:
49
67
  # the device won't respond to a request to ._esphomelib._tcp.local.
50
68
  poll_names.setdefault(entry.name, set()).add(entry)
51
69
  elif (online := host_mdns_state.get(entry.name, SENTINEL)) != SENTINEL:
52
- entries.async_set_state(entry, bool_to_entry_state(online))
70
+ self._async_set_state(entry, online)
53
71
  if poll_names and self.aiozc:
54
72
  results = await asyncio.gather(
55
73
  *(self.aiozc.async_resolve_host(name) for name in poll_names)
@@ -58,13 +76,25 @@ class MDNSStatus:
58
76
  result = bool(address_list)
59
77
  host_mdns_state[name] = result
60
78
  for entry in poll_names[name]:
61
- entries.async_set_state(entry, bool_to_entry_state(result))
79
+ self._async_set_state(entry, result)
80
+
81
+ def _async_set_state(self, entry: DashboardEntry, result: bool | None) -> None:
82
+ """Set the state of an entry."""
83
+ state = bool_to_entry_state(result, EntryStateSource.MDNS)
84
+ if result:
85
+ # If we can reach it via mDNS, we always set it online
86
+ # since its the fastest source if its working
87
+ self.dashboard.entries.async_set_state(entry, state)
88
+ else:
89
+ # However if we can't reach it via mDNS
90
+ # we only set it to offline if the state is unknown
91
+ # or from mDNS
92
+ self.dashboard.entries.async_set_state_if_source(entry, state)
62
93
 
63
94
  async def async_run(self) -> None:
64
- dashboard = DASHBOARD
95
+ """Run the mdns status."""
96
+ dashboard = self.dashboard
65
97
  entries = dashboard.entries
66
- aiozc = AsyncEsphomeZeroconf()
67
- self.aiozc = aiozc
68
98
  host_mdns_state = self.host_mdns_state
69
99
 
70
100
  def on_update(dat: dict[str, bool | None]) -> None:
@@ -73,15 +103,14 @@ class MDNSStatus:
73
103
  host_mdns_state[name] = result
74
104
  if matching_entries := entries.get_by_name(name):
75
105
  for entry in matching_entries:
76
- if not entry.no_mdns:
77
- entries.async_set_state(entry, bool_to_entry_state(result))
106
+ self._async_set_state(entry, result)
78
107
 
79
108
  stat = DashboardStatus(on_update)
80
109
  imports = DashboardImportDiscovery()
81
110
  dashboard.import_result = imports.import_state
82
111
 
83
112
  browser = DashboardBrowser(
84
- aiozc.zeroconf,
113
+ self.aiozc.zeroconf,
85
114
  ESPHOME_SERVICE_TYPE,
86
115
  [stat.browser_callback, imports.browser_callback],
87
116
  )
@@ -93,5 +122,5 @@ class MDNSStatus:
93
122
  ping_request.clear()
94
123
 
95
124
  await browser.async_cancel()
96
- await aiozc.async_close()
125
+ await self.aiozc.async_close()
97
126
  self.aiozc = None
@@ -4,19 +4,27 @@ import binascii
4
4
  import json
5
5
  import os
6
6
  import threading
7
+ import typing
7
8
 
8
9
  from esphome import mqtt
9
10
 
10
- from ..core import DASHBOARD
11
- from ..entries import EntryState
11
+ from ..entries import EntryStateSource, bool_to_entry_state
12
+
13
+ if typing.TYPE_CHECKING:
14
+ from ..core import ESPHomeDashboard
12
15
 
13
16
 
14
17
  class MqttStatusThread(threading.Thread):
15
18
  """Status thread to get the status of the devices via MQTT."""
16
19
 
20
+ def __init__(self, dashboard: ESPHomeDashboard) -> None:
21
+ """Initialize the status thread."""
22
+ super().__init__()
23
+ self.dashboard = dashboard
24
+
17
25
  def run(self) -> None:
18
26
  """Run the status thread."""
19
- dashboard = DASHBOARD
27
+ dashboard = self.dashboard
20
28
  entries = dashboard.entries
21
29
  current_entries = entries.all()
22
30
 
@@ -31,10 +39,13 @@ class MqttStatusThread(threading.Thread):
31
39
  data = json.loads(payload)
32
40
  if "name" not in data:
33
41
  return
34
- for entry in current_entries:
35
- if entry.name == data["name"]:
36
- entries.set_state(entry, EntryState.ONLINE)
37
- return
42
+ if matching_entries := entries.get_by_name(data["name"]):
43
+ for entry in matching_entries:
44
+ # Only override state if we don't have a state from another source
45
+ # or we have a state from MQTT and the device is reachable
46
+ entries.set_state_if_online_or_source(
47
+ entry, bool_to_entry_state(True, EntryStateSource.MQTT)
48
+ )
38
49
 
39
50
  def on_connect(client, userdata, flags, return_code):
40
51
  client.publish("esphome/discover", None, retain=False)
@@ -56,8 +67,10 @@ class MqttStatusThread(threading.Thread):
56
67
  current_entries = entries.all()
57
68
  # will be set to true on on_message
58
69
  for entry in current_entries:
59
- if entry.no_mdns:
60
- entries.set_state(entry, EntryState.OFFLINE)
70
+ # Only override state if we don't have a state from another source
71
+ entries.set_state_if_source(
72
+ entry, bool_to_entry_state(False, EntryStateSource.MQTT)
73
+ )
61
74
 
62
75
  client.publish("esphome/discover", None, retain=False)
63
76
  dashboard.mqtt_ping_request.wait()
@@ -3,29 +3,44 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import logging
5
5
  import time
6
+ import typing
6
7
  from typing import cast
7
8
 
8
9
  from icmplib import Host, SocketPermissionError, async_ping
9
10
 
10
11
  from ..const import MAX_EXECUTOR_WORKERS
11
- from ..core import DASHBOARD
12
- from ..entries import DashboardEntry, EntryState, bool_to_entry_state
12
+ from ..entries import (
13
+ DashboardEntry,
14
+ EntryState,
15
+ EntryStateSource,
16
+ ReachableState,
17
+ bool_to_entry_state,
18
+ )
13
19
  from ..util.itertools import chunked
14
20
 
21
+ if typing.TYPE_CHECKING:
22
+ from ..core import ESPHomeDashboard
23
+
24
+
15
25
  _LOGGER = logging.getLogger(__name__)
16
26
 
17
27
  GROUP_SIZE = int(MAX_EXECUTOR_WORKERS / 2)
18
28
 
29
+ DNS_FAILURE_STATE = EntryState(ReachableState.DNS_FAILURE, EntryStateSource.PING)
30
+
31
+ MIN_PING_INTERVAL = 5 # ensure we don't ping too often
32
+
19
33
 
20
34
  class PingStatus:
21
- def __init__(self) -> None:
35
+ def __init__(self, dashboard: ESPHomeDashboard) -> None:
22
36
  """Initialize the PingStatus class."""
23
37
  super().__init__()
24
38
  self._loop = asyncio.get_running_loop()
39
+ self.dashboard = dashboard
25
40
 
26
41
  async def async_run(self) -> None:
27
42
  """Run the ping status."""
28
- dashboard = DASHBOARD
43
+ dashboard = self.dashboard
29
44
  entries = dashboard.entries
30
45
  privileged = await _can_use_icmp_lib_with_privilege()
31
46
  if privileged is None:
@@ -36,10 +51,24 @@ class PingStatus:
36
51
  # Only ping if the dashboard is open
37
52
  await dashboard.ping_request.wait()
38
53
  dashboard.ping_request.clear()
54
+ iteration_start = time.monotonic()
39
55
  current_entries = dashboard.entries.async_all()
40
- to_ping: list[DashboardEntry] = [
41
- entry for entry in current_entries if entry.address is not None
42
- ]
56
+ to_ping: list[DashboardEntry] = []
57
+
58
+ for entry in current_entries:
59
+ if entry.address is None:
60
+ # No address or we already have a state from another source
61
+ # so no need to ping
62
+ continue
63
+ if (
64
+ entry.state.reachable is ReachableState.ONLINE
65
+ and entry.state.source
66
+ not in (EntryStateSource.PING, EntryStateSource.UNKNOWN)
67
+ ):
68
+ # If we already have a state from another source and
69
+ # it's online, we don't need to ping
70
+ continue
71
+ to_ping.append(entry)
43
72
 
44
73
  # Resolve DNS for all entries
45
74
  entries_with_addresses: dict[DashboardEntry, list[str]] = {}
@@ -56,7 +85,10 @@ class PingStatus:
56
85
 
57
86
  for entry, result in zip(ping_group, dns_results):
58
87
  if isinstance(result, Exception):
59
- entries.async_set_state(entry, EntryState.UNKNOWN)
88
+ # Only update state if its unknown or from ping
89
+ # so we don't mark it as offline if we have a state
90
+ # from mDNS or MQTT
91
+ entries.async_set_state_if_source(entry, DNS_FAILURE_STATE)
60
92
  continue
61
93
  if isinstance(result, BaseException):
62
94
  raise result
@@ -82,8 +114,20 @@ class PingStatus:
82
114
  else:
83
115
  host: Host = result
84
116
  ping_result = host.is_alive
85
- entry, _ = entry_addresses
86
- entries.async_set_state(entry, bool_to_entry_state(ping_result))
117
+ entry: DashboardEntry = entry_addresses[0]
118
+ # If we can reach it via ping, we always set it
119
+ # online, however if we can't reach it via ping
120
+ # we only set it to offline if the state is unknown
121
+ # or from ping
122
+ entries.async_set_state_if_online_or_source(
123
+ entry,
124
+ bool_to_entry_state(ping_result, EntryStateSource.PING),
125
+ )
126
+
127
+ if not dashboard.stop_event.is_set():
128
+ iteration_duration = time.monotonic() - iteration_start
129
+ if iteration_duration < MIN_PING_INTERVAL:
130
+ await asyncio.sleep(MIN_PING_INTERVAL - iteration_duration)
87
131
 
88
132
 
89
133
  async def _can_use_icmp_lib_with_privilege() -> None | bool:
@@ -33,18 +33,24 @@ import tornado.process
33
33
  import tornado.queues
34
34
  import tornado.web
35
35
  import tornado.websocket
36
+ import voluptuous as vol
36
37
  import yaml
37
38
  from yaml.nodes import Node
38
39
 
39
40
  from esphome import const, platformio_api, yaml_util
40
41
  from esphome.helpers import get_bool_env, mkdir_p
41
- from esphome.storage_json import StorageJSON, ext_storage_path, trash_storage_path
42
+ from esphome.storage_json import (
43
+ StorageJSON,
44
+ archive_storage_path,
45
+ ext_storage_path,
46
+ trash_storage_path,
47
+ )
42
48
  from esphome.util import get_serial_ports, shlex_quote
43
49
  from esphome.yaml_util import FastestAvailableSafeLoader
44
50
 
45
51
  from .const import DASHBOARD_COMMAND
46
52
  from .core import DASHBOARD
47
- from .entries import EntryState, entry_state_to_bool
53
+ from .entries import UNKNOWN_STATE, entry_state_to_bool
48
54
  from .util.file import write_file
49
55
  from .util.subprocess import async_run_system_command
50
56
  from .util.text import friendly_name_slugify
@@ -52,7 +58,6 @@ from .util.text import friendly_name_slugify
52
58
  if TYPE_CHECKING:
53
59
  from requests import Response
54
60
 
55
-
56
61
  _LOGGER = logging.getLogger(__name__)
57
62
 
58
63
  ENV_DEV = "ESPHOME_DASHBOARD_DEV"
@@ -381,7 +386,7 @@ class EsphomeRenameHandler(EsphomeCommandWebSocket):
381
386
  # Remove the old ping result from the cache
382
387
  entries = DASHBOARD.entries
383
388
  if entry := entries.get(self.old_name):
384
- entries.async_set_state(entry, EntryState.UNKNOWN)
389
+ entries.async_set_state(entry, UNKNOWN_STATE)
385
390
 
386
391
 
387
392
  class EsphomeUploadHandler(EsphomePortCommandWebSocket):
@@ -592,16 +597,39 @@ class IgnoreDeviceRequestHandler(BaseHandler):
592
597
  class DownloadListRequestHandler(BaseHandler):
593
598
  @authenticated
594
599
  @bind_config
595
- def get(self, configuration: str | None = None) -> None:
600
+ async def get(self, configuration: str | None = None) -> None:
601
+ loop = asyncio.get_running_loop()
602
+ try:
603
+ downloads_json = await loop.run_in_executor(None, self._get, configuration)
604
+ except vol.Invalid:
605
+ self.send_error(404)
606
+ return
607
+ if downloads_json is None:
608
+ self.send_error(404)
609
+ return
610
+ self.set_status(200)
611
+ self.set_header("content-type", "application/json")
612
+ self.write(downloads_json)
613
+ self.finish()
614
+
615
+ def _get(self, configuration: str | None = None) -> dict[str, Any] | None:
596
616
  storage_path = ext_storage_path(configuration)
597
617
  storage_json = StorageJSON.load(storage_path)
598
618
  if storage_json is None:
599
- self.send_error(404)
600
- return
619
+ return None
620
+
621
+ config = yaml_util.load_yaml(settings.rel_path(configuration))
622
+
623
+ if const.CONF_EXTERNAL_COMPONENTS in config:
624
+ from esphome.components.external_components import (
625
+ do_external_components_pass,
626
+ )
627
+
628
+ do_external_components_pass(config)
601
629
 
602
630
  from esphome.components.esp32 import VARIANTS as ESP32_VARIANTS
603
631
 
604
- downloads = []
632
+ downloads: list[dict[str, Any]] = []
605
633
  platform: str = storage_json.target_platform.lower()
606
634
 
607
635
  if platform.upper() in ESP32_VARIANTS:
@@ -615,12 +643,7 @@ class DownloadListRequestHandler(BaseHandler):
615
643
  except AttributeError as exc:
616
644
  raise ValueError(f"Unknown platform {platform}") from exc
617
645
  downloads = get_download_types(storage_json)
618
-
619
- self.set_status(200)
620
- self.set_header("content-type", "application/json")
621
- self.write(json.dumps(downloads))
622
- self.finish()
623
- return
646
+ return json.dumps(downloads)
624
647
 
625
648
 
626
649
  class DownloadBinaryRequestHandler(BaseHandler):
@@ -918,16 +941,16 @@ class EditRequestHandler(BaseHandler):
918
941
  self.set_status(200)
919
942
 
920
943
 
921
- class DeleteRequestHandler(BaseHandler):
944
+ class ArchiveRequestHandler(BaseHandler):
922
945
  @authenticated
923
946
  @bind_config
924
947
  def post(self, configuration: str | None = None) -> None:
925
948
  config_file = settings.rel_path(configuration)
926
949
  storage_path = ext_storage_path(configuration)
927
950
 
928
- trash_path = trash_storage_path()
929
- mkdir_p(trash_path)
930
- shutil.move(config_file, os.path.join(trash_path, configuration))
951
+ archive_path = archive_storage_path()
952
+ mkdir_p(archive_path)
953
+ shutil.move(config_file, os.path.join(archive_path, configuration))
931
954
 
932
955
  storage_json = StorageJSON.load(storage_path)
933
956
  if storage_json is not None:
@@ -935,16 +958,16 @@ class DeleteRequestHandler(BaseHandler):
935
958
  name = storage_json.name
936
959
  build_folder = os.path.join(settings.config_dir, name)
937
960
  if build_folder is not None:
938
- shutil.rmtree(build_folder, os.path.join(trash_path, name))
961
+ shutil.rmtree(build_folder, os.path.join(archive_path, name))
939
962
 
940
963
 
941
- class UndoDeleteRequestHandler(BaseHandler):
964
+ class UnArchiveRequestHandler(BaseHandler):
942
965
  @authenticated
943
966
  @bind_config
944
967
  def post(self, configuration: str | None = None) -> None:
945
968
  config_file = settings.rel_path(configuration)
946
- trash_path = trash_storage_path()
947
- shutil.move(os.path.join(trash_path, configuration), config_file)
969
+ archive_path = archive_storage_path()
970
+ shutil.move(os.path.join(archive_path, configuration), config_file)
948
971
 
949
972
 
950
973
  class LoginHandler(BaseHandler):
@@ -1185,8 +1208,10 @@ def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application:
1185
1208
  (f"{rel}download.bin", DownloadBinaryRequestHandler),
1186
1209
  (f"{rel}serial-ports", SerialPortRequestHandler),
1187
1210
  (f"{rel}ping", PingRequestHandler),
1188
- (f"{rel}delete", DeleteRequestHandler),
1189
- (f"{rel}undo-delete", UndoDeleteRequestHandler),
1211
+ (f"{rel}delete", ArchiveRequestHandler),
1212
+ (f"{rel}undo-delete", UnArchiveRequestHandler),
1213
+ (f"{rel}archive", ArchiveRequestHandler),
1214
+ (f"{rel}unarchive", UnArchiveRequestHandler),
1190
1215
  (f"{rel}wizard", WizardRequestHandler),
1191
1216
  (f"{rel}static/(.*)", StaticFileHandler, {"path": get_static_path()}),
1192
1217
  (f"{rel}devices", ListDevicesHandler),
@@ -1211,6 +1236,13 @@ def start_web_server(
1211
1236
  config_dir: str,
1212
1237
  ) -> None:
1213
1238
  """Start the web server listener."""
1239
+
1240
+ trash_path = trash_storage_path()
1241
+ if os.path.exists(trash_path):
1242
+ _LOGGER.info("Renaming 'trash' folder to 'archive'")
1243
+ archive_path = archive_storage_path()
1244
+ shutil.move(trash_path, archive_path)
1245
+
1214
1246
  if socket is None:
1215
1247
  _LOGGER.info(
1216
1248
  "Starting dashboard web server on http://%s:%s and configuration dir %s...",
esphome/storage_json.py CHANGED
@@ -36,6 +36,10 @@ def trash_storage_path() -> str:
36
36
  return CORE.relative_config_path("trash")
37
37
 
38
38
 
39
+ def archive_storage_path() -> str:
40
+ return CORE.relative_config_path("archive")
41
+
42
+
39
43
  class StorageJSON:
40
44
  def __init__(
41
45
  self,
esphome/wizard.py CHANGED
@@ -144,17 +144,17 @@ def wizard_file(**kwargs):
144
144
 
145
145
  # Configure API
146
146
  if "password" in kwargs:
147
- config += f" password: \"{kwargs['password']}\"\n"
147
+ config += f' password: "{kwargs["password"]}"\n'
148
148
  if "api_encryption_key" in kwargs:
149
- config += f" encryption:\n key: \"{kwargs['api_encryption_key']}\"\n"
149
+ config += f' encryption:\n key: "{kwargs["api_encryption_key"]}"\n'
150
150
 
151
151
  # Configure OTA
152
152
  config += "\nota:\n"
153
153
  config += " - platform: esphome\n"
154
154
  if "ota_password" in kwargs:
155
- config += f" password: \"{kwargs['ota_password']}\""
155
+ config += f' password: "{kwargs["ota_password"]}"'
156
156
  elif "password" in kwargs:
157
- config += f" password: \"{kwargs['password']}\""
157
+ config += f' password: "{kwargs["password"]}"'
158
158
 
159
159
  # Configuring wifi
160
160
  config += "\n\nwifi:\n"
@@ -181,18 +181,14 @@ def wizard_file(**kwargs):
181
181
  password: "{fallback_psk}"
182
182
 
183
183
  captive_portal:
184
- """.format(
185
- **kwargs
186
- )
184
+ """.format(**kwargs)
187
185
  else:
188
186
  config += """
189
187
  # Enable fallback hotspot in case wifi connection fails
190
188
  ap:
191
189
  ssid: "{fallback_name}"
192
190
  password: "{fallback_psk}"
193
- """.format(
194
- **kwargs
195
- )
191
+ """.format(**kwargs)
196
192
 
197
193
  return config
198
194
 
@@ -388,19 +384,19 @@ def wizard(path):
388
384
  safe_print()
389
385
  # Don't sleep because user needs to copy link
390
386
  if platform == "ESP32":
391
- safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'nodemcu-32s')}\".")
387
+ safe_print(f'For example "{color(Fore.BOLD_WHITE, "nodemcu-32s")}".')
392
388
  boards_list = esp32_boards.BOARDS.items()
393
389
  elif platform == "ESP8266":
394
- safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'nodemcuv2')}\".")
390
+ safe_print(f'For example "{color(Fore.BOLD_WHITE, "nodemcuv2")}".')
395
391
  boards_list = esp8266_boards.BOARDS.items()
396
392
  elif platform == "BK72XX":
397
- safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'cb2s')}\".")
393
+ safe_print(f'For example "{color(Fore.BOLD_WHITE, "cb2s")}".')
398
394
  boards_list = bk72xx_boards.BOARDS.items()
399
395
  elif platform == "RTL87XX":
400
- safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'wr3')}\".")
396
+ safe_print(f'For example "{color(Fore.BOLD_WHITE, "wr3")}".')
401
397
  boards_list = rtl87xx_boards.BOARDS.items()
402
398
  elif platform == "RP2040":
403
- safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'rpipicow')}\".")
399
+ safe_print(f'For example "{color(Fore.BOLD_WHITE, "rpipicow")}".')
404
400
  boards_list = rp2040_boards.BOARDS.items()
405
401
 
406
402
  else:
@@ -439,7 +435,7 @@ def wizard(path):
439
435
  f"First, what's the {color(Fore.GREEN, 'SSID')} (the name) of the WiFi network {name} should connect to?"
440
436
  )
441
437
  sleep(1.5)
442
- safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'Abraham Linksys')}\".")
438
+ safe_print(f'For example "{color(Fore.BOLD_WHITE, "Abraham Linksys")}".')
443
439
  while True:
444
440
  ssid = safe_input(color(Fore.BOLD_WHITE, "(ssid): "))
445
441
  try:
@@ -465,7 +461,7 @@ def wizard(path):
465
461
  f"Now please state the {color(Fore.GREEN, 'password')} of the WiFi network so that I can connect to it (Leave empty for no password)"
466
462
  )
467
463
  safe_print()
468
- safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'PASSWORD42')}\"")
464
+ safe_print(f'For example "{color(Fore.BOLD_WHITE, "PASSWORD42")}"')
469
465
  sleep(0.5)
470
466
  psk = safe_input(color(Fore.BOLD_WHITE, "(PSK): "))
471
467
  safe_print(
esphome/writer.py CHANGED
@@ -212,9 +212,7 @@ def write_platformio_project():
212
212
  write_platformio_ini(content)
213
213
 
214
214
 
215
- DEFINES_H_FORMAT = (
216
- ESPHOME_H_FORMAT
217
- ) = """\
215
+ DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\
218
216
  #pragma once
219
217
  #include "esphome/core/macros.h"
220
218
  {}
esphome/yaml_util.py CHANGED
@@ -273,48 +273,18 @@ class ESPHomeLoaderMixin:
273
273
 
274
274
  @_add_data_ref
275
275
  def construct_include(self, node):
276
+ from esphome.const import CONF_VARS
277
+
276
278
  def extract_file_vars(node):
277
279
  fields = self.construct_yaml_map(node)
278
280
  file = fields.get("file")
279
281
  if file is None:
280
282
  raise yaml.MarkedYAMLError("Must include 'file'", node.start_mark)
281
- vars = fields.get("vars")
283
+ vars = fields.get(CONF_VARS)
282
284
  if vars:
283
285
  vars = {k: str(v) for k, v in vars.items()}
284
286
  return file, vars
285
287
 
286
- def substitute_vars(config, vars):
287
- from esphome.components import substitutions
288
- from esphome.const import CONF_DEFAULTS, CONF_SUBSTITUTIONS
289
-
290
- org_subs = None
291
- result = config
292
- if not isinstance(config, dict):
293
- # when the included yaml contains a list or a scalar
294
- # wrap it into an OrderedDict because do_substitution_pass expects it
295
- result = OrderedDict([("yaml", config)])
296
- elif CONF_SUBSTITUTIONS in result:
297
- org_subs = result.pop(CONF_SUBSTITUTIONS)
298
-
299
- defaults = {}
300
- if CONF_DEFAULTS in result:
301
- defaults = result.pop(CONF_DEFAULTS)
302
-
303
- result[CONF_SUBSTITUTIONS] = vars
304
- for k, v in defaults.items():
305
- if k not in result[CONF_SUBSTITUTIONS]:
306
- result[CONF_SUBSTITUTIONS][k] = v
307
-
308
- # Ignore missing vars that refer to the top level substitutions
309
- substitutions.do_substitution_pass(result, None, ignore_missing=True)
310
- result.pop(CONF_SUBSTITUTIONS)
311
-
312
- if not isinstance(config, dict):
313
- result = result["yaml"] # unwrap the result
314
- elif org_subs:
315
- result[CONF_SUBSTITUTIONS] = org_subs
316
- return result
317
-
318
288
  if isinstance(node, yaml.nodes.MappingNode):
319
289
  file, vars = extract_file_vars(node)
320
290
  else:
@@ -432,6 +402,39 @@ def parse_yaml(file_name: str, file_handle: TextIOWrapper) -> Any:
432
402
  )
433
403
 
434
404
 
405
+ def substitute_vars(config, vars):
406
+ from esphome.components import substitutions
407
+ from esphome.const import CONF_DEFAULTS, CONF_SUBSTITUTIONS
408
+
409
+ org_subs = None
410
+ result = config
411
+ if not isinstance(config, dict):
412
+ # when the included yaml contains a list or a scalar
413
+ # wrap it into an OrderedDict because do_substitution_pass expects it
414
+ result = OrderedDict([("yaml", config)])
415
+ elif CONF_SUBSTITUTIONS in result:
416
+ org_subs = result.pop(CONF_SUBSTITUTIONS)
417
+
418
+ defaults = {}
419
+ if CONF_DEFAULTS in result:
420
+ defaults = result.pop(CONF_DEFAULTS)
421
+
422
+ result[CONF_SUBSTITUTIONS] = vars
423
+ for k, v in defaults.items():
424
+ if k not in result[CONF_SUBSTITUTIONS]:
425
+ result[CONF_SUBSTITUTIONS][k] = v
426
+
427
+ # Ignore missing vars that refer to the top level substitutions
428
+ substitutions.do_substitution_pass(result, None, ignore_missing=True)
429
+ result.pop(CONF_SUBSTITUTIONS)
430
+
431
+ if not isinstance(config, dict):
432
+ result = result["yaml"] # unwrap the result
433
+ elif org_subs:
434
+ result[CONF_SUBSTITUTIONS] = org_subs
435
+ return result
436
+
437
+
435
438
  def _load_yaml_internal(fname: str) -> Any:
436
439
  """Load a YAML file."""
437
440
  try: