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
@@ -118,7 +118,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
118
118
  item->type = type;
119
119
  item->callback = std::move(func);
120
120
  // Initialize remove to false (though it should already be from constructor)
121
- // Not using mark_item_removed_ helper since we're setting to false, not true
122
121
  #ifdef ESPHOME_THREAD_MULTI_ATOMICS
123
122
  item->remove.store(false, std::memory_order_relaxed);
124
123
  #else
@@ -600,12 +599,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
600
599
  #ifndef ESPHOME_THREAD_SINGLE
601
600
  // Mark items in defer queue as cancelled (they'll be skipped when processed)
602
601
  if (type == SchedulerItem::TIMEOUT) {
603
- for (auto &item : this->defer_queue_) {
604
- if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
605
- this->mark_item_removed_(item.get());
606
- total_cancelled++;
607
- }
608
- }
602
+ total_cancelled += this->mark_matching_items_removed_(this->defer_queue_, component, name_cstr, type, match_retry);
609
603
  }
610
604
  #endif /* not ESPHOME_THREAD_SINGLE */
611
605
 
@@ -620,23 +614,13 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
620
614
  total_cancelled++;
621
615
  }
622
616
  // For other items in heap, we can only mark for removal (can't remove from middle of heap)
623
- for (auto &item : this->items_) {
624
- if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
625
- this->mark_item_removed_(item.get());
626
- total_cancelled++;
627
- this->to_remove_++; // Track removals for heap items
628
- }
629
- }
617
+ size_t heap_cancelled = this->mark_matching_items_removed_(this->items_, component, name_cstr, type, match_retry);
618
+ total_cancelled += heap_cancelled;
619
+ this->to_remove_ += heap_cancelled; // Track removals for heap items
630
620
  }
631
621
 
632
622
  // Cancel items in to_add_
633
- for (auto &item : this->to_add_) {
634
- if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
635
- this->mark_item_removed_(item.get());
636
- total_cancelled++;
637
- // Don't track removals for to_add_ items
638
- }
639
- }
623
+ total_cancelled += this->mark_matching_items_removed_(this->to_add_, component, name_cstr, type, match_retry);
640
624
 
641
625
  return total_cancelled > 0;
642
626
  }
esphome/core/scheduler.h CHANGED
@@ -280,19 +280,30 @@ class Scheduler {
280
280
  #endif
281
281
  }
282
282
 
283
- // Helper to mark item for removal (platform-specific)
283
+ // Helper to mark matching items in a container as removed
284
+ // Returns the number of items marked for removal
284
285
  // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
285
286
  // function.
286
- void mark_item_removed_(SchedulerItem *item) {
287
+ template<typename Container>
288
+ size_t mark_matching_items_removed_(Container &container, Component *component, const char *name_cstr,
289
+ SchedulerItem::Type type, bool match_retry) {
290
+ size_t count = 0;
291
+ for (auto &item : container) {
292
+ if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
293
+ // Mark item for removal (platform-specific)
287
294
  #ifdef ESPHOME_THREAD_MULTI_ATOMICS
288
- // Multi-threaded with atomics: use atomic store
289
- item->remove.store(true, std::memory_order_release);
295
+ // Multi-threaded with atomics: use atomic store
296
+ item->remove.store(true, std::memory_order_release);
290
297
  #else
291
- // Single-threaded (ESPHOME_THREAD_SINGLE) or
292
- // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write
293
- // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock!
294
- item->remove = true;
298
+ // Single-threaded (ESPHOME_THREAD_SINGLE) or
299
+ // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write
300
+ // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock!
301
+ item->remove = true;
295
302
  #endif
303
+ count++;
304
+ }
305
+ }
306
+ return count;
296
307
  }
297
308
 
298
309
  // Template helper to check if any item in a container matches our criteria
