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] | ||||
|  | ||||
|  | ||||
| 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): | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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), | ||||
|   | ||||
		Reference in New Issue
	
	Block a user