md5 files and esphome publish

This commit is contained in:
oarcher 2024-04-04 13:06:46 +00:00
parent af3fb615ea
commit 77d3fb6803
4 changed files with 113 additions and 26 deletions

View File

@ -6,6 +6,8 @@ import os
import re import re
import sys import sys
import time import time
import hashlib
import gzip
from datetime import datetime from datetime import datetime
import argcomplete import argcomplete
@ -37,7 +39,7 @@ from esphome.const import (
SECRETS_FILES, SECRETS_FILES,
) )
from esphome.core import CORE, EsphomeError, coroutine from esphome.core import CORE, EsphomeError, coroutine
from esphome.helpers import indent, is_ip_address from esphome.helpers import indent, is_ip_address, copy_file_if_changed
from esphome.util import ( from esphome.util import (
run_external_command, run_external_command,
run_external_process, run_external_process,
@ -220,7 +222,25 @@ def compile_program(args, config):
if rc != 0: if rc != 0:
return rc return rc
idedata = platformio_api.get_idedata(config) idedata = platformio_api.get_idedata(config)
return 0 if idedata is not None else 1 if idedata is None:
return 1
with open(CORE.firmware_bin, "rb") as firmware_bin:
bin_content = firmware_bin.read()
md5 = hashlib.md5(bin_content).hexdigest()
with open(CORE.firmware_md5, "w", encoding="ascii") as firmware_md5:
firmware_md5.write(md5)
gz_content = gzip.compress(bin_content, compresslevel=9)
with open(CORE.firmware_gz, "wb") as firmware_gz:
firmware_gz.write(gz_content)
md5_gz = hashlib.md5(gz_content).hexdigest()
with open(CORE.firmware_gz_md5, "w", encoding="ascii") as firmware_gz_md5:
firmware_gz_md5.write(md5_gz)
return 0
def upload_using_esptool(config, port, file): def upload_using_esptool(config, port, file):
@ -686,6 +706,22 @@ def command_rename(args, config):
print() print()
return 0 return 0
def command_publish(args, config):
files = [ CORE.firmware_bin, CORE.firmware_md5, CORE.firmware_gz, CORE.firmware_gz_md5]
if not all(map(lambda f: os.path.isfile(f), files)):
exit_code = compile_program(args, config)
if exit_code != 0:
return exit_code
topdir = os.path.join(args.directory, CORE.name)
_LOGGER.info(f"Copying firmware files to {topdir}...")
for src_file in files:
dst_file = os.path.join(args.directory, CORE.name, os.path.basename(src_file))
copy_file_if_changed(src_file, dst_file)
return 0
PRE_CONFIG_ACTIONS = { PRE_CONFIG_ACTIONS = {
"wizard": command_wizard, "wizard": command_wizard,
@ -707,6 +743,7 @@ POST_CONFIG_ACTIONS = {
"idedata": command_idedata, "idedata": command_idedata,
"rename": command_rename, "rename": command_rename,
"discover": command_discover, "discover": command_discover,
"publish": command_publish,
} }
@ -804,6 +841,19 @@ def parse_args(argv):
"configuration", help="Your YAML configuration file.", nargs=1 "configuration", help="Your YAML configuration file.", nargs=1
) )
parser_publish = subparsers.add_parser(
"publish",
help="Publish compiled binary. (currently copy binary to directory)",
)
parser_publish.add_argument(
"configuration", help="Your YAML configuration file(s).", nargs="+"
)
parser_publish.add_argument(
"--directory",
default=os.path.join(os.path.sep, "config", "www"),
help="Destination directory",
)
parser_run = subparsers.add_parser( parser_run = subparsers.add_parser(
"run", "run",
help="Validate the configuration, create a binary, upload it, and start logs.", help="Validate the configuration, create a binary, upload it, and start logs.",

View File

@ -627,6 +627,18 @@ class EsphomeCore:
return self.relative_pioenvs_path(self.name, "firmware.uf2") return self.relative_pioenvs_path(self.name, "firmware.uf2")
return self.relative_pioenvs_path(self.name, "firmware.bin") return self.relative_pioenvs_path(self.name, "firmware.bin")
@property
def firmware_md5(self):
return self.firmware_bin + '.md5'
@property
def firmware_gz(self):
return self.firmware_bin + '.gz'
@property
def firmware_gz_md5(self):
return self.firmware_gz + '.md5'
@property @property
def target_platform(self): def target_platform(self):
return self.data[KEY_CORE][KEY_TARGET_PLATFORM] return self.data[KEY_CORE][KEY_TARGET_PLATFORM]

View File

@ -375,6 +375,11 @@ class EsphomeRunHandler(EsphomePortCommandWebSocket):
"""Build the command to run.""" """Build the command to run."""
return await self.build_device_command(["run"], json_message) return await self.build_device_command(["run"], json_message)
class EsphomePublishHandler(EsphomePortCommandWebSocket):
async def build_command(self, json_message: dict[str, Any]) -> list[str]:
config_file = settings.rel_path(json_message["configuration"])
return [*DASHBOARD_COMMAND, "publish", config_file]
class EsphomeCompileHandler(EsphomeCommandWebSocket): class EsphomeCompileHandler(EsphomeCommandWebSocket):
async def build_command(self, json_message: dict[str, Any]) -> list[str]: async def build_command(self, json_message: dict[str, Any]) -> list[str]:
@ -1038,6 +1043,8 @@ def get_base_frontend_path() -> str:
if not static_path.endswith("/"): if not static_path.endswith("/"):
static_path += "/" static_path += "/"
_LOGGER.warning("esphome_dashboard %s" % os.path.abspath(os.path.join(os.getcwd(), static_path, "esphome_dashboard")))
# This path can be relative, so resolve against the root or else templates don't work # This path can be relative, so resolve against the root or else templates don't work
return os.path.abspath(os.path.join(os.getcwd(), static_path, "esphome_dashboard")) return os.path.abspath(os.path.join(os.getcwd(), static_path, "esphome_dashboard"))
@ -1118,6 +1125,7 @@ def make_app(debug=get_bool_env(ENV_DEV)) -> tornado.web.Application:
(f"{rel}logs", EsphomeLogsHandler), (f"{rel}logs", EsphomeLogsHandler),
(f"{rel}upload", EsphomeUploadHandler), (f"{rel}upload", EsphomeUploadHandler),
(f"{rel}run", EsphomeRunHandler), (f"{rel}run", EsphomeRunHandler),
(f"{rel}publish", EsphomePublishHandler),
(f"{rel}compile", EsphomeCompileHandler), (f"{rel}compile", EsphomeCompileHandler),
(f"{rel}validate", EsphomeValidateHandler), (f"{rel}validate", EsphomeValidateHandler),
(f"{rel}clean-mqtt", EsphomeCleanMqttHandler), (f"{rel}clean-mqtt", EsphomeCleanMqttHandler),

View File

@ -8,8 +8,9 @@ import random
import socket import socket
import sys import sys
import time import time
from typing import Optional
from esphome.core import EsphomeError from esphome.core import EsphomeError, CORE
from esphome.helpers import is_ip_address, resolve_ip_address from esphome.helpers import is_ip_address, resolve_ip_address
RESPONSE_OK = 0x00 RESPONSE_OK = 0x00
@ -194,11 +195,11 @@ def send_check(sock, data, msg):
def perform_ota( def perform_ota(
sock: socket.socket, password: str, file_handle: io.IOBase, filename: str sock: socket.socket, password: str, filename: Optional[str] = None
) -> None: ) -> None:
file_contents = file_handle.read() #file_contents = file_handle.read()
file_size = len(file_contents) #file_size = len(file_contents)
_LOGGER.info("Uploading %s (%s bytes)", filename, file_size) #_LOGGER.info("Uploading %s (%s bytes)", filename, file_size)
# Enable nodelay, we need it for phase 1 # Enable nodelay, we need it for phase 1
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
@ -219,10 +220,28 @@ def perform_ota(
)[0] )[0]
if features == RESPONSE_SUPPORTS_COMPRESSION: if features == RESPONSE_SUPPORTS_COMPRESSION:
upload_contents = gzip.compress(file_contents, compresslevel=9) filename = CORE.firmware_gz
_LOGGER.info("Compressed to %s bytes", len(upload_contents)) filename_md5 = CORE.firmware_gz_md5
#upload_contents = gzip.compress(file_contents, compresslevel=9)
#_LOGGER.info("Compressed to %s bytes", len(upload_contents))
_LOGGER.info("Using compressed image")
else: else:
upload_contents = file_contents _LOGGER.info("Not using compressed image")
filename = CORE.firmware_bin
filename_md5 = CORE.firmware_md5
#upload_contents = file_contents
try:
with open(filename, "rb") as file_handle:
file_contents = file_handle.read()
except IOError as e:
raise OTAError(f"Error on {filename}: {str(e)}")
try:
with open(filename_md5, "r", encoding='ascii') as file_md5_handle:
md5 = file_md5_handle.read()
except IOError as e:
raise OTAError(f"Error on {filename_md5}: {str(e)}")
(auth,) = receive_exactly( (auth,) = receive_exactly(
sock, 1, "auth", [RESPONSE_REQUEST_AUTH, RESPONSE_AUTH_OK] sock, 1, "auth", [RESPONSE_REQUEST_AUTH, RESPONSE_AUTH_OK]
@ -249,7 +268,7 @@ def perform_ota(
send_check(sock, result, "auth result") send_check(sock, result, "auth result")
receive_exactly(sock, 1, "auth result", RESPONSE_AUTH_OK) receive_exactly(sock, 1, "auth result", RESPONSE_AUTH_OK)
upload_size = len(upload_contents) upload_size = len(file_contents)
upload_size_encoded = [ upload_size_encoded = [
(upload_size >> 24) & 0xFF, (upload_size >> 24) & 0xFF,
(upload_size >> 16) & 0xFF, (upload_size >> 16) & 0xFF,
@ -259,10 +278,9 @@ def perform_ota(
send_check(sock, upload_size_encoded, "binary size") send_check(sock, upload_size_encoded, "binary size")
receive_exactly(sock, 1, "binary size", RESPONSE_UPDATE_PREPARE_OK) receive_exactly(sock, 1, "binary size", RESPONSE_UPDATE_PREPARE_OK)
upload_md5 = hashlib.md5(upload_contents).hexdigest() _LOGGER.debug("MD5 of upload is %s", md5)
_LOGGER.debug("MD5 of upload is %s", upload_md5)
send_check(sock, upload_md5, "file checksum") send_check(sock, md5, "file checksum")
receive_exactly(sock, 1, "file checksum", RESPONSE_BIN_MD5_OK) receive_exactly(sock, 1, "file checksum", RESPONSE_BIN_MD5_OK)
# Disable nodelay for transfer # Disable nodelay for transfer
@ -278,7 +296,7 @@ def perform_ota(
offset = 0 offset = 0
progress = ProgressBar() progress = ProgressBar()
while True: while True:
chunk = upload_contents[offset : offset + UPLOAD_BLOCK_SIZE] chunk = file_contents[offset : offset + UPLOAD_BLOCK_SIZE]
if not chunk: if not chunk:
break break
offset += len(chunk) offset += len(chunk)
@ -310,7 +328,7 @@ def perform_ota(
time.sleep(1) time.sleep(1)
def run_ota_impl_(remote_host, remote_port, password, filename): def run_ota_impl_(remote_host, remote_port, password, filename=None):
if is_ip_address(remote_host): if is_ip_address(remote_host):
_LOGGER.info("Connecting to %s", remote_host) _LOGGER.info("Connecting to %s", remote_host)
ip = remote_host ip = remote_host
@ -339,9 +357,8 @@ def run_ota_impl_(remote_host, remote_port, password, filename):
_LOGGER.error("Connecting to %s:%s failed: %s", remote_host, remote_port, err) _LOGGER.error("Connecting to %s:%s failed: %s", remote_host, remote_port, err)
return 1 return 1
with open(filename, "rb") as file_handle:
try: try:
perform_ota(sock, password, file_handle, filename) perform_ota(sock, password, filename=filename)
except OTAError as err: except OTAError as err:
_LOGGER.error(str(err)) _LOGGER.error(str(err))
return 1 return 1
@ -351,9 +368,9 @@ def run_ota_impl_(remote_host, remote_port, password, filename):
return 0 return 0
def run_ota(remote_host, remote_port, password, filename): def run_ota(remote_host, remote_port, password, filename=None):
try: try:
return run_ota_impl_(remote_host, remote_port, password, filename) return run_ota_impl_(remote_host, remote_port, password, filename=filename)
except OTAError as err: except OTAError as err:
_LOGGER.error(err) _LOGGER.error(err)
return 1 return 1