esphome/core/string_ref.h CHANGED
@@ -130,7 +130,7 @@ inline std::string operator+(const StringRef &lhs, const char *rhs) {
130
130
 
131
131
  #ifdef USE_JSON
132
132
  // NOLINTNEXTLINE(readability-identifier-naming)
133
- void convertToJson(const StringRef &src, JsonVariant dst);
133
+ inline void convertToJson(const StringRef &src, JsonVariant dst) { dst.set(src.c_str()); }
134
134
  #endif // USE_JSON
135
135
 
136
136
  } // namespace esphome
esphome/core/time.cpp CHANGED
@@ -77,7 +77,7 @@ bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) {
77
77
  &hour, // NOLINT
78
78
  &minute, // NOLINT
79
79
  &second, &num) == 6 && // NOLINT
80
- num == time_to_parse.size()) {
80
+ num == static_cast<int>(time_to_parse.size())) {
81
81
  esp_time.year = year;
82
82
  esp_time.month = month;
83
83
  esp_time.day_of_month = day;
@@ -87,7 +87,7 @@ bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) {
87
87
  } else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT
88
88
  &hour, // NOLINT
89
89
  &minute, &num) == 5 && // NOLINT
90
- num == time_to_parse.size()) {
90
+ num == static_cast<int>(time_to_parse.size())) {
91
91
  esp_time.year = year;
92
92
  esp_time.month = month;
93
93
  esp_time.day_of_month = day;
@@ -95,17 +95,17 @@ bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) {
95
95
  esp_time.minute = minute;
96
96
  esp_time.second = 0;
97
97
  } else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT
98
- num == time_to_parse.size()) {
98
+ num == static_cast<int>(time_to_parse.size())) {
99
99
  esp_time.hour = hour;
100
100
  esp_time.minute = minute;
101
101
  esp_time.second = second;
102
102
  } else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT
103
- num == time_to_parse.size()) {
103
+ num == static_cast<int>(time_to_parse.size())) {
104
104
  esp_time.hour = hour;
105
105
  esp_time.minute = minute;
106
106
  esp_time.second = 0;
107
107
  } else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT
