esphome 2025.9.3__py3-none-any.whl → 2025.10.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 +87 -31
- esphome/address_cache.py +142 -0
- esphome/automation.py +130 -32
- esphome/build_gen/platformio.py +1 -3
- esphome/codegen.py +1 -0
- esphome/components/animation/animation.cpp +2 -2
- esphome/components/api/__init__.py +166 -3
- esphome/components/api/api_connection.cpp +84 -41
- esphome/components/api/api_connection.h +22 -16
- esphome/components/api/api_frame_helper.cpp +33 -19
- esphome/components/api/api_frame_helper.h +19 -4
- esphome/components/api/api_frame_helper_noise.cpp +41 -53
- esphome/components/api/api_frame_helper_noise.h +1 -1
- esphome/components/api/api_frame_helper_plaintext.cpp +22 -31
- esphome/components/api/api_frame_helper_plaintext.h +1 -1
- esphome/components/api/api_pb2.cpp +189 -15
- esphome/components/api/api_pb2.h +132 -20
- esphome/components/api/api_pb2_dump.cpp +97 -9
- esphome/components/api/api_pb2_service.cpp +118 -160
- esphome/components/api/api_pb2_service.h +31 -3
- esphome/components/api/api_server.cpp +68 -10
- esphome/components/api/api_server.h +32 -4
- esphome/components/api/custom_api_device.h +8 -8
- esphome/components/api/homeassistant_service.h +123 -6
- esphome/components/api/proto.h +6 -2
- esphome/components/api/user_services.h +2 -2
- esphome/components/as7341/sensor.py +1 -1
- esphome/components/audio/__init__.py +1 -1
- esphome/components/audio/audio.cpp +1 -1
- esphome/components/audio/audio_decoder.cpp +9 -9
- esphome/components/bl0906/bl0906.cpp +2 -2
- esphome/components/bl0942/bl0942.cpp +2 -2
- esphome/components/ble_client/__init__.py +1 -1
- esphome/components/bluetooth_proxy/__init__.py +4 -30
- esphome/components/bluetooth_proxy/bluetooth_connection.cpp +11 -4
- esphome/components/bluetooth_proxy/bluetooth_connection.h +2 -2
- esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +2 -2
- esphome/components/camera_encoder/__init__.py +2 -4
- esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp +4 -2
- esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h +3 -1
- esphome/components/canbus/canbus.cpp +7 -5
- esphome/components/canbus/canbus.h +4 -4
- esphome/components/captive_portal/__init__.py +18 -1
- esphome/components/captive_portal/captive_portal.cpp +40 -46
- esphome/components/captive_portal/captive_portal.h +20 -22
- esphome/components/captive_portal/dns_server_esp32_idf.cpp +205 -0
- esphome/components/captive_portal/dns_server_esp32_idf.h +27 -0
- esphome/components/ccs811/ccs811.cpp +1 -1
- esphome/components/climate/climate.cpp +10 -7
- esphome/components/cm1106/cm1106.cpp +1 -1
- esphome/components/copy/lock/copy_lock.cpp +1 -1
- esphome/components/cover/cover.cpp +1 -0
- esphome/components/daikin_arc/daikin_arc.cpp +19 -12
- esphome/components/deep_sleep/__init__.py +9 -2
- esphome/components/deep_sleep/deep_sleep_component.h +11 -9
- esphome/components/deep_sleep/deep_sleep_esp32.cpp +51 -27
- esphome/components/ektf2232/touchscreen/__init__.py +8 -5
- esphome/components/ektf2232/touchscreen/ektf2232.cpp +4 -4
- esphome/components/ektf2232/touchscreen/ektf2232.h +2 -2
- esphome/components/epaper_spi/__init__.py +1 -0
- esphome/components/epaper_spi/display.py +80 -0
- esphome/components/epaper_spi/epaper_spi.cpp +227 -0
- esphome/components/epaper_spi/epaper_spi.h +93 -0
- esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp +42 -0
- esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h +45 -0
- esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp +135 -0
- esphome/components/epaper_spi/epaper_spi_spectra_e6.h +23 -0
- esphome/components/es7210/es7210.cpp +3 -3
- esphome/components/esp32/__init__.py +254 -339
- esphome/components/esp32/boards.py +81 -0
- esphome/components/esp32/preferences.cpp +23 -17
- esphome/components/esp32_ble/__init__.py +159 -44
- esphome/components/esp32_ble/ble.cpp +47 -3
- esphome/components/esp32_ble/ble.h +18 -0
- esphome/components/esp32_ble/ble_advertising.cpp +7 -3
- esphome/components/esp32_ble/ble_advertising.h +4 -0
- esphome/components/esp32_ble/ble_uuid.cpp +16 -42
- esphome/components/esp32_ble_beacon/__init__.py +3 -4
- esphome/components/esp32_ble_client/ble_client_base.cpp +14 -12
- esphome/components/esp32_ble_server/__init__.py +28 -14
- esphome/components/esp32_ble_server/ble_characteristic.cpp +67 -57
- esphome/components/esp32_ble_server/ble_characteristic.h +27 -16
- esphome/components/esp32_ble_server/ble_descriptor.cpp +4 -3
- esphome/components/esp32_ble_server/ble_descriptor.h +13 -9
- esphome/components/esp32_ble_server/ble_server.cpp +59 -24
- esphome/components/esp32_ble_server/ble_server.h +38 -20
- esphome/components/esp32_ble_server/ble_server_automations.cpp +49 -33
- esphome/components/esp32_ble_server/ble_server_automations.h +39 -24
- esphome/components/esp32_ble_tracker/__init__.py +25 -80
- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +2 -4
- esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +0 -3
- esphome/components/esp32_camera/__init__.py +1 -3
- esphome/components/esp32_can/esp32_can.cpp +22 -4
- esphome/components/esp32_can/esp32_can.h +3 -0
- esphome/components/esp32_hosted/__init__.py +2 -1
- esphome/components/esp32_improv/esp32_improv_component.cpp +102 -44
- esphome/components/esp32_improv/esp32_improv_component.h +6 -1
- esphome/components/esp32_rmt_led_strip/led_strip.cpp +1 -1
- esphome/components/esp8266/__init__.py +3 -3
- esphome/components/esphome/ota/__init__.py +21 -2
- esphome/components/esphome/ota/ota_esphome.cpp +455 -145
- esphome/components/esphome/ota/ota_esphome.h +49 -2
- esphome/components/ethernet/__init__.py +39 -22
- esphome/components/ethernet/ethernet_component.cpp +28 -5
- esphome/components/ethernet/ethernet_component.h +5 -1
- esphome/components/external_components/__init__.py +8 -6
- esphome/components/fingerprint_grow/fingerprint_grow.cpp +1 -1
- esphome/components/fingerprint_grow/fingerprint_grow.h +2 -1
- esphome/components/font/__init__.py +5 -5
- esphome/components/graph/graph.cpp +1 -1
- esphome/components/graphical_display_menu/graphical_display_menu.cpp +3 -2
- esphome/components/haier/hon_climate.cpp +2 -2
- esphome/components/haier/hon_climate.h +1 -1
- esphome/components/hdc1080/hdc1080.cpp +42 -34
- esphome/components/hdc1080/hdc1080.h +1 -3
- esphome/components/homeassistant/number/homeassistant_number.cpp +2 -2
- esphome/components/homeassistant/switch/homeassistant_switch.cpp +2 -2
- esphome/components/http_request/__init__.py +3 -3
- esphome/components/htu21d/htu21d.cpp +13 -18
- esphome/components/htu21d/htu21d.h +1 -1
- esphome/components/i2s_audio/__init__.py +1 -2
- esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +1 -1
- esphome/components/ili9xxx/ili9xxx_display.cpp +2 -2
- esphome/components/improv_serial/improv_serial_component.cpp +12 -15
- esphome/components/improv_serial/improv_serial_component.h +6 -8
- esphome/components/json/json_util.cpp +35 -43
- esphome/components/json/json_util.h +57 -0
- esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +2 -2
- esphome/components/key_collector/key_collector.h +4 -4
- esphome/components/libretiny/__init__.py +6 -6
- esphome/components/libretiny/preferences.cpp +23 -16
- esphome/components/light/light_call.cpp +98 -120
- esphome/components/light/light_call.h +17 -7
- esphome/components/lm75b/__init__.py +0 -0
- esphome/components/lm75b/lm75b.cpp +39 -0
- esphome/components/lm75b/lm75b.h +19 -0
- esphome/components/lm75b/sensor.py +34 -0
- esphome/components/lock/lock.h +12 -6
- esphome/components/logger/__init__.py +15 -27
- esphome/components/logger/logger.cpp +10 -20
- esphome/components/logger/logger.h +105 -62
- esphome/components/logger/logger_esp32.cpp +0 -48
- esphome/components/logger/logger_zephyr.cpp +2 -3
- esphome/components/logger/select/logger_level_select.cpp +6 -7
- esphome/components/logger/select/logger_level_select.h +7 -0
- esphome/components/ltr501/ltr501.cpp +7 -6
- esphome/components/ltr_als_ps/ltr_als_ps.cpp +7 -6
- esphome/components/matrix_keypad/matrix_keypad.h +4 -4
- esphome/components/max7219digit/max7219digit.cpp +1 -1
- esphome/components/mcp2515/mcp2515.cpp +31 -3
- esphome/components/mcp2515/mcp2515_defs.h +3 -1
- esphome/components/md5/md5.cpp +0 -26
- esphome/components/md5/md5.h +10 -20
- esphome/components/mdns/__init__.py +19 -6
- esphome/components/mdns/mdns_component.cpp +27 -59
- esphome/components/mdns/mdns_component.h +23 -10
- esphome/components/mdns/mdns_esp32.cpp +7 -7
- esphome/components/mdns/mdns_esp8266.cpp +6 -6
- esphome/components/mdns/mdns_libretiny.cpp +3 -3
- esphome/components/mdns/mdns_rp2040.cpp +3 -3
- esphome/components/mipi/__init__.py +1 -5
- esphome/components/mipi_spi/display.py +24 -8
- esphome/components/mipi_spi/mipi_spi.h +3 -3
- esphome/components/mixer/speaker/mixer_speaker.cpp +3 -3
- esphome/components/mmc5603/mmc5603.cpp +3 -3
- esphome/components/modbus/modbus.cpp +27 -13
- esphome/components/modbus/modbus.h +5 -3
- esphome/components/modbus/modbus_definitions.h +86 -0
- esphome/components/modbus_controller/__init__.py +29 -1
- esphome/components/modbus_controller/const.py +4 -0
- esphome/components/modbus_controller/modbus_controller.cpp +38 -13
- esphome/components/modbus_controller/modbus_controller.h +18 -29
- esphome/components/mpr121/mpr121.cpp +41 -42
- esphome/components/mpr121/mpr121.h +0 -1
- esphome/components/nau7802/nau7802.cpp +2 -2
- esphome/components/network/__init__.py +7 -3
- esphome/components/nextion/display.py +4 -4
- esphome/components/nextion/nextion.cpp +8 -8
- esphome/components/number/__init__.py +2 -0
- esphome/components/number/number_call.cpp +23 -12
- esphome/components/number/number_call.h +5 -0
- esphome/components/online_image/bmp_image.cpp +2 -1
- esphome/components/online_image/jpeg_image.cpp +4 -2
- esphome/components/openthread/openthread.cpp +6 -7
- esphome/components/openthread/openthread.h +0 -1
- esphome/components/ota/ota_backend.h +1 -0
- esphome/components/packages/__init__.py +10 -8
- esphome/components/packet_transport/packet_transport.cpp +2 -0
- esphome/components/pid/pid_controller.cpp +1 -1
- esphome/components/prometheus/prometheus_handler.cpp +239 -239
- esphome/components/psram/__init__.py +30 -28
- esphome/components/qmc5883l/qmc5883l.cpp +15 -0
- esphome/components/qmc5883l/qmc5883l.h +3 -0
- esphome/components/qmc5883l/sensor.py +31 -12
- esphome/components/remote_base/gobox_protocol.cpp +3 -3
- esphome/components/remote_receiver/__init__.py +14 -2
- esphome/components/remote_receiver/{remote_receiver_esp8266.cpp → remote_receiver.cpp} +2 -2
- esphome/components/remote_receiver/remote_receiver.h +4 -0
- esphome/components/remote_receiver/remote_receiver_esp32.cpp +18 -1
- esphome/components/remote_transmitter/__init__.py +2 -2
- esphome/components/remote_transmitter/remote_transmitter.cpp +103 -0
- esphome/components/rp2040/__init__.py +11 -11
- esphome/components/rtttl/rtttl.cpp +2 -2
- esphome/components/scd30/sensor.py +1 -1
- esphome/components/script/__init__.py +1 -1
- esphome/components/script/script.h +7 -7
- esphome/components/select/select.cpp +5 -4
- esphome/components/select/select_call.cpp +1 -1
- esphome/components/sensirion_common/i2c_sensirion.cpp +2 -1
- esphome/components/sensor/__init__.py +2 -0
- esphome/components/sha256/__init__.py +22 -0
- esphome/components/sha256/sha256.cpp +116 -0
- esphome/components/sha256/sha256.h +60 -0
- esphome/components/socket/lwip_raw_tcp_impl.cpp +34 -6
- esphome/components/sonoff_d1/sonoff_d1.cpp +1 -1
- esphome/components/spi/__init__.py +0 -3
- esphome/components/split_buffer/__init__.py +5 -0
- esphome/components/split_buffer/split_buffer.cpp +133 -0
- esphome/components/split_buffer/split_buffer.h +40 -0
- esphome/components/sps30/sps30.cpp +14 -10
- esphome/components/sps30/sps30.h +2 -0
- esphome/components/st7567_i2c/st7567_i2c.cpp +3 -1
- esphome/components/st7789v/st7789v.cpp +3 -2
- esphome/components/statsd/statsd.cpp +1 -1
- esphome/components/substitutions/__init__.py +3 -1
- esphome/components/substitutions/jinja.py +13 -3
- esphome/components/sx126x/__init__.py +16 -0
- esphome/components/sx126x/sx126x.cpp +15 -1
- esphome/components/sx126x/sx126x.h +9 -1
- esphome/components/sx126x/sx126x_reg.h +2 -0
- esphome/components/text_sensor/text_sensor.cpp +16 -0
- esphome/components/text_sensor/text_sensor.h +3 -10
- esphome/components/tormatic/tormatic_cover.cpp +1 -1
- esphome/components/tuya/select/tuya_select.cpp +1 -1
- esphome/components/tuya/tuya.cpp +29 -4
- esphome/components/uart/__init__.py +36 -26
- esphome/components/uart/uart.h +6 -0
- esphome/components/uart/uart_component.cpp +8 -0
- esphome/components/uart/uart_component.h +28 -0
- esphome/components/uart/uart_component_esp_idf.cpp +64 -10
- esphome/components/uart/uart_component_esp_idf.h +5 -2
- esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp +1 -1
- esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp +1 -1
- esphome/components/uponor_smatrix/uponor_smatrix.cpp +3 -3
- esphome/components/usb_host/__init__.py +2 -1
- esphome/components/usb_host/usb_host.h +82 -13
- esphome/components/usb_host/usb_host_client.cpp +180 -24
- esphome/components/usb_host/usb_host_component.cpp +1 -1
- esphome/components/usb_uart/__init__.py +0 -1
- esphome/components/usb_uart/ch34x.cpp +4 -4
- esphome/components/usb_uart/cp210x.cpp +3 -3
- esphome/components/usb_uart/usb_uart.cpp +88 -32
- esphome/components/usb_uart/usb_uart.h +30 -6
- esphome/components/valve/valve.cpp +1 -0
- esphome/components/veml7700/veml7700.cpp +7 -6
- esphome/components/version/version_text_sensor.cpp +2 -1
- esphome/components/voice_assistant/voice_assistant.cpp +3 -2
- esphome/components/waveshare_epaper/waveshare_epaper.cpp +4 -4
- esphome/components/web_server/list_entities.cpp +3 -4
- esphome/components/web_server/list_entities.h +8 -10
- esphome/components/web_server/ota/__init__.py +1 -1
- esphome/components/web_server/ota/ota_web_server.cpp +9 -3
- esphome/components/web_server/web_server.cpp +509 -404
- esphome/components/web_server/web_server.h +5 -6
- esphome/components/web_server/web_server_v1.cpp +21 -19
- esphome/components/web_server_base/__init__.py +5 -2
- esphome/components/web_server_base/web_server_base.h +27 -7
- esphome/components/web_server_idf/__init__.py +1 -1
- esphome/components/web_server_idf/multipart.cpp +2 -2
- esphome/components/web_server_idf/multipart.h +2 -2
- esphome/components/web_server_idf/utils.cpp +2 -2
- esphome/components/web_server_idf/utils.h +2 -2
- esphome/components/web_server_idf/web_server_idf.cpp +118 -26
- esphome/components/web_server_idf/web_server_idf.h +12 -10
- esphome/components/wifi/__init__.py +13 -11
- esphome/components/wifi/wifi_component.cpp +73 -56
- esphome/components/wifi/wifi_component.h +4 -4
- esphome/components/wifi/wifi_component_esp8266.cpp +1 -1
- esphome/components/wifi/wifi_component_esp_idf.cpp +24 -4
- esphome/components/wireguard/__init__.py +1 -1
- esphome/components/wts01/__init__.py +0 -0
- esphome/components/wts01/sensor.py +41 -0
- esphome/components/wts01/wts01.cpp +91 -0
- esphome/components/wts01/wts01.h +27 -0
- esphome/components/zephyr/__init__.py +5 -5
- esphome/components/zwave_proxy/__init__.py +43 -0
- esphome/components/zwave_proxy/zwave_proxy.cpp +346 -0
- esphome/components/zwave_proxy/zwave_proxy.h +93 -0
- esphome/config.py +79 -24
- esphome/config_validation.py +13 -15
- esphome/const.py +9 -2
- esphome/core/__init__.py +31 -22
- esphome/core/component.cpp +28 -18
- esphome/core/component_iterator.h +2 -1
- esphome/core/config.py +15 -15
- esphome/core/defines.h +19 -0
- esphome/core/hash_base.h +56 -0
- esphome/core/helpers.cpp +19 -3
- esphome/core/helpers.h +26 -0
- esphome/core/scheduler.cpp +5 -21
- esphome/core/scheduler.h +19 -8
- esphome/core/string_ref.h +1 -1
- esphome/core/time.cpp +5 -5
- esphome/cpp_generator.py +4 -29
- esphome/dashboard/const.py +21 -4
- esphome/dashboard/core.py +10 -8
- esphome/dashboard/dns.py +15 -0
- esphome/dashboard/entries.py +15 -21
- esphome/dashboard/models.py +76 -0
- esphome/dashboard/settings.py +7 -7
- esphome/dashboard/status/mdns.py +46 -2
- esphome/dashboard/web_server.py +367 -93
- esphome/espota2.py +111 -31
- esphome/external_files.py +6 -7
- esphome/git.py +8 -0
- esphome/helpers.py +124 -77
- esphome/loader.py +8 -9
- esphome/platformio_api.py +25 -18
- esphome/storage_json.py +26 -21
- esphome/types.py +30 -2
- esphome/util.py +32 -16
- esphome/vscode.py +8 -8
- esphome/wizard.py +10 -10
- esphome/writer.py +50 -15
- esphome/yaml_util.py +37 -31
- esphome/zeroconf.py +12 -3
- {esphome-2025.9.3.dist-info → esphome-2025.10.0b1.dist-info}/METADATA +11 -11
- {esphome-2025.9.3.dist-info → esphome-2025.10.0b1.dist-info}/RECORD +332 -312
- esphome/components/event_emitter/__init__.py +0 -5
- esphome/components/event_emitter/event_emitter.cpp +0 -14
- esphome/components/event_emitter/event_emitter.h +0 -63
- esphome/components/remote_receiver/remote_receiver_libretiny.cpp +0 -125
- esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp +0 -107
- esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp +0 -110
- esphome/components/uart/uart_component_esp32_arduino.cpp +0 -214
- esphome/components/uart/uart_component_esp32_arduino.h +0 -60
- esphome/components/wifi/wifi_component_esp32_arduino.cpp +0 -860
- esphome/core/string_ref.cpp +0 -12
- esphome/dashboard/util/file.py +0 -63
- {esphome-2025.9.3.dist-info → esphome-2025.10.0b1.dist-info}/WHEEL +0 -0
- {esphome-2025.9.3.dist-info → esphome-2025.10.0b1.dist-info}/entry_points.txt +0 -0
- {esphome-2025.9.3.dist-info → esphome-2025.10.0b1.dist-info}/licenses/LICENSE +0 -0
- {esphome-2025.9.3.dist-info → esphome-2025.10.0b1.dist-info}/top_level.txt +0 -0
esphome/dashboard/web_server.py
CHANGED
@@ -4,8 +4,10 @@ import asyncio
|
|
4
4
|
import base64
|
5
5
|
import binascii
|
6
6
|
from collections.abc import Callable, Iterable
|
7
|
+
import contextlib
|
7
8
|
import datetime
|
8
9
|
import functools
|
10
|
+
from functools import partial
|
9
11
|
import gzip
|
10
12
|
import hashlib
|
11
13
|
import importlib
|
@@ -49,10 +51,11 @@ from esphome.storage_json import (
|
|
49
51
|
from esphome.util import get_serial_ports, shlex_quote
|
50
52
|
from esphome.yaml_util import FastestAvailableSafeLoader
|
51
53
|
|
52
|
-
from
|
53
|
-
from .
|
54
|
-
from .
|
55
|
-
from .
|
54
|
+
from ..helpers import write_file
|
55
|
+
from .const import DASHBOARD_COMMAND, DashboardEvent
|
56
|
+
from .core import DASHBOARD, ESPHomeDashboard, Event
|
57
|
+
from .entries import UNKNOWN_STATE, DashboardEntry, entry_state_to_bool
|
58
|
+
from .models import build_device_list_response
|
56
59
|
from .util.subprocess import async_run_system_command
|
57
60
|
from .util.text import friendly_name_slugify
|
58
61
|
|
@@ -283,11 +286,23 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
|
|
283
286
|
def _stdout_thread(self) -> None:
|
284
287
|
if not self._use_popen:
|
285
288
|
return
|
289
|
+
line = b""
|
290
|
+
cr = False
|
286
291
|
while True:
|
287
|
-
data = self._proc.stdout.
|
292
|
+
data = self._proc.stdout.read(1)
|
288
293
|
if data:
|
289
|
-
data
|
290
|
-
|
294
|
+
if data == b"\r":
|
295
|
+
cr = True
|
296
|
+
elif data == b"\n":
|
297
|
+
self._queue.put_nowait(line + b"\n")
|
298
|
+
line = b""
|
299
|
+
cr = False
|
300
|
+
elif cr:
|
301
|
+
self._queue.put_nowait(line + b"\r")
|
302
|
+
line = data
|
303
|
+
cr = False
|
304
|
+
else:
|
305
|
+
line += data
|
291
306
|
if self._proc.poll() is not None:
|
292
307
|
break
|
293
308
|
self._proc.wait(1.0)
|
@@ -314,6 +329,73 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler):
|
|
314
329
|
raise NotImplementedError
|
315
330
|
|
316
331
|
|
332
|
+
def build_cache_arguments(
|
333
|
+
entry: DashboardEntry | None,
|
334
|
+
dashboard: ESPHomeDashboard,
|
335
|
+
now: float,
|
336
|
+
) -> list[str]:
|
337
|
+
"""Build cache arguments for passing to CLI.
|
338
|
+
|
339
|
+
Args:
|
340
|
+
entry: Dashboard entry for the configuration
|
341
|
+
dashboard: Dashboard instance with cache access
|
342
|
+
now: Current monotonic time for DNS cache expiry checks
|
343
|
+
|
344
|
+
Returns:
|
345
|
+
List of cache arguments to pass to CLI
|
346
|
+
"""
|
347
|
+
cache_args: list[str] = []
|
348
|
+
|
349
|
+
if not entry:
|
350
|
+
return cache_args
|
351
|
+
|
352
|
+
_LOGGER.debug(
|
353
|
+
"Building cache for entry (address=%s, name=%s)",
|
354
|
+
entry.address,
|
355
|
+
entry.name,
|
356
|
+
)
|
357
|
+
|
358
|
+
def add_cache_entry(hostname: str, addresses: list[str], cache_type: str) -> None:
|
359
|
+
"""Add a cache entry to the command arguments."""
|
360
|
+
if not addresses:
|
361
|
+
return
|
362
|
+
normalized = hostname.rstrip(".").lower()
|
363
|
+
cache_args.extend(
|
364
|
+
[
|
365
|
+
f"--{cache_type}-address-cache",
|
366
|
+
f"{normalized}={','.join(sort_ip_addresses(addresses))}",
|
367
|
+
]
|
368
|
+
)
|
369
|
+
|
370
|
+
# Check entry.address for cached addresses
|
371
|
+
if use_address := entry.address:
|
372
|
+
if use_address.endswith(".local"):
|
373
|
+
# mDNS cache for .local addresses
|
374
|
+
if (mdns := dashboard.mdns_status) and (
|
375
|
+
cached := mdns.get_cached_addresses(use_address)
|
376
|
+
):
|
377
|
+
_LOGGER.debug("mDNS cache hit for %s: %s", use_address, cached)
|
378
|
+
add_cache_entry(use_address, cached, "mdns")
|
379
|
+
# DNS cache for non-.local addresses
|
380
|
+
elif cached := dashboard.dns_cache.get_cached_addresses(use_address, now):
|
381
|
+
_LOGGER.debug("DNS cache hit for %s: %s", use_address, cached)
|
382
|
+
add_cache_entry(use_address, cached, "dns")
|
383
|
+
|
384
|
+
# Check entry.name if we haven't already cached via address
|
385
|
+
# For mDNS devices, entry.name typically doesn't have .local suffix
|
386
|
+
if entry.name and not use_address:
|
387
|
+
mdns_name = (
|
388
|
+
f"{entry.name}.local" if not entry.name.endswith(".local") else entry.name
|
389
|
+
)
|
390
|
+
if (mdns := dashboard.mdns_status) and (
|
391
|
+
cached := mdns.get_cached_addresses(mdns_name)
|
392
|
+
):
|
393
|
+
_LOGGER.debug("mDNS cache hit for %s: %s", mdns_name, cached)
|
394
|
+
add_cache_entry(mdns_name, cached, "mdns")
|
395
|
+
|
396
|
+
return cache_args
|
397
|
+
|
398
|
+
|
317
399
|
class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
|
318
400
|
"""Base class for commands that require a port."""
|
319
401
|
|
@@ -326,52 +408,22 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
|
|
326
408
|
configuration = json_message["configuration"]
|
327
409
|
config_file = settings.rel_path(configuration)
|
328
410
|
port = json_message["port"]
|
329
|
-
|
411
|
+
|
412
|
+
# Build cache arguments to pass to CLI
|
413
|
+
cache_args: list[str] = []
|
414
|
+
|
330
415
|
if (
|
331
416
|
port == "OTA" # pylint: disable=too-many-boolean-expressions
|
332
417
|
and (entry := entries.get(config_file))
|
333
418
|
and entry.loaded_integrations
|
334
419
|
and "api" in entry.loaded_integrations
|
335
420
|
):
|
336
|
-
|
337
|
-
if (
|
338
|
-
(use_address := entry.address)
|
339
|
-
and (
|
340
|
-
address_list := await dashboard.dns_cache.async_resolve(
|
341
|
-
use_address, time.monotonic()
|
342
|
-
)
|
343
|
-
)
|
344
|
-
and not isinstance(address_list, Exception)
|
345
|
-
):
|
346
|
-
addresses.extend(sort_ip_addresses(address_list))
|
347
|
-
|
348
|
-
# Second priority: mDNS
|
349
|
-
if (
|
350
|
-
(mdns := dashboard.mdns_status)
|
351
|
-
and (address_list := await mdns.async_resolve_host(entry.name))
|
352
|
-
and (
|
353
|
-
new_addresses := [
|
354
|
-
addr for addr in address_list if addr not in addresses
|
355
|
-
]
|
356
|
-
)
|
357
|
-
):
|
358
|
-
# Use the IP address if available but only
|
359
|
-
# if the API is loaded and the device is online
|
360
|
-
# since MQTT logging will not work otherwise
|
361
|
-
addresses.extend(sort_ip_addresses(new_addresses))
|
362
|
-
|
363
|
-
if not addresses:
|
364
|
-
# If no address was found, use the port directly
|
365
|
-
# as otherwise they will get the chooser which
|
366
|
-
# does not work with the dashboard as there is no
|
367
|
-
# interactive way to get keyboard input
|
368
|
-
addresses = [port]
|
369
|
-
|
370
|
-
device_args: list[str] = [
|
371
|
-
arg for address in addresses for arg in ("--device", address)
|
372
|
-
]
|
421
|
+
cache_args = build_cache_arguments(entry, dashboard, time.monotonic())
|
373
422
|
|
374
|
-
|
423
|
+
# Cache arguments must come before the subcommand
|
424
|
+
cmd = [*DASHBOARD_COMMAND, *cache_args, *args, config_file, "--device", port]
|
425
|
+
_LOGGER.debug("Built command: %s", cmd)
|
426
|
+
return cmd
|
375
427
|
|
376
428
|
|
377
429
|
class EsphomeLogsHandler(EsphomePortCommandWebSocket):
|
@@ -442,6 +494,14 @@ class EsphomeCleanMqttHandler(EsphomeCommandWebSocket):
|
|
442
494
|
return [*DASHBOARD_COMMAND, "clean-mqtt", config_file]
|
443
495
|
|
444
496
|
|
497
|
+
class EsphomeCleanAllHandler(EsphomeCommandWebSocket):
|
498
|
+
async def build_command(self, json_message: dict[str, Any]) -> list[str]:
|
499
|
+
clean_build_dir = json_message.get("clean_build_dir", True)
|
500
|
+
if clean_build_dir:
|
501
|
+
return [*DASHBOARD_COMMAND, "clean-all", settings.config_dir]
|
502
|
+
return [*DASHBOARD_COMMAND, "clean-all"]
|
503
|
+
|
504
|
+
|
445
505
|
class EsphomeCleanHandler(EsphomeCommandWebSocket):
|
446
506
|
async def build_command(self, json_message: dict[str, Any]) -> list[str]:
|
447
507
|
config_file = settings.rel_path(json_message["configuration"])
|
@@ -463,6 +523,243 @@ class EsphomeUpdateAllHandler(EsphomeCommandWebSocket):
|
|
463
523
|
return [*DASHBOARD_COMMAND, "update-all", settings.config_dir]
|
464
524
|
|
465
525
|
|
526
|
+
# Dashboard polling constants
|
527
|
+
DASHBOARD_POLL_INTERVAL = 2 # seconds
|
528
|
+
DASHBOARD_ENTRIES_UPDATE_INTERVAL = 10 # seconds
|
529
|
+
DASHBOARD_ENTRIES_UPDATE_ITERATIONS = (
|
530
|
+
DASHBOARD_ENTRIES_UPDATE_INTERVAL // DASHBOARD_POLL_INTERVAL
|
531
|
+
)
|
532
|
+
|
533
|
+
|
534
|
+
class DashboardSubscriber:
|
535
|
+
"""Manages dashboard event polling task lifecycle based on active subscribers."""
|
536
|
+
|
537
|
+
def __init__(self) -> None:
|
538
|
+
"""Initialize the dashboard subscriber."""
|
539
|
+
self._subscribers: set[DashboardEventsWebSocket] = set()
|
540
|
+
self._event_loop_task: asyncio.Task | None = None
|
541
|
+
self._refresh_event: asyncio.Event = asyncio.Event()
|
542
|
+
|
543
|
+
def subscribe(self, subscriber: DashboardEventsWebSocket) -> Callable[[], None]:
|
544
|
+
"""Subscribe to dashboard updates and start event loop if needed."""
|
545
|
+
self._subscribers.add(subscriber)
|
546
|
+
if not self._event_loop_task or self._event_loop_task.done():
|
547
|
+
self._event_loop_task = asyncio.create_task(self._event_loop())
|
548
|
+
_LOGGER.info("Started dashboard event loop")
|
549
|
+
return partial(self._unsubscribe, subscriber)
|
550
|
+
|
551
|
+
def _unsubscribe(self, subscriber: DashboardEventsWebSocket) -> None:
|
552
|
+
"""Unsubscribe from dashboard updates and stop event loop if no subscribers."""
|
553
|
+
self._subscribers.discard(subscriber)
|
554
|
+
if (
|
555
|
+
not self._subscribers
|
556
|
+
and self._event_loop_task
|
557
|
+
and not self._event_loop_task.done()
|
558
|
+
):
|
559
|
+
self._event_loop_task.cancel()
|
560
|
+
self._event_loop_task = None
|
561
|
+
_LOGGER.info("Stopped dashboard event loop - no subscribers")
|
562
|
+
|
563
|
+
def request_refresh(self) -> None:
|
564
|
+
"""Signal the polling loop to refresh immediately."""
|
565
|
+
self._refresh_event.set()
|
566
|
+
|
567
|
+
async def _event_loop(self) -> None:
|
568
|
+
"""Run the event polling loop while there are subscribers."""
|
569
|
+
dashboard = DASHBOARD
|
570
|
+
entries_update_counter = 0
|
571
|
+
|
572
|
+
while self._subscribers:
|
573
|
+
# Signal that we need ping updates (non-blocking)
|
574
|
+
dashboard.ping_request.set()
|
575
|
+
if settings.status_use_mqtt:
|
576
|
+
dashboard.mqtt_ping_request.set()
|
577
|
+
|
578
|
+
# Check if it's time to update entries or if refresh was requested
|
579
|
+
entries_update_counter += 1
|
580
|
+
if (
|
581
|
+
entries_update_counter >= DASHBOARD_ENTRIES_UPDATE_ITERATIONS
|
582
|
+
or self._refresh_event.is_set()
|
583
|
+
):
|
584
|
+
entries_update_counter = 0
|
585
|
+
await dashboard.entries.async_request_update_entries()
|
586
|
+
# Clear the refresh event if it was set
|
587
|
+
self._refresh_event.clear()
|
588
|
+
|
589
|
+
# Wait for either timeout or refresh event
|
590
|
+
try:
|
591
|
+
async with asyncio.timeout(DASHBOARD_POLL_INTERVAL):
|
592
|
+
await self._refresh_event.wait()
|
593
|
+
# If we get here, refresh was requested - continue loop immediately
|
594
|
+
except TimeoutError:
|
595
|
+
# Normal timeout - continue with regular polling
|
596
|
+
pass
|
597
|
+
|
598
|
+
|
599
|
+
# Global dashboard subscriber instance
|
600
|
+
DASHBOARD_SUBSCRIBER = DashboardSubscriber()
|
601
|
+
|
602
|
+
|
603
|
+
@websocket_class
|
604
|
+
class DashboardEventsWebSocket(tornado.websocket.WebSocketHandler):
|
605
|
+
"""WebSocket handler for real-time dashboard events."""
|
606
|
+
|
607
|
+
_event_listeners: list[Callable[[], None]] | None = None
|
608
|
+
_dashboard_unsubscribe: Callable[[], None] | None = None
|
609
|
+
|
610
|
+
async def get(self, *args: str, **kwargs: str) -> None:
|
611
|
+
"""Handle WebSocket upgrade request."""
|
612
|
+
if not is_authenticated(self):
|
613
|
+
self.set_status(401)
|
614
|
+
self.finish("Unauthorized")
|
615
|
+
return
|
616
|
+
await super().get(*args, **kwargs)
|
617
|
+
|
618
|
+
async def open(self, *args: str, **kwargs: str) -> None: # pylint: disable=invalid-overridden-method
|
619
|
+
"""Handle new WebSocket connection."""
|
620
|
+
# Ensure messages are sent immediately to avoid
|
621
|
+
# a 200-500ms delay when nodelay is not set.
|
622
|
+
self.set_nodelay(True)
|
623
|
+
|
624
|
+
# Update entries first
|
625
|
+
await DASHBOARD.entries.async_request_update_entries()
|
626
|
+
# Send initial state
|
627
|
+
self._send_initial_state()
|
628
|
+
# Subscribe to events
|
629
|
+
self._subscribe_to_events()
|
630
|
+
# Subscribe to dashboard updates
|
631
|
+
self._dashboard_unsubscribe = DASHBOARD_SUBSCRIBER.subscribe(self)
|
632
|
+
_LOGGER.debug("Dashboard status WebSocket opened")
|
633
|
+
|
634
|
+
def _send_initial_state(self) -> None:
|
635
|
+
"""Send initial device list and ping status."""
|
636
|
+
entries = DASHBOARD.entries.async_all()
|
637
|
+
|
638
|
+
# Send initial state
|
639
|
+
self._safe_send_message(
|
640
|
+
{
|
641
|
+
"event": DashboardEvent.INITIAL_STATE,
|
642
|
+
"data": {
|
643
|
+
"devices": build_device_list_response(DASHBOARD, entries),
|
644
|
+
"ping": {
|
645
|
+
entry.filename: entry_state_to_bool(entry.state)
|
646
|
+
for entry in entries
|
647
|
+
},
|
648
|
+
},
|
649
|
+
}
|
650
|
+
)
|
651
|
+
|
652
|
+
def _subscribe_to_events(self) -> None:
|
653
|
+
"""Subscribe to dashboard events."""
|
654
|
+
async_add_listener = DASHBOARD.bus.async_add_listener
|
655
|
+
# Subscribe to all events
|
656
|
+
self._event_listeners = [
|
657
|
+
async_add_listener(
|
658
|
+
DashboardEvent.ENTRY_STATE_CHANGED, self._on_entry_state_changed
|
659
|
+
),
|
660
|
+
async_add_listener(
|
661
|
+
DashboardEvent.ENTRY_ADDED,
|
662
|
+
self._make_entry_handler(DashboardEvent.ENTRY_ADDED),
|
663
|
+
),
|
664
|
+
async_add_listener(
|
665
|
+
DashboardEvent.ENTRY_REMOVED,
|
666
|
+
self._make_entry_handler(DashboardEvent.ENTRY_REMOVED),
|
667
|
+
),
|
668
|
+
async_add_listener(
|
669
|
+
DashboardEvent.ENTRY_UPDATED,
|
670
|
+
self._make_entry_handler(DashboardEvent.ENTRY_UPDATED),
|
671
|
+
),
|
672
|
+
async_add_listener(
|
673
|
+
DashboardEvent.IMPORTABLE_DEVICE_ADDED, self._on_importable_added
|
674
|
+
),
|
675
|
+
async_add_listener(
|
676
|
+
DashboardEvent.IMPORTABLE_DEVICE_REMOVED,
|
677
|
+
self._on_importable_removed,
|
678
|
+
),
|
679
|
+
]
|
680
|
+
|
681
|
+
def _on_entry_state_changed(self, event: Event) -> None:
|
682
|
+
"""Handle entry state change event."""
|
683
|
+
entry = event.data["entry"]
|
684
|
+
state = event.data["state"]
|
685
|
+
self._safe_send_message(
|
686
|
+
{
|
687
|
+
"event": DashboardEvent.ENTRY_STATE_CHANGED,
|
688
|
+
"data": {
|
689
|
+
"filename": entry.filename,
|
690
|
+
"name": entry.name,
|
691
|
+
"state": entry_state_to_bool(state),
|
692
|
+
},
|
693
|
+
}
|
694
|
+
)
|
695
|
+
|
696
|
+
def _make_entry_handler(
|
697
|
+
self, event_type: DashboardEvent
|
698
|
+
) -> Callable[[Event], None]:
|
699
|
+
"""Create an entry event handler."""
|
700
|
+
|
701
|
+
def handler(event: Event) -> None:
|
702
|
+
self._safe_send_message(
|
703
|
+
{"event": event_type, "data": {"device": event.data["entry"].to_dict()}}
|
704
|
+
)
|
705
|
+
|
706
|
+
return handler
|
707
|
+
|
708
|
+
def _on_importable_added(self, event: Event) -> None:
|
709
|
+
"""Handle importable device added event."""
|
710
|
+
# Don't send if device is already configured
|
711
|
+
device_name = event.data.get("device", {}).get("name")
|
712
|
+
if device_name and DASHBOARD.entries.get_by_name(device_name):
|
713
|
+
return
|
714
|
+
self._safe_send_message(
|
715
|
+
{"event": DashboardEvent.IMPORTABLE_DEVICE_ADDED, "data": event.data}
|
716
|
+
)
|
717
|
+
|
718
|
+
def _on_importable_removed(self, event: Event) -> None:
|
719
|
+
"""Handle importable device removed event."""
|
720
|
+
self._safe_send_message(
|
721
|
+
{"event": DashboardEvent.IMPORTABLE_DEVICE_REMOVED, "data": event.data}
|
722
|
+
)
|
723
|
+
|
724
|
+
def _safe_send_message(self, message: dict[str, Any]) -> None:
|
725
|
+
"""Send a message to the WebSocket client, ignoring closed errors."""
|
726
|
+
with contextlib.suppress(tornado.websocket.WebSocketClosedError):
|
727
|
+
self.write_message(json.dumps(message))
|
728
|
+
|
729
|
+
def on_message(self, message: str) -> None:
|
730
|
+
"""Handle incoming WebSocket messages."""
|
731
|
+
_LOGGER.debug("WebSocket received message: %s", message)
|
732
|
+
try:
|
733
|
+
data = json.loads(message)
|
734
|
+
except json.JSONDecodeError as err:
|
735
|
+
_LOGGER.debug("Failed to parse WebSocket message: %s", err)
|
736
|
+
return
|
737
|
+
|
738
|
+
event = data.get("event")
|
739
|
+
_LOGGER.debug("WebSocket message event: %s", event)
|
740
|
+
if event == DashboardEvent.PING:
|
741
|
+
# Send pong response for client ping
|
742
|
+
_LOGGER.debug("Received client ping, sending pong")
|
743
|
+
self._safe_send_message({"event": DashboardEvent.PONG})
|
744
|
+
elif event == DashboardEvent.REFRESH:
|
745
|
+
# Signal the polling loop to refresh immediately
|
746
|
+
_LOGGER.debug("Received refresh request, signaling polling loop")
|
747
|
+
DASHBOARD_SUBSCRIBER.request_refresh()
|
748
|
+
|
749
|
+
def on_close(self) -> None:
|
750
|
+
"""Handle WebSocket close."""
|
751
|
+
# Unsubscribe from dashboard updates
|
752
|
+
if self._dashboard_unsubscribe:
|
753
|
+
self._dashboard_unsubscribe()
|
754
|
+
self._dashboard_unsubscribe = None
|
755
|
+
|
756
|
+
# Unsubscribe from events
|
757
|
+
for remove_listener in self._event_listeners or []:
|
758
|
+
remove_listener()
|
759
|
+
|
760
|
+
_LOGGER.debug("Dashboard status WebSocket closed")
|
761
|
+
|
762
|
+
|
466
763
|
class SerialPortRequestHandler(BaseHandler):
|
467
764
|
@authenticated
|
468
765
|
async def get(self) -> None:
|
@@ -544,7 +841,7 @@ class WizardRequestHandler(BaseHandler):
|
|
544
841
|
destination = settings.rel_path(filename)
|
545
842
|
|
546
843
|
# Check if destination file already exists
|
547
|
-
if
|
844
|
+
if destination.exists():
|
548
845
|
self.set_status(409) # Conflict status code
|
549
846
|
self.set_header("content-type", "application/json")
|
550
847
|
self.write(
|
@@ -761,10 +1058,9 @@ class DownloadBinaryRequestHandler(BaseHandler):
|
|
761
1058
|
"download",
|
762
1059
|
f"{storage_json.name}-{file_name}",
|
763
1060
|
)
|
764
|
-
path =
|
765
|
-
path = os.path.join(path, file_name)
|
1061
|
+
path = storage_json.firmware_bin_path.with_name(file_name)
|
766
1062
|
|
767
|
-
if not
|
1063
|
+
if not path.is_file():
|
768
1064
|
args = ["esphome", "idedata", settings.rel_path(configuration)]
|
769
1065
|
rc, stdout, _ = await async_run_system_command(args)
|
770
1066
|
|
@@ -818,28 +1114,7 @@ class ListDevicesHandler(BaseHandler):
|
|
818
1114
|
await dashboard.entries.async_request_update_entries()
|
819
1115
|
entries = dashboard.entries.async_all()
|
820
1116
|
self.set_header("content-type", "application/json")
|
821
|
-
|
822
|
-
|
823
|
-
self.write(
|
824
|
-
json.dumps(
|
825
|
-
{
|
826
|
-
"configured": [entry.to_dict() for entry in entries],
|
827
|
-
"importable": [
|
828
|
-
{
|
829
|
-
"name": res.device_name,
|
830
|
-
"friendly_name": res.friendly_name,
|
831
|
-
"package_import_url": res.package_import_url,
|
832
|
-
"project_name": res.project_name,
|
833
|
-
"project_version": res.project_version,
|
834
|
-
"network": res.network,
|
835
|
-
"ignored": res.device_name in dashboard.ignored_devices,
|
836
|
-
}
|
837
|
-
for res in dashboard.import_result.values()
|
838
|
-
if res.device_name not in configured
|
839
|
-
],
|
840
|
-
}
|
841
|
-
)
|
842
|
-
)
|
1117
|
+
self.write(json.dumps(build_device_list_response(dashboard, entries)))
|
843
1118
|
|
844
1119
|
|
845
1120
|
class MainRequestHandler(BaseHandler):
|
@@ -979,7 +1254,7 @@ class EditRequestHandler(BaseHandler):
|
|
979
1254
|
return
|
980
1255
|
|
981
1256
|
filename = settings.rel_path(configuration)
|
982
|
-
if
|
1257
|
+
if filename.resolve().parent != settings.absolute_config_dir:
|
983
1258
|
self.send_error(404)
|
984
1259
|
return
|
985
1260
|
|
@@ -1002,10 +1277,6 @@ class EditRequestHandler(BaseHandler):
|
|
1002
1277
|
self.set_status(404)
|
1003
1278
|
return None
|
1004
1279
|
|
1005
|
-
def _write_file(self, filename: str, content: bytes) -> None:
|
1006
|
-
"""Write a file with the given content."""
|
1007
|
-
write_file(filename, content)
|
1008
|
-
|
1009
1280
|
@authenticated
|
1010
1281
|
@bind_config
|
1011
1282
|
async def post(self, configuration: str | None = None) -> None:
|
@@ -1015,12 +1286,12 @@ class EditRequestHandler(BaseHandler):
|
|
1015
1286
|
return
|
1016
1287
|
|
1017
1288
|
filename = settings.rel_path(configuration)
|
1018
|
-
if
|
1289
|
+
if filename.resolve().parent != settings.absolute_config_dir:
|
1019
1290
|
self.send_error(404)
|
1020
1291
|
return
|
1021
1292
|
|
1022
1293
|
loop = asyncio.get_running_loop()
|
1023
|
-
await loop.run_in_executor(None,
|
1294
|
+
await loop.run_in_executor(None, write_file, filename, self.request.body)
|
1024
1295
|
# Ensure the StorageJSON is updated as well
|
1025
1296
|
DASHBOARD.entries.async_schedule_storage_json_update(filename)
|
1026
1297
|
self.set_status(200)
|
@@ -1035,7 +1306,7 @@ class ArchiveRequestHandler(BaseHandler):
|
|
1035
1306
|
|
1036
1307
|
archive_path = archive_storage_path()
|
1037
1308
|
mkdir_p(archive_path)
|
1038
|
-
shutil.move(config_file,
|
1309
|
+
shutil.move(config_file, archive_path / configuration)
|
1039
1310
|
|
1040
1311
|
storage_json = StorageJSON.load(storage_path)
|
1041
1312
|
if storage_json is not None and storage_json.build_path:
|
@@ -1049,7 +1320,7 @@ class UnArchiveRequestHandler(BaseHandler):
|
|
1049
1320
|
def post(self, configuration: str | None = None) -> None:
|
1050
1321
|
config_file = settings.rel_path(configuration)
|
1051
1322
|
archive_path = archive_storage_path()
|
1052
|
-
shutil.move(
|
1323
|
+
shutil.move(archive_path / configuration, config_file)
|
1053
1324
|
|
1054
1325
|
|
1055
1326
|
class LoginHandler(BaseHandler):
|
@@ -1136,7 +1407,7 @@ class SecretKeysRequestHandler(BaseHandler):
|
|
1136
1407
|
|
1137
1408
|
for secret_filename in const.SECRETS_FILES:
|
1138
1409
|
relative_filename = settings.rel_path(secret_filename)
|
1139
|
-
if
|
1410
|
+
if relative_filename.is_file():
|
1140
1411
|
filename = relative_filename
|
1141
1412
|
break
|
1142
1413
|
|
@@ -1169,16 +1440,17 @@ class JsonConfigRequestHandler(BaseHandler):
|
|
1169
1440
|
@bind_config
|
1170
1441
|
async def get(self, configuration: str | None = None) -> None:
|
1171
1442
|
filename = settings.rel_path(configuration)
|
1172
|
-
if not
|
1443
|
+
if not filename.is_file():
|
1173
1444
|
self.send_error(404)
|
1174
1445
|
return
|
1175
1446
|
|
1176
|
-
args = ["esphome", "config", filename, "--show-secrets"]
|
1447
|
+
args = ["esphome", "config", str(filename), "--show-secrets"]
|
1177
1448
|
|
1178
|
-
rc, stdout,
|
1449
|
+
rc, stdout, stderr = await async_run_system_command(args)
|
1179
1450
|
|
1180
1451
|
if rc != 0:
|
1181
|
-
self.
|
1452
|
+
self.set_status(422)
|
1453
|
+
self.write(stderr)
|
1182
1454
|
return
|
1183
1455
|
|
1184
1456
|
data = yaml.load(stdout, Loader=SafeLoaderIgnoreUnknown)
|
@@ -1187,7 +1459,7 @@ class JsonConfigRequestHandler(BaseHandler):
|
|
1187
1459
|
self.finish()
|
1188
1460
|
|
1189
1461
|
|
1190
|
-
def get_base_frontend_path() ->
|
1462
|
+
def get_base_frontend_path() -> Path:
|
1191
1463
|
if ENV_DEV not in os.environ:
|
1192
1464
|
import esphome_dashboard
|
1193
1465
|
|
@@ -1198,11 +1470,12 @@ def get_base_frontend_path() -> str:
|
|
1198
1470
|
static_path += "/"
|
1199
1471
|
|
1200
1472
|
# This path can be relative, so resolve against the root or else templates don't work
|
1201
|
-
|
1473
|
+
path = Path(os.getcwd()) / static_path / "esphome_dashboard"
|
1474
|
+
return path.resolve()
|
1202
1475
|
|
1203
1476
|
|
1204
|
-
def get_static_path(*args: Iterable[str]) ->
|
1205
|
-
return
|
1477
|
+
def get_static_path(*args: Iterable[str]) -> Path:
|
1478
|
+
return get_base_frontend_path() / "static" / Path(*args)
|
1206
1479
|
|
1207
1480
|
|
1208
1481
|
@functools.cache
|
@@ -1219,8 +1492,7 @@ def get_static_file_url(name: str) -> str:
|
|
1219
1492
|
return base.replace("index.js", esphome_dashboard.entrypoint())
|
1220
1493
|
|
1221
1494
|
path = get_static_path(name)
|
1222
|
-
|
1223
|
-
hash_ = hashlib.md5(f_handle.read()).hexdigest()[:8]
|
1495
|
+
hash_ = hashlib.md5(path.read_bytes()).hexdigest()[:8]
|
1224
1496
|
return f"{base}?hash={hash_}"
|
1225
1497
|
|
1226
1498
|
|
@@ -1280,6 +1552,7 @@ def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application:
|
|
1280
1552
|
(f"{rel}compile", EsphomeCompileHandler),
|
1281
1553
|
(f"{rel}validate", EsphomeValidateHandler),
|
1282
1554
|
(f"{rel}clean-mqtt", EsphomeCleanMqttHandler),
|
1555
|
+
(f"{rel}clean-all", EsphomeCleanAllHandler),
|
1283
1556
|
(f"{rel}clean", EsphomeCleanHandler),
|
1284
1557
|
(f"{rel}vscode", EsphomeVscodeHandler),
|
1285
1558
|
(f"{rel}ace", EsphomeAceEditorHandler),
|
@@ -1297,6 +1570,7 @@ def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application:
|
|
1297
1570
|
(f"{rel}wizard", WizardRequestHandler),
|
1298
1571
|
(f"{rel}static/(.*)", StaticFileHandler, {"path": get_static_path()}),
|
1299
1572
|
(f"{rel}devices", ListDevicesHandler),
|
1573
|
+
(f"{rel}events", DashboardEventsWebSocket),
|
1300
1574
|
(f"{rel}import", ImportRequestHandler),
|
1301
1575
|
(f"{rel}secret_keys", SecretKeysRequestHandler),
|
1302
1576
|
(f"{rel}json-config", JsonConfigRequestHandler),
|
@@ -1320,7 +1594,7 @@ def start_web_server(
|
|
1320
1594
|
"""Start the web server listener."""
|
1321
1595
|
|
1322
1596
|
trash_path = trash_storage_path()
|
1323
|
-
if
|
1597
|
+
if trash_path.is_dir() and trash_path.exists():
|
1324
1598
|
_LOGGER.info("Renaming 'trash' folder to 'archive'")
|
1325
1599
|
archive_path = archive_storage_path()
|
1326
1600
|
shutil.move(trash_path, archive_path)
|