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.
Files changed (343) hide show
  1. esphome/__main__.py +87 -31
  2. esphome/address_cache.py +142 -0
  3. esphome/automation.py +130 -32
  4. esphome/build_gen/platformio.py +1 -3
  5. esphome/codegen.py +1 -0
  6. esphome/components/animation/animation.cpp +2 -2
  7. esphome/components/api/__init__.py +166 -3
  8. esphome/components/api/api_connection.cpp +84 -41
  9. esphome/components/api/api_connection.h +22 -16
  10. esphome/components/api/api_frame_helper.cpp +33 -19
  11. esphome/components/api/api_frame_helper.h +19 -4
  12. esphome/components/api/api_frame_helper_noise.cpp +41 -53
  13. esphome/components/api/api_frame_helper_noise.h +1 -1
  14. esphome/components/api/api_frame_helper_plaintext.cpp +22 -31
  15. esphome/components/api/api_frame_helper_plaintext.h +1 -1
  16. esphome/components/api/api_pb2.cpp +189 -15
  17. esphome/components/api/api_pb2.h +132 -20
  18. esphome/components/api/api_pb2_dump.cpp +97 -9
  19. esphome/components/api/api_pb2_service.cpp +118 -160
  20. esphome/components/api/api_pb2_service.h +31 -3
  21. esphome/components/api/api_server.cpp +68 -10
  22. esphome/components/api/api_server.h +32 -4
  23. esphome/components/api/custom_api_device.h +8 -8
  24. esphome/components/api/homeassistant_service.h +123 -6
  25. esphome/components/api/proto.h +6 -2
  26. esphome/components/api/user_services.h +2 -2
  27. esphome/components/as7341/sensor.py +1 -1
  28. esphome/components/audio/__init__.py +1 -1
  29. esphome/components/audio/audio.cpp +1 -1
  30. esphome/components/audio/audio_decoder.cpp +9 -9
  31. esphome/components/bl0906/bl0906.cpp +2 -2
  32. esphome/components/bl0942/bl0942.cpp +2 -2
  33. esphome/components/ble_client/__init__.py +1 -1
  34. esphome/components/bluetooth_proxy/__init__.py +4 -30
  35. esphome/components/bluetooth_proxy/bluetooth_connection.cpp +11 -4
  36. esphome/components/bluetooth_proxy/bluetooth_connection.h +2 -2
  37. esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +2 -2
  38. esphome/components/camera_encoder/__init__.py +2 -4
  39. esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp +4 -2
  40. esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h +3 -1
  41. esphome/components/canbus/canbus.cpp +7 -5
  42. esphome/components/canbus/canbus.h +4 -4
  43. esphome/components/captive_portal/__init__.py +18 -1
  44. esphome/components/captive_portal/captive_portal.cpp +40 -46
  45. esphome/components/captive_portal/captive_portal.h +20 -22
  46. esphome/components/captive_portal/dns_server_esp32_idf.cpp +205 -0
  47. esphome/components/captive_portal/dns_server_esp32_idf.h +27 -0
  48. esphome/components/ccs811/ccs811.cpp +1 -1
  49. esphome/components/climate/climate.cpp +10 -7
  50. esphome/components/cm1106/cm1106.cpp +1 -1
  51. esphome/components/copy/lock/copy_lock.cpp +1 -1
  52. esphome/components/cover/cover.cpp +1 -0
  53. esphome/components/daikin_arc/daikin_arc.cpp +19 -12
  54. esphome/components/deep_sleep/__init__.py +9 -2
  55. esphome/components/deep_sleep/deep_sleep_component.h +11 -9
  56. esphome/components/deep_sleep/deep_sleep_esp32.cpp +51 -27
  57. esphome/components/ektf2232/touchscreen/__init__.py +8 -5
  58. esphome/components/ektf2232/touchscreen/ektf2232.cpp +4 -4
  59. esphome/components/ektf2232/touchscreen/ektf2232.h +2 -2
  60. esphome/components/epaper_spi/__init__.py +1 -0
  61. esphome/components/epaper_spi/display.py +80 -0
  62. esphome/components/epaper_spi/epaper_spi.cpp +227 -0
  63. esphome/components/epaper_spi/epaper_spi.h +93 -0
  64. esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.cpp +42 -0
  65. esphome/components/epaper_spi/epaper_spi_model_7p3in_spectra_e6.h +45 -0
  66. esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp +135 -0
  67. esphome/components/epaper_spi/epaper_spi_spectra_e6.h +23 -0
  68. esphome/components/es7210/es7210.cpp +3 -3
  69. esphome/components/esp32/__init__.py +254 -339
  70. esphome/components/esp32/boards.py +81 -0
  71. esphome/components/esp32/preferences.cpp +23 -17
  72. esphome/components/esp32_ble/__init__.py +159 -44
  73. esphome/components/esp32_ble/ble.cpp +47 -3
  74. esphome/components/esp32_ble/ble.h +18 -0
  75. esphome/components/esp32_ble/ble_advertising.cpp +7 -3
  76. esphome/components/esp32_ble/ble_advertising.h +4 -0
  77. esphome/components/esp32_ble/ble_uuid.cpp +16 -42
  78. esphome/components/esp32_ble_beacon/__init__.py +3 -4
  79. esphome/components/esp32_ble_client/ble_client_base.cpp +14 -12
  80. esphome/components/esp32_ble_server/__init__.py +28 -14
  81. esphome/components/esp32_ble_server/ble_characteristic.cpp +67 -57
  82. esphome/components/esp32_ble_server/ble_characteristic.h +27 -16
  83. esphome/components/esp32_ble_server/ble_descriptor.cpp +4 -3
  84. esphome/components/esp32_ble_server/ble_descriptor.h +13 -9
  85. esphome/components/esp32_ble_server/ble_server.cpp +59 -24
  86. esphome/components/esp32_ble_server/ble_server.h +38 -20
  87. esphome/components/esp32_ble_server/ble_server_automations.cpp +49 -33
  88. esphome/components/esp32_ble_server/ble_server_automations.h +39 -24
  89. esphome/components/esp32_ble_tracker/__init__.py +25 -80
  90. esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +2 -4
  91. esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +0 -3
  92. esphome/components/esp32_camera/__init__.py +1 -3
  93. esphome/components/esp32_can/esp32_can.cpp +22 -4
  94. esphome/components/esp32_can/esp32_can.h +3 -0
  95. esphome/components/esp32_hosted/__init__.py +2 -1
  96. esphome/components/esp32_improv/esp32_improv_component.cpp +102 -44
  97. esphome/components/esp32_improv/esp32_improv_component.h +6 -1
  98. esphome/components/esp32_rmt_led_strip/led_strip.cpp +1 -1
  99. esphome/components/esp8266/__init__.py +3 -3
  100. esphome/components/esphome/ota/__init__.py +21 -2
  101. esphome/components/esphome/ota/ota_esphome.cpp +455 -145
  102. esphome/components/esphome/ota/ota_esphome.h +49 -2
  103. esphome/components/ethernet/__init__.py +39 -22
  104. esphome/components/ethernet/ethernet_component.cpp +28 -5
  105. esphome/components/ethernet/ethernet_component.h +5 -1
  106. esphome/components/external_components/__init__.py +8 -6
  107. esphome/components/fingerprint_grow/fingerprint_grow.cpp +1 -1
  108. esphome/components/fingerprint_grow/fingerprint_grow.h +2 -1
  109. esphome/components/font/__init__.py +5 -5
  110. esphome/components/graph/graph.cpp +1 -1
  111. esphome/components/graphical_display_menu/graphical_display_menu.cpp +3 -2
  112. esphome/components/haier/hon_climate.cpp +2 -2
  113. esphome/components/haier/hon_climate.h +1 -1
  114. esphome/components/hdc1080/hdc1080.cpp +42 -34
  115. esphome/components/hdc1080/hdc1080.h +1 -3
  116. esphome/components/homeassistant/number/homeassistant_number.cpp +2 -2
  117. esphome/components/homeassistant/switch/homeassistant_switch.cpp +2 -2
  118. esphome/components/http_request/__init__.py +3 -3
  119. esphome/components/htu21d/htu21d.cpp +13 -18
  120. esphome/components/htu21d/htu21d.h +1 -1
  121. esphome/components/i2s_audio/__init__.py +1 -2
  122. esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +1 -1
  123. esphome/components/ili9xxx/ili9xxx_display.cpp +2 -2
  124. esphome/components/improv_serial/improv_serial_component.cpp +12 -15
  125. esphome/components/improv_serial/improv_serial_component.h +6 -8
  126. esphome/components/json/json_util.cpp +35 -43
  127. esphome/components/json/json_util.h +57 -0
  128. esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +2 -2
  129. esphome/components/key_collector/key_collector.h +4 -4
  130. esphome/components/libretiny/__init__.py +6 -6
  131. esphome/components/libretiny/preferences.cpp +23 -16
  132. esphome/components/light/light_call.cpp +98 -120
  133. esphome/components/light/light_call.h +17 -7
  134. esphome/components/lm75b/__init__.py +0 -0
  135. esphome/components/lm75b/lm75b.cpp +39 -0
  136. esphome/components/lm75b/lm75b.h +19 -0
  137. esphome/components/lm75b/sensor.py +34 -0
  138. esphome/components/lock/lock.h +12 -6
  139. esphome/components/logger/__init__.py +15 -27
  140. esphome/components/logger/logger.cpp +10 -20
  141. esphome/components/logger/logger.h +105 -62
  142. esphome/components/logger/logger_esp32.cpp +0 -48
  143. esphome/components/logger/logger_zephyr.cpp +2 -3
  144. esphome/components/logger/select/logger_level_select.cpp +6 -7
  145. esphome/components/logger/select/logger_level_select.h +7 -0
  146. esphome/components/ltr501/ltr501.cpp +7 -6
  147. esphome/components/ltr_als_ps/ltr_als_ps.cpp +7 -6
  148. esphome/components/matrix_keypad/matrix_keypad.h +4 -4
  149. esphome/components/max7219digit/max7219digit.cpp +1 -1
  150. esphome/components/mcp2515/mcp2515.cpp +31 -3
  151. esphome/components/mcp2515/mcp2515_defs.h +3 -1
  152. esphome/components/md5/md5.cpp +0 -26
  153. esphome/components/md5/md5.h +10 -20
  154. esphome/components/mdns/__init__.py +19 -6
  155. esphome/components/mdns/mdns_component.cpp +27 -59
  156. esphome/components/mdns/mdns_component.h +23 -10
  157. esphome/components/mdns/mdns_esp32.cpp +7 -7
  158. esphome/components/mdns/mdns_esp8266.cpp +6 -6
  159. esphome/components/mdns/mdns_libretiny.cpp +3 -3
  160. esphome/components/mdns/mdns_rp2040.cpp +3 -3
  161. esphome/components/mipi/__init__.py +1 -5
  162. esphome/components/mipi_spi/display.py +24 -8
  163. esphome/components/mipi_spi/mipi_spi.h +3 -3
  164. esphome/components/mixer/speaker/mixer_speaker.cpp +3 -3
  165. esphome/components/mmc5603/mmc5603.cpp +3 -3
  166. esphome/components/modbus/modbus.cpp +27 -13
  167. esphome/components/modbus/modbus.h +5 -3
  168. esphome/components/modbus/modbus_definitions.h +86 -0
  169. esphome/components/modbus_controller/__init__.py +29 -1
  170. esphome/components/modbus_controller/const.py +4 -0
  171. esphome/components/modbus_controller/modbus_controller.cpp +38 -13
  172. esphome/components/modbus_controller/modbus_controller.h +18 -29
  173. esphome/components/mpr121/mpr121.cpp +41 -42
  174. esphome/components/mpr121/mpr121.h +0 -1
  175. esphome/components/nau7802/nau7802.cpp +2 -2
  176. esphome/components/network/__init__.py +7 -3
  177. esphome/components/nextion/display.py +4 -4
  178. esphome/components/nextion/nextion.cpp +8 -8
  179. esphome/components/number/__init__.py +2 -0
  180. esphome/components/number/number_call.cpp +23 -12
  181. esphome/components/number/number_call.h +5 -0
  182. esphome/components/online_image/bmp_image.cpp +2 -1
  183. esphome/components/online_image/jpeg_image.cpp +4 -2
  184. esphome/components/openthread/openthread.cpp +6 -7
  185. esphome/components/openthread/openthread.h +0 -1
  186. esphome/components/ota/ota_backend.h +1 -0
  187. esphome/components/packages/__init__.py +10 -8
  188. esphome/components/packet_transport/packet_transport.cpp +2 -0
  189. esphome/components/pid/pid_controller.cpp +1 -1
  190. esphome/components/prometheus/prometheus_handler.cpp +239 -239
  191. esphome/components/psram/__init__.py +30 -28
  192. esphome/components/qmc5883l/qmc5883l.cpp +15 -0
  193. esphome/components/qmc5883l/qmc5883l.h +3 -0
  194. esphome/components/qmc5883l/sensor.py +31 -12
  195. esphome/components/remote_base/gobox_protocol.cpp +3 -3
  196. esphome/components/remote_receiver/__init__.py +14 -2
  197. esphome/components/remote_receiver/{remote_receiver_esp8266.cpp → remote_receiver.cpp} +2 -2
  198. esphome/components/remote_receiver/remote_receiver.h +4 -0
  199. esphome/components/remote_receiver/remote_receiver_esp32.cpp +18 -1
  200. esphome/components/remote_transmitter/__init__.py +2 -2
  201. esphome/components/remote_transmitter/remote_transmitter.cpp +103 -0
  202. esphome/components/rp2040/__init__.py +11 -11
  203. esphome/components/rtttl/rtttl.cpp +2 -2
  204. esphome/components/scd30/sensor.py +1 -1
  205. esphome/components/script/__init__.py +1 -1
  206. esphome/components/script/script.h +7 -7
  207. esphome/components/select/select.cpp +5 -4
  208. esphome/components/select/select_call.cpp +1 -1
  209. esphome/components/sensirion_common/i2c_sensirion.cpp +2 -1
  210. esphome/components/sensor/__init__.py +2 -0
  211. esphome/components/sha256/__init__.py +22 -0
  212. esphome/components/sha256/sha256.cpp +116 -0
  213. esphome/components/sha256/sha256.h +60 -0
  214. esphome/components/socket/lwip_raw_tcp_impl.cpp +34 -6
  215. esphome/components/sonoff_d1/sonoff_d1.cpp +1 -1
  216. esphome/components/spi/__init__.py +0 -3
  217. esphome/components/split_buffer/__init__.py +5 -0
  218. esphome/components/split_buffer/split_buffer.cpp +133 -0
  219. esphome/components/split_buffer/split_buffer.h +40 -0
  220. esphome/components/sps30/sps30.cpp +14 -10
  221. esphome/components/sps30/sps30.h +2 -0
  222. esphome/components/st7567_i2c/st7567_i2c.cpp +3 -1
  223. esphome/components/st7789v/st7789v.cpp +3 -2
  224. esphome/components/statsd/statsd.cpp +1 -1
  225. esphome/components/substitutions/__init__.py +3 -1
  226. esphome/components/substitutions/jinja.py +13 -3
  227. esphome/components/sx126x/__init__.py +16 -0
  228. esphome/components/sx126x/sx126x.cpp +15 -1
  229. esphome/components/sx126x/sx126x.h +9 -1
  230. esphome/components/sx126x/sx126x_reg.h +2 -0
  231. esphome/components/text_sensor/text_sensor.cpp +16 -0
  232. esphome/components/text_sensor/text_sensor.h +3 -10
  233. esphome/components/tormatic/tormatic_cover.cpp +1 -1
  234. esphome/components/tuya/select/tuya_select.cpp +1 -1
  235. esphome/components/tuya/tuya.cpp +29 -4
  236. esphome/components/uart/__init__.py +36 -26
  237. esphome/components/uart/uart.h +6 -0
  238. esphome/components/uart/uart_component.cpp +8 -0
  239. esphome/components/uart/uart_component.h +28 -0
  240. esphome/components/uart/uart_component_esp_idf.cpp +64 -10
  241. esphome/components/uart/uart_component_esp_idf.h +5 -2
  242. esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp +1 -1
  243. esphome/components/uponor_smatrix/sensor/uponor_smatrix_sensor.cpp +1 -1
  244. esphome/components/uponor_smatrix/uponor_smatrix.cpp +3 -3
  245. esphome/components/usb_host/__init__.py +2 -1
  246. esphome/components/usb_host/usb_host.h +82 -13
  247. esphome/components/usb_host/usb_host_client.cpp +180 -24
  248. esphome/components/usb_host/usb_host_component.cpp +1 -1
  249. esphome/components/usb_uart/__init__.py +0 -1
  250. esphome/components/usb_uart/ch34x.cpp +4 -4
  251. esphome/components/usb_uart/cp210x.cpp +3 -3
  252. esphome/components/usb_uart/usb_uart.cpp +88 -32
  253. esphome/components/usb_uart/usb_uart.h +30 -6
  254. esphome/components/valve/valve.cpp +1 -0
  255. esphome/components/veml7700/veml7700.cpp +7 -6
  256. esphome/components/version/version_text_sensor.cpp +2 -1
  257. esphome/components/voice_assistant/voice_assistant.cpp +3 -2
  258. esphome/components/waveshare_epaper/waveshare_epaper.cpp +4 -4
  259. esphome/components/web_server/list_entities.cpp +3 -4
  260. esphome/components/web_server/list_entities.h +8 -10
  261. esphome/components/web_server/ota/__init__.py +1 -1
  262. esphome/components/web_server/ota/ota_web_server.cpp +9 -3
  263. esphome/components/web_server/web_server.cpp +509 -404
  264. esphome/components/web_server/web_server.h +5 -6
  265. esphome/components/web_server/web_server_v1.cpp +21 -19
  266. esphome/components/web_server_base/__init__.py +5 -2
  267. esphome/components/web_server_base/web_server_base.h +27 -7
  268. esphome/components/web_server_idf/__init__.py +1 -1
  269. esphome/components/web_server_idf/multipart.cpp +2 -2
  270. esphome/components/web_server_idf/multipart.h +2 -2
  271. esphome/components/web_server_idf/utils.cpp +2 -2
  272. esphome/components/web_server_idf/utils.h +2 -2
  273. esphome/components/web_server_idf/web_server_idf.cpp +118 -26
  274. esphome/components/web_server_idf/web_server_idf.h +12 -10
  275. esphome/components/wifi/__init__.py +13 -11
  276. esphome/components/wifi/wifi_component.cpp +73 -56
  277. esphome/components/wifi/wifi_component.h +4 -4
  278. esphome/components/wifi/wifi_component_esp8266.cpp +1 -1
  279. esphome/components/wifi/wifi_component_esp_idf.cpp +24 -4
  280. esphome/components/wireguard/__init__.py +1 -1
  281. esphome/components/wts01/__init__.py +0 -0
  282. esphome/components/wts01/sensor.py +41 -0
  283. esphome/components/wts01/wts01.cpp +91 -0
  284. esphome/components/wts01/wts01.h +27 -0
  285. esphome/components/zephyr/__init__.py +5 -5
  286. esphome/components/zwave_proxy/__init__.py +43 -0
  287. esphome/components/zwave_proxy/zwave_proxy.cpp +346 -0
  288. esphome/components/zwave_proxy/zwave_proxy.h +93 -0
  289. esphome/config.py +79 -24
  290. esphome/config_validation.py +13 -15
  291. esphome/const.py +9 -2
  292. esphome/core/__init__.py +31 -22
  293. esphome/core/component.cpp +28 -18
  294. esphome/core/component_iterator.h +2 -1
  295. esphome/core/config.py +15 -15
  296. esphome/core/defines.h +19 -0
  297. esphome/core/hash_base.h +56 -0
  298. esphome/core/helpers.cpp +19 -3
  299. esphome/core/helpers.h +26 -0
  300. esphome/core/scheduler.cpp +5 -21
  301. esphome/core/scheduler.h +19 -8
  302. esphome/core/string_ref.h +1 -1
  303. esphome/core/time.cpp +5 -5
  304. esphome/cpp_generator.py +4 -29
  305. esphome/dashboard/const.py +21 -4
  306. esphome/dashboard/core.py +10 -8
  307. esphome/dashboard/dns.py +15 -0
  308. esphome/dashboard/entries.py +15 -21
  309. esphome/dashboard/models.py +76 -0
  310. esphome/dashboard/settings.py +7 -7
  311. esphome/dashboard/status/mdns.py +46 -2
  312. esphome/dashboard/web_server.py +367 -93
  313. esphome/espota2.py +111 -31
  314. esphome/external_files.py +6 -7
  315. esphome/git.py +8 -0
  316. esphome/helpers.py +124 -77
  317. esphome/loader.py +8 -9
  318. esphome/platformio_api.py +25 -18
  319. esphome/storage_json.py +26 -21
  320. esphome/types.py +30 -2
  321. esphome/util.py +32 -16
  322. esphome/vscode.py +8 -8
  323. esphome/wizard.py +10 -10
  324. esphome/writer.py +50 -15
  325. esphome/yaml_util.py +37 -31
  326. esphome/zeroconf.py +12 -3
  327. {esphome-2025.9.3.dist-info → esphome-2025.10.0b1.dist-info}/METADATA +11 -11
  328. {esphome-2025.9.3.dist-info → esphome-2025.10.0b1.dist-info}/RECORD +332 -312
  329. esphome/components/event_emitter/__init__.py +0 -5
  330. esphome/components/event_emitter/event_emitter.cpp +0 -14
  331. esphome/components/event_emitter/event_emitter.h +0 -63
  332. esphome/components/remote_receiver/remote_receiver_libretiny.cpp +0 -125
  333. esphome/components/remote_transmitter/remote_transmitter_esp8266.cpp +0 -107
  334. esphome/components/remote_transmitter/remote_transmitter_libretiny.cpp +0 -110
  335. esphome/components/uart/uart_component_esp32_arduino.cpp +0 -214
  336. esphome/components/uart/uart_component_esp32_arduino.h +0 -60
  337. esphome/components/wifi/wifi_component_esp32_arduino.cpp +0 -860
  338. esphome/core/string_ref.cpp +0 -12
  339. esphome/dashboard/util/file.py +0 -63
  340. {esphome-2025.9.3.dist-info → esphome-2025.10.0b1.dist-info}/WHEEL +0 -0
  341. {esphome-2025.9.3.dist-info → esphome-2025.10.0b1.dist-info}/entry_points.txt +0 -0
  342. {esphome-2025.9.3.dist-info → esphome-2025.10.0b1.dist-info}/licenses/LICENSE +0 -0
  343. {esphome-2025.9.3.dist-info → esphome-2025.10.0b1.dist-info}/top_level.txt +0 -0
