mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Add dashboard API to get firmware binaries (#4675)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							f8a03be2f1
						
					
				
				
					commit
					c3332e4a39
				
			| @@ -84,6 +84,23 @@ def get_board(core_obj=None): | |||||||
|     return (core_obj or CORE).data[KEY_ESP32][KEY_BOARD] |     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): | def only_on_variant(*, supported=None, unsupported=None): | ||||||
|     """Config validator for features only available on some ESP32 variants.""" |     """Config validator for features only available on some ESP32 variants.""" | ||||||
|     if supported is not None and not isinstance(supported, list): |     if supported is not None and not isinstance(supported, list): | ||||||
|   | |||||||
| @@ -50,6 +50,17 @@ def set_core_data(config): | |||||||
|     return 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: | def _format_framework_arduino_version(ver: cv.Version) -> str: | ||||||
|     # format the given arduino (https://github.com/esp8266/Arduino/releases) version to |     # format the given arduino (https://github.com/esp8266/Arduino/releases) version to | ||||||
|     # a PIO platformio/framework-arduinoespressif8266 value |     # a PIO platformio/framework-arduinoespressif8266 value | ||||||
|   | |||||||
| @@ -42,6 +42,17 @@ def set_core_data(config): | |||||||
|     return 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: | 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 most recent releases have not been uploaded to platformio so grabbing them directly from | ||||||
|     # the GitHub release is one path forward for now. |     # the GitHub release is one path forward for now. | ||||||
|   | |||||||
| @@ -530,11 +530,43 @@ class ImportRequestHandler(BaseHandler): | |||||||
|         self.finish() |         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): | class DownloadBinaryRequestHandler(BaseHandler): | ||||||
|     @authenticated |     @authenticated | ||||||
|     @bind_config |     @bind_config | ||||||
|     def get(self, configuration=None): |     def get(self, configuration=None): | ||||||
|         type = self.get_argument("type", "firmware.bin") |  | ||||||
|         compressed = self.get_argument("compressed", "0") == "1" |         compressed = self.get_argument("compressed", "0") == "1" | ||||||
|  |  | ||||||
|         storage_path = ext_storage_path(settings.config_dir, configuration) |         storage_path = ext_storage_path(settings.config_dir, configuration) | ||||||
| @@ -543,27 +575,22 @@ class DownloadBinaryRequestHandler(BaseHandler): | |||||||
|             self.send_error(404) |             self.send_error(404) | ||||||
|             return |             return | ||||||
|  |  | ||||||
|         if storage_json.target_platform.lower() == const.PLATFORM_RP2040: |         # fallback to type=, but prioritize file= | ||||||
|             filename = f"{storage_json.name}.uf2" |         file_name = self.get_argument("type", None) | ||||||
|             path = storage_json.firmware_bin_path.replace( |         file_name = self.get_argument("file", file_name) | ||||||
|                 "firmware.bin", "firmware.uf2" |         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: |         if not Path(path).is_file(): | ||||||
|             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: |  | ||||||
|             args = ["esphome", "idedata", settings.rel_path(configuration)] |             args = ["esphome", "idedata", settings.rel_path(configuration)] | ||||||
|             rc, stdout, _ = run_system_command(*args) |             rc, stdout, _ = run_system_command(*args) | ||||||
|  |  | ||||||
| @@ -575,9 +602,9 @@ class DownloadBinaryRequestHandler(BaseHandler): | |||||||
|  |  | ||||||
|             found = False |             found = False | ||||||
|             for image in idedata.extra_flash_images: |             for image in idedata.extra_flash_images: | ||||||
|                 if image.path.endswith(type): |                 if image.path.endswith(file_name): | ||||||
|                     path = image.path |                     path = image.path | ||||||
|                     filename = type |                     download_name = file_name | ||||||
|                     found = True |                     found = True | ||||||
|                     break |                     break | ||||||
|  |  | ||||||
| @@ -585,10 +612,12 @@ class DownloadBinaryRequestHandler(BaseHandler): | |||||||
|                 self.send_error(404) |                 self.send_error(404) | ||||||
|                 return |                 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-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") |         self.set_header("Cache-Control", "no-cache") | ||||||
|         if not Path(path).is_file(): |         if not Path(path).is_file(): | ||||||
|             self.send_error(404) |             self.send_error(404) | ||||||
| @@ -1259,6 +1288,7 @@ def make_app(debug=get_bool_env(ENV_DEV)): | |||||||
|             (f"{rel}update-all", EsphomeUpdateAllHandler), |             (f"{rel}update-all", EsphomeUpdateAllHandler), | ||||||
|             (f"{rel}info", InfoRequestHandler), |             (f"{rel}info", InfoRequestHandler), | ||||||
|             (f"{rel}edit", EditRequestHandler), |             (f"{rel}edit", EditRequestHandler), | ||||||
|  |             (f"{rel}downloads", DownloadListRequestHandler), | ||||||
|             (f"{rel}download.bin", DownloadBinaryRequestHandler), |             (f"{rel}download.bin", DownloadBinaryRequestHandler), | ||||||
|             (f"{rel}serial-ports", SerialPortRequestHandler), |             (f"{rel}serial-ports", SerialPortRequestHandler), | ||||||
|             (f"{rel}ping", PingRequestHandler), |             (f"{rel}ping", PingRequestHandler), | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user