diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index d158528066..ee18315518 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -84,6 +84,23 @@ def get_board(core_obj=None): return (core_obj or CORE).data[KEY_ESP32][KEY_BOARD] +def get_download_types(storage_json): + return [ + { + "title": "Modern format", + "description": "For use with ESPHome Web and other tools.", + "file": "firmware-factory.bin", + "download": f"{storage_json.name}-factory.bin", + }, + { + "title": "Legacy format", + "description": "For use with ESPHome Flasher.", + "file": "firmware.bin", + "download": f"{storage_json.name}.bin", + }, + ] + + def only_on_variant(*, supported=None, unsupported=None): """Config validator for features only available on some ESP32 variants.""" if supported is not None and not isinstance(supported, list): diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 674f433d52..412c2d903f 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -50,6 +50,17 @@ def set_core_data(config): return config +def get_download_types(storage_json): + return [ + { + "title": "Standard format", + "description": "For flashing ESP8266.", + "file": "firmware.bin", + "download": f"{storage_json.name}.bin", + }, + ] + + def _format_framework_arduino_version(ver: cv.Version) -> str: # format the given arduino (https://github.com/esp8266/Arduino/releases) version to # a PIO platformio/framework-arduinoespressif8266 value diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 9e9db02de1..b31192f73f 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -42,6 +42,17 @@ def set_core_data(config): return config +def get_download_types(storage_json): + return [ + { + "title": "UF2 format", + "description": "For copying to RP2040 over USB.", + "file": "firmware.uf2", + "download": f"{storage_json.name}.uf2", + }, + ] + + def _format_framework_arduino_version(ver: cv.Version) -> str: # The most recent releases have not been uploaded to platformio so grabbing them directly from # the GitHub release is one path forward for now. diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index eae004fa09..c05e1fcfcc 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -530,11 +530,43 @@ class ImportRequestHandler(BaseHandler): self.finish() +class DownloadListRequestHandler(BaseHandler): + @authenticated + @bind_config + def get(self, configuration=None): + storage_path = ext_storage_path(settings.config_dir, configuration) + storage_json = StorageJSON.load(storage_path) + if storage_json is None: + self.send_error(404) + return + + from esphome.components.esp32 import get_download_types as esp32_types + from esphome.components.esp8266 import get_download_types as esp8266_types + from esphome.components.rp2040 import get_download_types as rp2040_types + + downloads = [] + platform = storage_json.target_platform.lower() + if platform == const.PLATFORM_RP2040: + downloads = rp2040_types(storage_json) + elif platform == const.PLATFORM_ESP8266: + downloads = esp8266_types(storage_json) + elif platform == const.PLATFORM_ESP32: + downloads = esp32_types(storage_json) + else: + self.send_error(418) + return + + self.set_status(200) + self.set_header("content-type", "application/json") + self.write(json.dumps(downloads)) + self.finish() + return + + class DownloadBinaryRequestHandler(BaseHandler): @authenticated @bind_config def get(self, configuration=None): - type = self.get_argument("type", "firmware.bin") compressed = self.get_argument("compressed", "0") == "1" storage_path = ext_storage_path(settings.config_dir, configuration) @@ -543,27 +575,22 @@ class DownloadBinaryRequestHandler(BaseHandler): self.send_error(404) return - if storage_json.target_platform.lower() == const.PLATFORM_RP2040: - filename = f"{storage_json.name}.uf2" - path = storage_json.firmware_bin_path.replace( - "firmware.bin", "firmware.uf2" - ) + # fallback to type=, but prioritize file= + file_name = self.get_argument("type", None) + file_name = self.get_argument("file", file_name) + if file_name is None: + self.send_error(400) + return + file_name = file_name.replace("..", "").lstrip("/") + # get requested download name, or build it based on filename + download_name = self.get_argument( + "download", + f"{storage_json.name}-{file_name}", + ) + path = os.path.dirname(storage_json.firmware_bin_path) + path = os.path.join(path, file_name) - elif storage_json.target_platform.lower() == const.PLATFORM_ESP8266: - filename = f"{storage_json.name}.bin" - path = storage_json.firmware_bin_path - - elif type == "firmware.bin": - filename = f"{storage_json.name}.bin" - path = storage_json.firmware_bin_path - - elif type == "firmware-factory.bin": - filename = f"{storage_json.name}-factory.bin" - path = storage_json.firmware_bin_path.replace( - "firmware.bin", "firmware-factory.bin" - ) - - else: + if not Path(path).is_file(): args = ["esphome", "idedata", settings.rel_path(configuration)] rc, stdout, _ = run_system_command(*args) @@ -575,9 +602,9 @@ class DownloadBinaryRequestHandler(BaseHandler): found = False for image in idedata.extra_flash_images: - if image.path.endswith(type): + if image.path.endswith(file_name): path = image.path - filename = type + download_name = file_name found = True break @@ -585,10 +612,12 @@ class DownloadBinaryRequestHandler(BaseHandler): self.send_error(404) return - filename = filename + ".gz" if compressed else filename + download_name = download_name + ".gz" if compressed else download_name self.set_header("Content-Type", "application/octet-stream") - self.set_header("Content-Disposition", f'attachment; filename="{filename}"') + self.set_header( + "Content-Disposition", f'attachment; filename="{download_name}"' + ) self.set_header("Cache-Control", "no-cache") if not Path(path).is_file(): self.send_error(404) @@ -1259,6 +1288,7 @@ def make_app(debug=get_bool_env(ENV_DEV)): (f"{rel}update-all", EsphomeUpdateAllHandler), (f"{rel}info", InfoRequestHandler), (f"{rel}edit", EditRequestHandler), + (f"{rel}downloads", DownloadListRequestHandler), (f"{rel}download.bin", DownloadBinaryRequestHandler), (f"{rel}serial-ports", SerialPortRequestHandler), (f"{rel}ping", PingRequestHandler),