@@ -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 .const import DASHBOARD_COMMAND
53
- from .core import DASHBOARD
54
- from .entries import UNKNOWN_STATE, entry_state_to_bool
55
- from .util.file import write_file
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.readline()
292
+ data = self._proc.stdout.read(1)
288
293
  if data:
289
- data = data.replace(b"\r", b"")
290
- self._queue.put_nowait(data)
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
- addresses: list[str] = []
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
- # First priority: entry.address AKA use_address
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
- return [*DASHBOARD_COMMAND, *args, config_file, *device_args]
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 os.path.exists(destination):
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 = os.path.dirname(storage_json.firmware_bin_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 Path(path).is_file():
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
- configured = {entry.name for entry in entries}
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 Path(filename).resolve().parent != settings.absolute_config_dir:
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 Path(filename).resolve().parent != settings.absolute_config_dir:
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, self._write_file, filename, self.request.body)
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, os.path.join(archive_path, configuration))
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(os.path.join(archive_path, configuration), config_file)
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 os.path.isfile(relative_filename):
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 os.path.isfile(filename):
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, _ = await async_run_system_command(args)
1449
+ rc, stdout, stderr = await async_run_system_command(args)
1179
1450
 
1180
1451
  if rc != 0:
1181
- self.send_error(422)
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() -> str:
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
- return os.path.abspath(os.path.join(os.getcwd(), static_path, "esphome_dashboard"))
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]) -> str:
1205
- return os.path.join(get_base_frontend_path(), "static", *args)
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
- with open(path, "rb") as f_handle:
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 os.path.exists(trash_path):
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)