I’ll plug into the UART again tomorrow to see if I can get better logs, but these are over-the-network logs, snipped heavily:
[00:14:23.071][I][app:151]: ESPHome version 2026.9.1 compiled on 2026-10-01 20:59:03 +0200
[00:14:23.072][I][app:158]: ESP32 Chip: ESP32 rev3.0, 1 core(s)
[00:14:23.072][W][app:171]: Chip rev >= 3.0 detected. Set minimum_chip_revision: "3.0" under esp32 > framework > advanced to reduce binary size
[00:14:23.072][W][app:198]: Bootloader supports SRAM1 as IRAM (+40KB). Set sram1_as_iram: true under esp32 > framework > advanced
...
[00:14:23.570][C][esphome.ota:125]: Over-The-Air updates:
[00:14:23.570][C][esphome.ota:125]: Address: shp1pm-test.local:3232
[00:14:23.570][C][esphome.ota:125]: Version: 2
[00:14:23.570][C][esphome.ota:125]: Encryption: offered, plaintext accepted
[00:14:23.580][C][esphome.ota:149]: Password configured
[00:14:23.598][C][safe_mode:058]: Safe Mode:
[00:14:23.598][C][safe_mode:058]: Successful after: 60s
[00:14:23.598][C][safe_mode:058]: Invoke after: 10 attempts
[00:14:23.598][C][safe_mode:058]: Duration: 300s
[00:14:23.616][C][safe_mode:079]: Bootloader rollback: support unknown
...
[00:15:15.798][I][safe_mode:154]: Boot seems successful; resetting boot loop counter
[00:15:15.806][D][esp-idf:000]: E (60542) esp_ota_ops: ota data invalid, no current app. Assuming factory
That last line seems interesting.
The ESPHome binary was built using the template from the esphome website and I flashed it onto a different Plus1PM with UART yesterday. The important bits:
esphome:
name: shp1pm-test
friendly_name: Shelly Plus 1PM Test
platformio_options: # Only needed for the single core 160MHz variant
board_build.f_cpu: 160000000L
esp32:
variant: esp32
framework:
type: esp-idf
sdkconfig_options: # Only needed for the single core 160MHz variant
CONFIG_FREERTOS_UNICORE: y
CONFIG_ESP32_DEFAULT_CPU_FREQ_160: y
CONFIG_ESP32_DEFAULT_CPU_FREQ_MHZ: "160"
And for what it’s worth, I shoved some lazy code in the main block to make it easier to just run for other models:
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("model", type=str, help="Shelly model code")
parser.add_argument("app", type=Path, help="ESPHome OTA-format app binary")
parser.add_argument("output", type=Path, help="Candidate Shelly-format ZIP path")
args = parser.parse_args()
if args.output.suffix.lower() != ".zip":
args.output = args.output.with_suffix(".zip")
print("Getting official firmware info")
with urllib.request.urlopen(
f"https://updates.shelly.cloud/update/{args.model}",
context=ssl._create_unverified_context()
) as resp:
fw_info = json.load(resp)
shelly_fw_url = fw_info["stable"]["url"]
print("Downloading official fiemware zip file")
with urllib.request.urlopen(
shelly_fw_url, context=ssl._create_unverified_context()
) as resp, open(f"{args.model}.zip", "wb") as f:
shutil.copyfileobj(resp, f)
print("Building")
build(args.model, args.app, args.output)
print(f"Created {args.output}")
Edit: I have a bunch more to do, so will do the next one with the partition table properly and see if it’s any different.