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
esphome/dashboard/settings.py
CHANGED
@@ -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")
|
esphome/dashboard/status/mdns.py
CHANGED
@@ -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 ..
|
15
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
esphome/dashboard/status/mqtt.py
CHANGED
@@ -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 ..
|
11
|
-
|
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 =
|
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
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
60
|
-
|
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()
|
esphome/dashboard/status/ping.py
CHANGED
@@ -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 ..
|
12
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
86
|
-
|
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:
|
esphome/dashboard/web_server.py
CHANGED
@@ -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
|
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
|
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,
|
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
|
-
|
600
|
-
|
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
|
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
|
-
|
929
|
-
mkdir_p(
|
930
|
-
shutil.move(config_file, os.path.join(
|
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(
|
961
|
+
shutil.rmtree(build_folder, os.path.join(archive_path, name))
|
939
962
|
|
940
963
|
|
941
|
-
class
|
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
|
-
|
947
|
-
shutil.move(os.path.join(
|
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",
|
1189
|
-
(f"{rel}undo-delete",
|
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
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
|
147
|
+
config += f' password: "{kwargs["password"]}"\n'
|
148
148
|
if "api_encryption_key" in kwargs:
|
149
|
-
config += f
|
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
|
155
|
+
config += f' password: "{kwargs["ota_password"]}"'
|
156
156
|
elif "password" in kwargs:
|
157
|
-
config += f
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
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(
|
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:
|