108
- num == time_to_parse.size()) {
108
+ num == static_cast<int>(time_to_parse.size())) {
109
109
  esp_time.year = year;
110
110
  esp_time.month = month;
111
111
  esp_time.day_of_month = day;
esphome/cpp_generator.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import abc
2
- from collections.abc import Callable, Sequence
2
+ from collections.abc import Callable
3
3
  import inspect
4
4
  import math
5
5
  import re
@@ -13,7 +13,6 @@ from esphome.core import (
13
13
  HexInt,
14
14
  Lambda,
15
15
  Library,
16
- TimePeriod,
17
16
  TimePeriodMicroseconds,
18
17
  TimePeriodMilliseconds,
19
18
  TimePeriodMinutes,
@@ -21,35 +20,11 @@ from esphome.core import (
21
20
  TimePeriodSeconds,
22
21
  )
23
22
  from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last
23
+ from esphome.types import Expression, SafeExpType, TemplateArgsType
24
24
  from esphome.util import OrderedDict
25
25
  from esphome.yaml_util import ESPHomeDataBase
26
26
 
27
27
 
28
- class Expression(abc.ABC):
29
- __slots__ = ()
30
-
31
- @abc.abstractmethod
32
- def __str__(self):
33
- """
34
- Convert expression into C++ code
35
- """
36
-
37
-
38
- SafeExpType = (
39
- Expression
40
- | bool
41
- | str
42
- | str
43
- | int
44
- | float
45
- | TimePeriod
46
- | type[bool]
47
- | type[int]
48
- | type[float]
49
- | Sequence[Any]
50
- )
51
-
52
-
53
28
  class RawExpression(Expression):
54
29
  __slots__ = ("text",)
55
30
 
@@ -575,7 +550,7 @@ def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj":
575
550
  return obj
576
551
 
577
552
 
578
- def new_Pvariable(id_: ID, *args: SafeExpType) -> Pvariable:
553
+ def new_Pvariable(id_: ID, *args: SafeExpType) -> "MockObj":
579
554
  """Declare a new pointer variable in the code generation by calling it's constructor
580
555
  with the given arguments.
581
556
 
@@ -681,7 +656,7 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]:
681
656
 
682
657
  async def process_lambda(
683
658
  value: Lambda,
684
- parameters: list[tuple[SafeExpType, str]],
659
+ parameters: TemplateArgsType,
685
660
  capture: str = "=",
686
661
  return_type: SafeExpType = None,
687
662
  ) -> LambdaExpression | None:
@@ -1,9 +1,26 @@
1
1
  from __future__ import annotations
2
2
 
3
- EVENT_ENTRY_ADDED = "entry_added"
4
- EVENT_ENTRY_REMOVED = "entry_removed"
5
- EVENT_ENTRY_UPDATED = "entry_updated"
6
- EVENT_ENTRY_STATE_CHANGED = "entry_state_changed"
3
+ from esphome.enum import StrEnum
4
+
5
+
6
+ class DashboardEvent(StrEnum):
7
+ """Dashboard WebSocket event types."""
8
+
9
+ # Server -> Client events (backend sends to frontend)
10
+ ENTRY_ADDED = "entry_added"
11
+ ENTRY_REMOVED = "entry_removed"
12
+ ENTRY_UPDATED = "entry_updated"
13
+ ENTRY_STATE_CHANGED = "entry_state_changed"
14
+ IMPORTABLE_DEVICE_ADDED = "importable_device_added"
15
+ IMPORTABLE_DEVICE_REMOVED = "importable_device_removed"
16
+ INITIAL_STATE = "initial_state" # Sent on WebSocket connection
17
+ PONG = "pong" # Response to client ping
18
+
19
+ # Client -> Server events (frontend sends to backend)
20
+ PING = "ping" # WebSocket keepalive from client
21
+ REFRESH = "refresh" # Force backend to poll for changes
22
+
23
+
7
24
  MAX_EXECUTOR_WORKERS = 48
8
25
 
9
26
 
esphome/dashboard/core.py CHANGED
@@ -7,13 +7,13 @@ from dataclasses import dataclass
7
7
  from functools import partial
8
8
  import json
9
9
  import logging
10
- from pathlib import Path
11
10
  import threading
12
11
  from typing import Any
13
12
 
14
13
  from esphome.storage_json import ignored_devices_storage_path
15
14
 
16
15
  from ..zeroconf import DiscoveredImport
16
+ from .const import DashboardEvent
17
17
  from .dns import DNSCache
18
18
  from .entries import DashboardEntries
19
19
  from .settings import DashboardSettings
@@ -31,7 +31,7 @@ MDNS_BOOTSTRAP_TIME = 7.5
31
31
  class Event:
32
32
  """Dashboard Event."""
33
33
 
34
- event_type: str
34
+ event_type: DashboardEvent
35
35
  data: dict[str, Any]
36
36
 
37
37
 
@@ -40,22 +40,24 @@ class EventBus:
40
40
 
41
41
  def __init__(self) -> None:
42
42
  """Initialize the Dashboard event bus."""
43
- self._listeners: dict[str, set[Callable[[Event], None]]] = {}
43
+ self._listeners: dict[DashboardEvent, set[Callable[[Event], None]]] = {}
44
44
 
45
45
  def async_add_listener(
46
- self, event_type: str, listener: Callable[[Event], None]
46
+ self, event_type: DashboardEvent, listener: Callable[[Event], None]
47
47
  ) -> Callable[[], None]:
48
48
  """Add a listener to the event bus."""
49
49
  self._listeners.setdefault(event_type, set()).add(listener)
50
50
  return partial(self._async_remove_listener, event_type, listener)
51
51
 
52
52
  def _async_remove_listener(
53
- self, event_type: str, listener: Callable[[Event], None]
53
+ self, event_type: DashboardEvent, listener: Callable[[Event], None]
54
54
  ) -> None:
55
55
  """Remove a listener from the event bus."""
56
56
  self._listeners[event_type].discard(listener)
57
57
 
58
- def async_fire(self, event_type: str, event_data: dict[str, Any]) -> None:
58
+ def async_fire(
59
+ self, event_type: DashboardEvent, event_data: dict[str, Any]
60
+ ) -> None:
59
61
  """Fire an event."""
60
62
  event = Event(event_type, event_data)
61
63
 
@@ -108,7 +110,7 @@ class ESPHomeDashboard:
108
110
  await self.loop.run_in_executor(None, self.load_ignored_devices)
109
111
 
110
112
  def load_ignored_devices(self) -> None:
111
- storage_path = Path(ignored_devices_storage_path())
113
+ storage_path = ignored_devices_storage_path()
112
114
  try:
113
115
  with storage_path.open("r", encoding="utf-8") as f_handle:
114
116
  data = json.load(f_handle)
@@ -117,7 +119,7 @@ class ESPHomeDashboard:
117
119
  pass
118
120
 
119
121
  def save_ignored_devices(self) -> None:
120
- storage_path = Path(ignored_devices_storage_path())
122
+ storage_path = ignored_devices_storage_path()
121
123
  with storage_path.open("w", encoding="utf-8") as f_handle:
122
124
  json.dump(
123
125
  {"ignored_devices": sorted(self.ignored_devices)}, indent=2, fp=f_handle
esphome/dashboard/dns.py CHANGED
@@ -28,6 +28,21 @@ class DNSCache:
28
28
  self._cache: dict[str, tuple[float, list[str] | Exception]] = {}
29
29
  self._ttl = ttl
30
30
 
31
+ def get_cached_addresses(
32
+ self, hostname: str, now_monotonic: float
33
+ ) -> list[str] | None:
34
+ """Get cached addresses without triggering resolution.
35
+
36
+ Returns None if not in cache, list of addresses if found.
37
+ """
38
+ # Normalize hostname for consistent lookups
39
+ normalized = hostname.rstrip(".").lower()
40
+ if expire_time_addresses := self._cache.get(normalized):
41
+ expire_time, addresses = expire_time_addresses
42
+ if expire_time > now_monotonic and not isinstance(addresses, Exception):
43
+ return addresses
44
+ return None
45
+
31
46
  async def async_resolve(
32
47
  self, hostname: str, now_monotonic: float
33
48
  ) -> list[str] | Exception:
@@ -5,20 +5,14 @@ from collections import defaultdict
5
5
  from dataclasses import dataclass
6
6
  from functools import lru_cache
7
7
  import logging
8
- import os
8
+ from pathlib import Path
9
9
  from typing import TYPE_CHECKING, Any
10
10
 
11
11
  from esphome import const, util
12
12
  from esphome.enum import StrEnum
13
13
  from esphome.storage_json import StorageJSON, ext_storage_path
14
14
 
15
- from .const import (
16
- DASHBOARD_COMMAND,
17
- EVENT_ENTRY_ADDED,
18
- EVENT_ENTRY_REMOVED,
19
- EVENT_ENTRY_STATE_CHANGED,
20
- EVENT_ENTRY_UPDATED,
21
- )
15
+ from .const import DASHBOARD_COMMAND, DashboardEvent
22
16
  from .util.subprocess import async_run_system_command
23
17
 
24
18
  if TYPE_CHECKING:
@@ -102,12 +96,12 @@ class DashboardEntries:
102
96
  # "path/to/file.yaml": DashboardEntry,
103
97
  # ...
104
98
  # }
105
- self._entries: dict[str, DashboardEntry] = {}
99
+ self._entries: dict[Path, DashboardEntry] = {}
106
100
  self._loaded_entries = False
107
101
  self._update_lock = asyncio.Lock()
108
102
  self._name_to_entry: dict[str, set[DashboardEntry]] = defaultdict(set)
109
103
 
110
- def get(self, path: str) -> DashboardEntry | None:
104
+ def get(self, path: Path) -> DashboardEntry | None:
111
105
  """Get an entry by path."""
112
106
  return self._entries.get(path)
113
107
 
@@ -192,7 +186,7 @@ class DashboardEntries:
192
186
  return
193
187
  entry.state = state
194
188
  self._dashboard.bus.async_fire(
195
- EVENT_ENTRY_STATE_CHANGED, {"entry": entry, "state": state}
189
+ DashboardEvent.ENTRY_STATE_CHANGED, {"entry": entry, "state": state}
196
190
  )
197
191
 
198
192
  async def async_request_update_entries(self) -> None:
@@ -260,22 +254,22 @@ class DashboardEntries:
260
254
  for entry in added:
261
255
  entries[entry.path] = entry
262
256
  name_to_entry[entry.name].add(entry)
263
- bus.async_fire(EVENT_ENTRY_ADDED, {"entry": entry})
257
+ bus.async_fire(DashboardEvent.ENTRY_ADDED, {"entry": entry})
264
258
 
265
259
  for entry in removed:
266
260
  del entries[entry.path]
267
261
  name_to_entry[entry.name].discard(entry)
268
- bus.async_fire(EVENT_ENTRY_REMOVED, {"entry": entry})
262
+ bus.async_fire(DashboardEvent.ENTRY_REMOVED, {"entry": entry})
269
263
 
270
264
  for entry in updated:
271
265
  if (original_name := original_names[entry]) != (current_name := entry.name):
272
266
  name_to_entry[original_name].discard(entry)
273
267
  name_to_entry[current_name].add(entry)
274
- bus.async_fire(EVENT_ENTRY_UPDATED, {"entry": entry})
268
+ bus.async_fire(DashboardEvent.ENTRY_UPDATED, {"entry": entry})
275
269
 
276
- def _get_path_to_cache_key(self) -> dict[str, DashboardCacheKeyType]:
270
+ def _get_path_to_cache_key(self) -> dict[Path, DashboardCacheKeyType]:
277
271
  """Return a dict of path to cache key."""
278
- path_to_cache_key: dict[str, DashboardCacheKeyType] = {}
272
+ path_to_cache_key: dict[Path, DashboardCacheKeyType] = {}
279
273
  #
280
274
  # The cache key is (inode, device, mtime, size)
281
275
  # which allows us to avoid locking since it ensures
@@ -287,12 +281,12 @@ class DashboardEntries:
287
281
  for file in util.list_yaml_files([self._config_dir]):
288
282
  try:
289
283
  # Prefer the json storage path if it exists
290
- stat = os.stat(ext_storage_path(os.path.basename(file)))
284
+ stat = ext_storage_path(file.name).stat()
291
285
  except OSError:
292
286
  try:
293
287
  # Fallback to the yaml file if the storage
294
288
  # file does not exist or could not be generated
295
- stat = os.stat(file)
289
+ stat = file.stat()
296
290
  except OSError:
297
291
  # File was deleted, ignore
298
292
  continue
@@ -329,10 +323,10 @@ class DashboardEntry:
329
323
  "_to_dict",
330
324
  )
331
325
 
332
- def __init__(self, path: str, cache_key: DashboardCacheKeyType) -> None:
326
+ def __init__(self, path: Path, cache_key: DashboardCacheKeyType) -> None:
333
327
  """Initialize the DashboardEntry."""
334
328
  self.path = path
335
- self.filename: str = os.path.basename(path)
329
+ self.filename: str = path.name
336
330
  self._storage_path = ext_storage_path(self.filename)
337
331
  self.cache_key = cache_key
338
332
  self.storage: StorageJSON | None = None
@@ -365,7 +359,7 @@ class DashboardEntry:
365
359
  "loaded_integrations": sorted(self.loaded_integrations),
366
360
  "deployed_version": self.update_old,
367
361
  "current_version": self.update_new,
368
- "path": self.path,
362
+ "path": str(self.path),
369
363
  "comment": self.comment,
370
364
  "address": self.address,
371
365
  "web_port": self.web_port,
@@ -0,0 +1,76 @@
1
+ """Data models and builders for the dashboard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, TypedDict
6
+
7
+ if TYPE_CHECKING:
8
+ from esphome.zeroconf import DiscoveredImport
9
+
10
+ from .core import ESPHomeDashboard
11
+ from .entries import DashboardEntry
12
+
13
+
14
+ class ImportableDeviceDict(TypedDict):
15
+ """Dictionary representation of an importable device."""
16
+
17
+ name: str
18
+ friendly_name: str | None
19
+ package_import_url: str
20
+ project_name: str
21
+ project_version: str
22
+ network: str
23
+ ignored: bool
24
+
25
+
26
+ class ConfiguredDeviceDict(TypedDict, total=False):
27
+ """Dictionary representation of a configured device."""
28
+
29
+ name: str
30
+ friendly_name: str | None
31
+ configuration: str
32
+ loaded_integrations: list[str] | None
33
+ deployed_version: str | None
34
+ current_version: str | None
35
+ path: str
36
+ comment: str | None
37
+ address: str | None
38
+ web_port: int | None
39
+ target_platform: str | None
40
+
41
+
42
+ class DeviceListResponse(TypedDict):
43
+ """Response for device list API."""
44
+
45
+ configured: list[ConfiguredDeviceDict]
46
+ importable: list[ImportableDeviceDict]
47
+
48
+
49
+ def build_importable_device_dict(
50
+ dashboard: ESPHomeDashboard, discovered: DiscoveredImport
51
+ ) -> ImportableDeviceDict:
52
+ """Build the importable device dictionary."""
53
+ return ImportableDeviceDict(
54
+ name=discovered.device_name,
55
+ friendly_name=discovered.friendly_name,
56
+ package_import_url=discovered.package_import_url,
57
+ project_name=discovered.project_name,
58
+ project_version=discovered.project_version,
59
+ network=discovered.network,
60
+ ignored=discovered.device_name in dashboard.ignored_devices,
61
+ )
62
+
63
+
64
+ def build_device_list_response(
65
+ dashboard: ESPHomeDashboard, entries: list[DashboardEntry]
66
+ ) -> DeviceListResponse:
67
+ """Build the device list response data."""
68
+ configured = {entry.name for entry in entries}
69
+ return DeviceListResponse(
70
+ configured=[entry.to_dict() for entry in entries],
71
+ importable=[
72
+ build_importable_device_dict(dashboard, res)
73
+ for res in dashboard.import_result.values()
74
+ if res.device_name not in configured
75
+ ],
76
+ )
@@ -27,7 +27,7 @@ class DashboardSettings:
27
27
 
28
28
  def __init__(self) -> None:
29
29
  """Initialize the dashboard settings."""
30
- self.config_dir: str = ""
30
+ self.config_dir: Path = None
31
31
  self.password_hash: str = ""
32
32
  self.username: str = ""
33
33
  self.using_password: bool = False
@@ -45,10 +45,10 @@ class DashboardSettings:
45
45
  self.using_password = bool(password)
46
46
  if self.using_password:
47
47
  self.password_hash = password_hash(password)
48
- self.config_dir = args.configuration
49
- self.absolute_config_dir = Path(self.config_dir).resolve()
48
+ self.config_dir = Path(args.configuration)
49
+ self.absolute_config_dir = self.config_dir.resolve()
50
50
  self.verbose = args.verbose
51
- CORE.config_path = os.path.join(self.config_dir, ".")
51
+ CORE.config_path = self.config_dir / "."
52
52
 
53
53
  @property
54
54
  def relative_url(self) -> str:
@@ -81,9 +81,9 @@ class DashboardSettings:
81
81
  # Compare password in constant running time (to prevent timing attacks)
82
82
  return hmac.compare_digest(self.password_hash, password_hash(password))
83
83
 
84
- def rel_path(self, *args: Any) -> str:
84
+ def rel_path(self, *args: Any) -> Path:
85
85
  """Return a path relative to the ESPHome config folder."""
86
- joined_path = os.path.join(self.config_dir, *args)
86
+ joined_path = self.config_dir / Path(*args)
87
87
  # Raises ValueError if not relative to ESPHome config folder
88
- Path(joined_path).resolve().relative_to(self.absolute_config_dir)
88
+ joined_path.resolve().relative_to(self.absolute_config_dir)
89
89
  return joined_path
@@ -4,16 +4,21 @@ import asyncio
4
4
  import logging
5
5
  import typing
6
6
 
7
+ from zeroconf import AddressResolver, IPVersion
8
+
9
+ from esphome.address_cache import normalize_hostname
7
10
  from esphome.zeroconf import (
8
11
  ESPHOME_SERVICE_TYPE,
9
12
  AsyncEsphomeZeroconf,
10
13
  DashboardBrowser,
11
14
  DashboardImportDiscovery,
12
15
  DashboardStatus,
16
+ DiscoveredImport,
13
17
  )
14
18
 
15
- from ..const import SENTINEL
19
+ from ..const import SENTINEL, DashboardEvent
16
20
  from ..entries import DashboardEntry, EntryStateSource, bool_to_entry_state
21
+ from ..models import build_importable_device_dict
17
22
 
18
23
  if typing.TYPE_CHECKING:
19
24
  from ..core import ESPHomeDashboard
@@ -50,6 +55,44 @@ class MDNSStatus:
50
55
  return await aiozc.async_resolve_host(host_name)
51
56
  return None
52
57
 
58
+ def get_cached_addresses(self, host_name: str) -> list[str] | None:
59
+ """Get cached addresses for a host without triggering resolution.
60
+
61
+ Returns None if not in cache or no zeroconf available.
62
+ """
63
+ if not self.aiozc:
64
+ _LOGGER.debug("No zeroconf instance available for %s", host_name)
65
+ return None
66
+
67
+ # Normalize hostname and get the base name
68
+ normalized = normalize_hostname(host_name)
69
+ base_name = normalized.partition(".")[0]
70
+
71
+ # Try to load from zeroconf cache without triggering resolution
72
+ resolver_name = f"{base_name}.local."
73
+ info = AddressResolver(resolver_name)
74
+ # Let zeroconf use its own current time for cache checking
75
+ if info.load_from_cache(self.aiozc.zeroconf):
76
+ addresses = info.parsed_scoped_addresses(IPVersion.All)
77
+ _LOGGER.debug("Found %s in zeroconf cache: %s", resolver_name, addresses)
78
+ return addresses
79
+ _LOGGER.debug("Not found in zeroconf cache: %s", resolver_name)
80
+ return None
81
+
82
+ def _on_import_update(self, name: str, discovered: DiscoveredImport | None) -> None:
83
+ """Handle importable device updates."""
84
+ if discovered is None:
85
+ # Device removed
86
+ self.dashboard.bus.async_fire(
87
+ DashboardEvent.IMPORTABLE_DEVICE_REMOVED, {"name": name}
88
+ )
89
+ else:
90
+ # Device added
91
+ self.dashboard.bus.async_fire(
92
+ DashboardEvent.IMPORTABLE_DEVICE_ADDED,
93
+ {"device": build_importable_device_dict(self.dashboard, discovered)},
94
+ )
95
+
53
96
  async def async_refresh_hosts(self) -> None:
54
97
  """Refresh the hosts to track."""
55
98
  dashboard = self.dashboard
@@ -106,7 +149,8 @@ class MDNSStatus:
106
149
  self._async_set_state(entry, result)
107
150
 
108
151
  stat = DashboardStatus(on_update)
109
- imports = DashboardImportDiscovery()
152
+
153
+ imports = DashboardImportDiscovery(self._on_import_update)
110
154
  dashboard.import_result = imports.import_state
111
155
 
112
156
  browser = DashboardBrowser(