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/espota2.py
CHANGED
@@ -1,19 +1,23 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
from collections.abc import Callable
|
3
4
|
import gzip
|
4
5
|
import hashlib
|
5
6
|
import io
|
6
7
|
import logging
|
8
|
+
from pathlib import Path
|
7
9
|
import random
|
8
10
|
import socket
|
9
11
|
import sys
|
10
12
|
import time
|
13
|
+
from typing import Any
|
11
14
|
|
12
15
|
from esphome.core import EsphomeError
|
13
16
|
from esphome.helpers import resolve_ip_address
|
14
17
|
|
15
18
|
RESPONSE_OK = 0x00
|
16
19
|
RESPONSE_REQUEST_AUTH = 0x01
|
20
|
+
RESPONSE_REQUEST_SHA256_AUTH = 0x02
|
17
21
|
|
18
22
|
RESPONSE_HEADER_OK = 0x40
|
19
23
|
RESPONSE_AUTH_OK = 0x41
|
@@ -44,6 +48,7 @@ OTA_VERSION_2_0 = 2
|
|
44
48
|
MAGIC_BYTES = [0x6C, 0x26, 0xF7, 0x5C, 0x45]
|
45
49
|
|
46
50
|
FEATURE_SUPPORTS_COMPRESSION = 0x01
|
51
|
+
FEATURE_SUPPORTS_SHA256_AUTH = 0x02
|
47
52
|
|
48
53
|
|
49
54
|
UPLOAD_BLOCK_SIZE = 8192
|
@@ -51,6 +56,12 @@ UPLOAD_BUFFER_SIZE = UPLOAD_BLOCK_SIZE * 8
|
|
51
56
|
|
52
57
|
_LOGGER = logging.getLogger(__name__)
|
53
58
|
|
59
|
+
# Authentication method lookup table: response -> (hash_func, nonce_size, name)
|
60
|
+
_AUTH_METHODS: dict[int, tuple[Callable[..., Any], int, str]] = {
|
61
|
+
RESPONSE_REQUEST_SHA256_AUTH: (hashlib.sha256, 64, "SHA256"),
|
62
|
+
RESPONSE_REQUEST_AUTH: (hashlib.md5, 32, "MD5"),
|
63
|
+
}
|
64
|
+
|
54
65
|
|
55
66
|
class ProgressBar:
|
56
67
|
def __init__(self):
|
@@ -80,18 +91,43 @@ class OTAError(EsphomeError):
|
|
80
91
|
pass
|
81
92
|
|
82
93
|
|
83
|
-
def recv_decode(
|
94
|
+
def recv_decode(
|
95
|
+
sock: socket.socket, amount: int, decode: bool = True
|
96
|
+
) -> bytes | list[int]:
|
97
|
+
"""Receive data from socket and optionally decode to list of integers.
|
98
|
+
|
99
|
+
:param sock: Socket to receive data from.
|
100
|
+
:param amount: Number of bytes to receive.
|
101
|
+
:param decode: If True, convert bytes to list of integers, otherwise return raw bytes.
|
102
|
+
:return: List of integers if decode=True, otherwise raw bytes.
|
103
|
+
"""
|
84
104
|
data = sock.recv(amount)
|
85
105
|
if not decode:
|
86
106
|
return data
|
87
107
|
return list(data)
|
88
108
|
|
89
109
|
|
90
|
-
def receive_exactly(
|
91
|
-
|
110
|
+
def receive_exactly(
|
111
|
+
sock: socket.socket,
|
112
|
+
amount: int,
|
113
|
+
msg: str,
|
114
|
+
expect: int | list[int] | None,
|
115
|
+
decode: bool = True,
|
116
|
+
) -> list[int] | bytes:
|
117
|
+
"""Receive exactly the specified amount of data from socket with error checking.
|
118
|
+
|
119
|
+
:param sock: Socket to receive data from.
|
120
|
+
:param amount: Exact number of bytes to receive.
|
121
|
+
:param msg: Description of what is being received for error messages.
|
122
|
+
:param expect: Expected response code(s) for validation, None to skip validation.
|
123
|
+
:param decode: If True, return list of integers, otherwise return raw bytes.
|
124
|
+
:return: List of integers if decode=True, otherwise raw bytes.
|
125
|
+
:raises OTAError: If receiving fails or response doesn't match expected.
|
126
|
+
"""
|
127
|
+
data: list[int] | bytes = [] if decode else b""
|
92
128
|
|
93
129
|
try:
|
94
|
-
data += recv_decode(sock, 1, decode=decode)
|
130
|
+
data += recv_decode(sock, 1, decode=decode) # type: ignore[operator]
|
95
131
|
except OSError as err:
|
96
132
|
raise OTAError(f"Error receiving acknowledge {msg}: {err}") from err
|
97
133
|
|
@@ -103,13 +139,19 @@ def receive_exactly(sock, amount, msg, expect, decode=True):
|
|
103
139
|
|
104
140
|
while len(data) < amount:
|
105
141
|
try:
|
106
|
-
data += recv_decode(sock, amount - len(data), decode=decode)
|
142
|
+
data += recv_decode(sock, amount - len(data), decode=decode) # type: ignore[operator]
|
107
143
|
except OSError as err:
|
108
144
|
raise OTAError(f"Error receiving {msg}: {err}") from err
|
109
145
|
return data
|
110
146
|
|
111
147
|
|
112
|
-
def check_error(data, expect):
|
148
|
+
def check_error(data: list[int] | bytes, expect: int | list[int] | None) -> None:
|
149
|
+
"""Check response data for error codes and validate against expected response.
|
150
|
+
|
151
|
+
:param data: Response data from device (first byte is the response code).
|
152
|
+
:param expect: Expected response code(s), None to skip validation.
|
153
|
+
:raises OTAError: If an error code is detected or response doesn't match expected.
|
154
|
+
"""
|
113
155
|
if not expect:
|
114
156
|
return
|
115
157
|
dat = data[0]
|
@@ -124,7 +166,7 @@ def check_error(data, expect):
|
|
124
166
|
raise OTAError("Error: Authentication invalid. Is the password correct?")
|
125
167
|
if dat == RESPONSE_ERROR_WRITING_FLASH:
|
126
168
|
raise OTAError(
|
127
|
-
"Error:
|
169
|
+
"Error: Writing OTA data to flash memory failed. See USB logs for more "
|
128
170
|
"information."
|
129
171
|
)
|
130
172
|
if dat == RESPONSE_ERROR_UPDATE_END:
|
@@ -176,7 +218,16 @@ def check_error(data, expect):
|
|
176
218
|
raise OTAError(f"Unexpected response from ESP: 0x{data[0]:02X}")
|
177
219
|
|
178
220
|
|
179
|
-
def send_check(
|
221
|
+
def send_check(
|
222
|
+
sock: socket.socket, data: list[int] | tuple[int, ...] | int | str | bytes, msg: str
|
223
|
+
) -> None:
|
224
|
+
"""Send data to socket with error handling.
|
225
|
+
|
226
|
+
:param sock: Socket to send data to.
|
227
|
+
:param data: Data to send (can be list/tuple of ints, single int, string, or bytes).
|
228
|
+
:param msg: Description of what is being sent for error messages.
|
229
|
+
:raises OTAError: If sending fails.
|
230
|
+
"""
|
180
231
|
try:
|
181
232
|
if isinstance(data, (list, tuple)):
|
182
233
|
data = bytes(data)
|
@@ -191,7 +242,7 @@ def send_check(sock, data, msg):
|
|
191
242
|
|
192
243
|
|
193
244
|
def perform_ota(
|
194
|
-
sock: socket.socket, password: str, file_handle: io.IOBase, filename:
|
245
|
+
sock: socket.socket, password: str, file_handle: io.IOBase, filename: Path
|
195
246
|
) -> None:
|
196
247
|
file_contents = file_handle.read()
|
197
248
|
file_size = len(file_contents)
|
@@ -209,10 +260,14 @@ def perform_ota(
|
|
209
260
|
f"Device uses unsupported OTA version {version}, this ESPHome supports {supported_versions}"
|
210
261
|
)
|
211
262
|
|
212
|
-
# Features
|
213
|
-
|
263
|
+
# Features - send both compression and SHA256 auth support
|
264
|
+
features_to_send = FEATURE_SUPPORTS_COMPRESSION | FEATURE_SUPPORTS_SHA256_AUTH
|
265
|
+
send_check(sock, features_to_send, "features")
|
214
266
|
features = receive_exactly(
|
215
|
-
sock,
|
267
|
+
sock,
|
268
|
+
1,
|
269
|
+
"features",
|
270
|
+
None, # Accept any response
|
216
271
|
)[0]
|
217
272
|
|
218
273
|
if features == RESPONSE_SUPPORTS_COMPRESSION:
|
@@ -221,31 +276,52 @@ def perform_ota(
|
|
221
276
|
else:
|
222
277
|
upload_contents = file_contents
|
223
278
|
|
224
|
-
|
225
|
-
sock
|
226
|
-
|
227
|
-
|
279
|
+
def perform_auth(
|
280
|
+
sock: socket.socket,
|
281
|
+
password: str,
|
282
|
+
hash_func: Callable[..., Any],
|
283
|
+
nonce_size: int,
|
284
|
+
hash_name: str,
|
285
|
+
) -> None:
|
286
|
+
"""Perform challenge-response authentication using specified hash algorithm."""
|
228
287
|
if not password:
|
229
288
|
raise OTAError("ESP requests password, but no password given!")
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
289
|
+
|
290
|
+
nonce_bytes = receive_exactly(
|
291
|
+
sock, nonce_size, f"{hash_name} authentication nonce", [], decode=False
|
292
|
+
)
|
293
|
+
assert isinstance(nonce_bytes, bytes)
|
294
|
+
nonce = nonce_bytes.decode()
|
295
|
+
_LOGGER.debug("Auth: %s Nonce is %s", hash_name, nonce)
|
296
|
+
|
297
|
+
# Generate cnonce
|
298
|
+
cnonce = hash_func(str(random.random()).encode()).hexdigest()
|
299
|
+
_LOGGER.debug("Auth: %s CNonce is %s", hash_name, cnonce)
|
236
300
|
|
237
301
|
send_check(sock, cnonce, "auth cnonce")
|
238
302
|
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
303
|
+
# Calculate challenge response
|
304
|
+
hasher = hash_func()
|
305
|
+
hasher.update(password.encode("utf-8"))
|
306
|
+
hasher.update(nonce.encode())
|
307
|
+
hasher.update(cnonce.encode())
|
308
|
+
result = hasher.hexdigest()
|
309
|
+
_LOGGER.debug("Auth: %s Result is %s", hash_name, result)
|
245
310
|
|
246
311
|
send_check(sock, result, "auth result")
|
247
312
|
receive_exactly(sock, 1, "auth result", RESPONSE_AUTH_OK)
|
248
313
|
|
314
|
+
(auth,) = receive_exactly(
|
315
|
+
sock,
|
316
|
+
1,
|
317
|
+
"auth",
|
318
|
+
[RESPONSE_REQUEST_AUTH, RESPONSE_REQUEST_SHA256_AUTH, RESPONSE_AUTH_OK],
|
319
|
+
)
|
320
|
+
|
321
|
+
if auth != RESPONSE_AUTH_OK:
|
322
|
+
hash_func, nonce_size, hash_name = _AUTH_METHODS[auth]
|
323
|
+
perform_auth(sock, password, hash_func, nonce_size, hash_name)
|
324
|
+
|
249
325
|
# Set higher timeout during upload
|
250
326
|
sock.settimeout(30.0)
|
251
327
|
|
@@ -309,12 +385,16 @@ def perform_ota(
|
|
309
385
|
|
310
386
|
|
311
387
|
def run_ota_impl_(
|
312
|
-
remote_host: str | list[str], remote_port: int, password: str, filename:
|
388
|
+
remote_host: str | list[str], remote_port: int, password: str, filename: Path
|
313
389
|
) -> tuple[int, str | None]:
|
390
|
+
from esphome.core import CORE
|
391
|
+
|
314
392
|
# Handle both single host and list of hosts
|
315
393
|
try:
|
316
394
|
# Resolve all hosts at once for parallel DNS resolution
|
317
|
-
res = resolve_ip_address(
|
395
|
+
res = resolve_ip_address(
|
396
|
+
remote_host, remote_port, address_cache=CORE.address_cache
|
397
|
+
)
|
318
398
|
except EsphomeError as err:
|
319
399
|
_LOGGER.error(
|
320
400
|
"Error resolving IP address of %s. Is it connected to WiFi?",
|
@@ -356,7 +436,7 @@ def run_ota_impl_(
|
|
356
436
|
|
357
437
|
|
358
438
|
def run_ota(
|
359
|
-
remote_host: str | list[str], remote_port: int, password: str, filename:
|
439
|
+
remote_host: str | list[str], remote_port: int, password: str, filename: Path
|
360
440
|
) -> tuple[int, str | None]:
|
361
441
|
try:
|
362
442
|
return run_ota_impl_(remote_host, remote_port, password, filename)
|
esphome/external_files.py
CHANGED
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
from datetime import datetime
|
4
4
|
import logging
|
5
|
-
import os
|
6
5
|
from pathlib import Path
|
7
6
|
|
8
7
|
import requests
|
@@ -23,11 +22,11 @@ CONTENT_DISPOSITION = "content-disposition"
|
|
23
22
|
TEMP_DIR = "temp"
|
24
23
|
|
25
24
|
|
26
|
-
def has_remote_file_changed(url, local_file_path):
|
27
|
-
if
|
25
|
+
def has_remote_file_changed(url: str, local_file_path: Path) -> bool:
|
26
|
+
if local_file_path.exists():
|
28
27
|
_LOGGER.debug("has_remote_file_changed: File exists at %s", local_file_path)
|
29
28
|
try:
|
30
|
-
local_modification_time =
|
29
|
+
local_modification_time = local_file_path.stat().st_mtime
|
31
30
|
local_modification_time_str = datetime.utcfromtimestamp(
|
32
31
|
local_modification_time
|
33
32
|
).strftime("%a, %d %b %Y %H:%M:%S GMT")
|
@@ -65,9 +64,9 @@ def has_remote_file_changed(url, local_file_path):
|
|
65
64
|
return True
|
66
65
|
|
67
66
|
|
68
|
-
def is_file_recent(file_path:
|
69
|
-
if
|
70
|
-
creation_time =
|
67
|
+
def is_file_recent(file_path: Path, refresh: TimePeriodSeconds) -> bool:
|
68
|
+
if file_path.exists():
|
69
|
+
creation_time = file_path.stat().st_ctime
|
71
70
|
current_time = datetime.now().timestamp()
|
72
71
|
return current_time - creation_time <= refresh.total_seconds
|
73
72
|
return False
|
esphome/git.py
CHANGED
@@ -13,6 +13,9 @@ from esphome.core import CORE, TimePeriodSeconds
|
|
13
13
|
|
14
14
|
_LOGGER = logging.getLogger(__name__)
|
15
15
|
|
16
|
+
# Special value to indicate never refresh
|
17
|
+
NEVER_REFRESH = TimePeriodSeconds(seconds=-1)
|
18
|
+
|
16
19
|
|
17
20
|
def run_git_command(cmd, cwd=None) -> str:
|
18
21
|
_LOGGER.debug("Running git command: %s", " ".join(cmd))
|
@@ -85,6 +88,11 @@ def clone_or_update(
|
|
85
88
|
|
86
89
|
else:
|
87
90
|
# Check refresh needed
|
91
|
+
# Skip refresh if NEVER_REFRESH is specified
|
92
|
+
if refresh == NEVER_REFRESH:
|
93
|
+
_LOGGER.debug("Skipping update for %s (refresh disabled)", key)
|
94
|
+
return repo_dir, None
|
95
|
+
|
88
96
|
file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD")
|
89
97
|
# On first clone, FETCH_HEAD does not exists
|
90
98
|
if not file_timestamp.exists():
|
esphome/helpers.py
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
-
import codecs
|
4
3
|
from contextlib import suppress
|
5
4
|
import ipaddress
|
6
5
|
import logging
|
@@ -8,11 +7,16 @@ import os
|
|
8
7
|
from pathlib import Path
|
9
8
|
import platform
|
10
9
|
import re
|
10
|
+
import shutil
|
11
11
|
import tempfile
|
12
|
+
from typing import TYPE_CHECKING
|
12
13
|
from urllib.parse import urlparse
|
13
14
|
|
14
15
|
from esphome.const import __version__ as ESPHOME_VERSION
|
15
16
|
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
from esphome.address_cache import AddressCache
|
19
|
+
|
16
20
|
# Type aliases for socket address information
|
17
21
|
AddrInfo = tuple[
|
18
22
|
int, # family (AF_INET, AF_INET6, etc.)
|
@@ -136,16 +140,16 @@ def run_system_command(*args):
|
|
136
140
|
return rc, stdout, stderr
|
137
141
|
|
138
142
|
|
139
|
-
def mkdir_p(path):
|
143
|
+
def mkdir_p(path: Path):
|
140
144
|
if not path:
|
141
145
|
# Empty path - means create current dir
|
142
146
|
return
|
143
147
|
try:
|
144
|
-
|
148
|
+
path.mkdir(parents=True, exist_ok=True)
|
145
149
|
except OSError as err:
|
146
150
|
import errno
|
147
151
|
|
148
|
-
if err.errno == errno.EEXIST and
|
152
|
+
if err.errno == errno.EEXIST and path.is_dir():
|
149
153
|
pass
|
150
154
|
else:
|
151
155
|
from esphome.core import EsphomeError
|
@@ -173,7 +177,24 @@ def addr_preference_(res: AddrInfo) -> int:
|
|
173
177
|
return 1
|
174
178
|
|
175
179
|
|
176
|
-
def
|
180
|
+
def _add_ip_addresses_to_addrinfo(
|
181
|
+
addresses: list[str], port: int, res: list[AddrInfo]
|
182
|
+
) -> None:
|
183
|
+
"""Helper to add IP addresses to addrinfo results with error handling."""
|
184
|
+
import socket
|
185
|
+
|
186
|
+
for addr in addresses:
|
187
|
+
try:
|
188
|
+
res += socket.getaddrinfo(
|
189
|
+
addr, port, proto=socket.IPPROTO_TCP, flags=socket.AI_NUMERICHOST
|
190
|
+
)
|
191
|
+
except OSError:
|
192
|
+
_LOGGER.debug("Failed to parse IP address '%s'", addr)
|
193
|
+
|
194
|
+
|
195
|
+
def resolve_ip_address(
|
196
|
+
host: str | list[str], port: int, address_cache: AddressCache | None = None
|
197
|
+
) -> list[AddrInfo]:
|
177
198
|
import socket
|
178
199
|
|
179
200
|
# There are five cases here. The host argument could be one of:
|
@@ -194,47 +215,69 @@ def resolve_ip_address(host: str | list[str], port: int) -> list[AddrInfo]:
|
|
194
215
|
hosts = [host]
|
195
216
|
|
196
217
|
res: list[AddrInfo] = []
|
218
|
+
|
219
|
+
# Fast path: if all hosts are already IP addresses
|
197
220
|
if all(is_ip_address(h) for h in hosts):
|
198
|
-
|
199
|
-
for addr in hosts:
|
200
|
-
try:
|
201
|
-
res += socket.getaddrinfo(
|
202
|
-
addr, port, proto=socket.IPPROTO_TCP, flags=socket.AI_NUMERICHOST
|
203
|
-
)
|
204
|
-
except OSError:
|
205
|
-
_LOGGER.debug("Failed to parse IP address '%s'", addr)
|
221
|
+
_add_ip_addresses_to_addrinfo(hosts, port, res)
|
206
222
|
# Sort by preference
|
207
223
|
res.sort(key=addr_preference_)
|
208
224
|
return res
|
209
225
|
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
for
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
226
|
+
# Process hosts
|
227
|
+
cached_addresses: list[str] = []
|
228
|
+
uncached_hosts: list[str] = []
|
229
|
+
has_cache = address_cache is not None
|
230
|
+
|
231
|
+
for h in hosts:
|
232
|
+
if is_ip_address(h):
|
233
|
+
if has_cache:
|
234
|
+
# If we have a cache, treat IPs as cached
|
235
|
+
cached_addresses.append(h)
|
236
|
+
else:
|
237
|
+
# If no cache, pass IPs through to resolver with hostnames
|
238
|
+
uncached_hosts.append(h)
|
239
|
+
elif address_cache and (cached := address_cache.get_addresses(h)):
|
240
|
+
# Found in cache
|
241
|
+
cached_addresses.extend(cached)
|
225
242
|
else:
|
226
|
-
#
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
243
|
+
# Not cached, need to resolve
|
244
|
+
if address_cache and address_cache.has_cache():
|
245
|
+
_LOGGER.info("Host %s not in cache, will need to resolve", h)
|
246
|
+
uncached_hosts.append(h)
|
247
|
+
|
248
|
+
# Process cached addresses (includes direct IPs and cached lookups)
|
249
|
+
_add_ip_addresses_to_addrinfo(cached_addresses, port, res)
|
250
|
+
|
251
|
+
# If we have uncached hosts (only non-IP hostnames), resolve them
|
252
|
+
if uncached_hosts:
|
253
|
+
from esphome.resolver import AsyncResolver
|
254
|
+
|
255
|
+
resolver = AsyncResolver(uncached_hosts, port)
|
256
|
+
addr_infos = resolver.resolve()
|
257
|
+
# Convert aioesphomeapi AddrInfo to our format
|
258
|
+
for addr_info in addr_infos:
|
259
|
+
sockaddr = addr_info.sockaddr
|
260
|
+
if addr_info.family == socket.AF_INET6:
|
261
|
+
# IPv6
|
262
|
+
sockaddr_tuple = (
|
263
|
+
sockaddr.address,
|
264
|
+
sockaddr.port,
|
265
|
+
sockaddr.flowinfo,
|
266
|
+
sockaddr.scope_id,
|
267
|
+
)
|
268
|
+
else:
|
269
|
+
# IPv4
|
270
|
+
sockaddr_tuple = (sockaddr.address, sockaddr.port)
|
271
|
+
|
272
|
+
res.append(
|
273
|
+
(
|
274
|
+
addr_info.family,
|
275
|
+
addr_info.type,
|
276
|
+
addr_info.proto,
|
277
|
+
"", # canonname
|
278
|
+
sockaddr_tuple,
|
279
|
+
)
|
236
280
|
)
|
237
|
-
)
|
238
281
|
|
239
282
|
# Sort by preference
|
240
283
|
res.sort(key=addr_preference_)
|
@@ -256,14 +299,7 @@ def sort_ip_addresses(address_list: list[str]) -> list[str]:
|
|
256
299
|
# First "resolve" all the IP addresses to getaddrinfo() tuples of the form
|
257
300
|
# (family, type, proto, canonname, sockaddr)
|
258
301
|
res: list[AddrInfo] = []
|
259
|
-
|
260
|
-
# This should always work as these are supposed to be IP addresses
|
261
|
-
try:
|
262
|
-
res += socket.getaddrinfo(
|
263
|
-
addr, 0, proto=socket.IPPROTO_TCP, flags=socket.AI_NUMERICHOST
|
264
|
-
)
|
265
|
-
except OSError:
|
266
|
-
_LOGGER.info("Failed to parse IP address '%s'", addr)
|
302
|
+
_add_ip_addresses_to_addrinfo(address_list, 0, res)
|
267
303
|
|
268
304
|
# Now use that information to sort them.
|
269
305
|
res.sort(key=addr_preference_)
|
@@ -295,16 +331,15 @@ def is_ha_addon():
|
|
295
331
|
return get_bool_env("ESPHOME_IS_HA_ADDON")
|
296
332
|
|
297
333
|
|
298
|
-
def walk_files(path):
|
334
|
+
def walk_files(path: Path):
|
299
335
|
for root, _, files in os.walk(path):
|
300
336
|
for name in files:
|
301
|
-
yield
|
337
|
+
yield Path(root) / name
|
302
338
|
|
303
339
|
|
304
|
-
def read_file(path):
|
340
|
+
def read_file(path: Path) -> str:
|
305
341
|
try:
|
306
|
-
|
307
|
-
return f_handle.read()
|
342
|
+
return path.read_text(encoding="utf-8")
|
308
343
|
except OSError as err:
|
309
344
|
from esphome.core import EsphomeError
|
310
345
|
|
@@ -315,13 +350,15 @@ def read_file(path):
|
|
315
350
|
raise EsphomeError(f"Error reading file {path}: {err}") from err
|
316
351
|
|
317
352
|
|
318
|
-
def _write_file(
|
353
|
+
def _write_file(
|
354
|
+
path: Path,
|
355
|
+
text: str | bytes,
|
356
|
+
private: bool = False,
|
357
|
+
) -> None:
|
319
358
|
"""Atomically writes `text` to the given path.
|
320
359
|
|
321
360
|
Automatically creates all parent directories.
|
322
361
|
"""
|
323
|
-
if not isinstance(path, Path):
|
324
|
-
path = Path(path)
|
325
362
|
data = text
|
326
363
|
if isinstance(text, str):
|
327
364
|
data = text.encode()
|
@@ -329,42 +366,54 @@ def _write_file(path: Path | str, text: str | bytes):
|
|
329
366
|
directory = path.parent
|
330
367
|
directory.mkdir(exist_ok=True, parents=True)
|
331
368
|
|
332
|
-
|
369
|
+
tmp_filename: Path | None = None
|
370
|
+
missing_fchmod = False
|
333
371
|
try:
|
372
|
+
# Modern versions of Python tempfile create this file with mode 0o600
|
334
373
|
with tempfile.NamedTemporaryFile(
|
335
374
|
mode="wb", dir=directory, delete=False
|
336
375
|
) as f_handle:
|
337
|
-
tmp_path = f_handle.name
|
338
376
|
f_handle.write(data)
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
377
|
+
tmp_filename = Path(f_handle.name)
|
378
|
+
|
379
|
+
if not private:
|
380
|
+
try:
|
381
|
+
os.fchmod(f_handle.fileno(), 0o644)
|
382
|
+
except AttributeError:
|
383
|
+
# os.fchmod is not available on Windows
|
384
|
+
missing_fchmod = True
|
385
|
+
shutil.move(tmp_filename, path)
|
386
|
+
if missing_fchmod:
|
387
|
+
path.chmod(0o644)
|
343
388
|
finally:
|
344
|
-
if
|
389
|
+
if tmp_filename and tmp_filename.exists():
|
345
390
|
try:
|
346
|
-
|
391
|
+
tmp_filename.unlink()
|
347
392
|
except OSError as err:
|
348
|
-
|
393
|
+
# If we are cleaning up then something else went wrong, so
|
394
|
+
# we should suppress likely follow-on errors in the cleanup
|
395
|
+
_LOGGER.error(
|
396
|
+
"File replacement cleanup failed for %s while saving %s: %s",
|
397
|
+
tmp_filename,
|
398
|
+
path,
|
399
|
+
err,
|
400
|
+
)
|
349
401
|
|
350
402
|
|
351
|
-
def write_file(path: Path |
|
403
|
+
def write_file(path: Path, text: str | bytes, private: bool = False) -> None:
|
352
404
|
try:
|
353
|
-
_write_file(path, text)
|
405
|
+
_write_file(path, text, private=private)
|
354
406
|
except OSError as err:
|
355
407
|
from esphome.core import EsphomeError
|
356
408
|
|
357
409
|
raise EsphomeError(f"Could not write file at {path}") from err
|
358
410
|
|
359
411
|
|
360
|
-
def write_file_if_changed(path: Path
|
412
|
+
def write_file_if_changed(path: Path, text: str) -> bool:
|
361
413
|
"""Write text to the given path, but not if the contents match already.
|
362
414
|
|
363
415
|
Returns true if the file was changed.
|
364
416
|
"""
|
365
|
-
if not isinstance(path, Path):
|
366
|
-
path = Path(path)
|
367
|
-
|
368
417
|
src_content = None
|
369
418
|
if path.is_file():
|
370
419
|
src_content = read_file(path)
|
@@ -374,12 +423,10 @@ def write_file_if_changed(path: Path | str, text: str) -> bool:
|
|
374
423
|
return True
|
375
424
|
|
376
425
|
|
377
|
-
def copy_file_if_changed(src:
|
378
|
-
import shutil
|
379
|
-
|
426
|
+
def copy_file_if_changed(src: Path, dst: Path) -> None:
|
380
427
|
if file_compare(src, dst):
|
381
428
|
return
|
382
|
-
|
429
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
383
430
|
try:
|
384
431
|
shutil.copyfile(src, dst)
|
385
432
|
except OSError as err:
|
@@ -404,12 +451,12 @@ def list_starts_with(list_, sub):
|
|
404
451
|
return len(sub) <= len(list_) and all(list_[i] == x for i, x in enumerate(sub))
|
405
452
|
|
406
453
|
|
407
|
-
def file_compare(path1:
|
454
|
+
def file_compare(path1: Path, path2: Path) -> bool:
|
408
455
|
"""Return True if the files path1 and path2 have the same contents."""
|
409
456
|
import stat
|
410
457
|
|
411
458
|
try:
|
412
|
-
stat1, stat2 =
|
459
|
+
stat1, stat2 = path1.stat(), path2.stat()
|
413
460
|
except OSError:
|
414
461
|
# File doesn't exist or another error -> not equal
|
415
462
|
return False
|
@@ -426,7 +473,7 @@ def file_compare(path1: os.PathLike, path2: os.PathLike) -> bool:
|
|
426
473
|
|
427
474
|
bufsize = 8 * 1024
|
428
475
|
# Read files in blocks until a mismatch is found
|
429
|
-
with open(
|
476
|
+
with path1.open("rb") as fh1, path2.open("rb") as fh2:
|
430
477
|
while True:
|
431
478
|
blob1, blob2 = fh1.read(bufsize), fh2.read(bufsize)
|
432
479
|
if blob1 != blob2:
|