Summary
WLED.upgrade() reports success even when the device rejects the OTA upload. WLED answers rejected updates with an HTTP 200 HTML error page, and upgrade() only checks the HTTP status of the POST to /update — it never inspects the response body. The result in Home Assistant is an update that "completes" in under a second, changes nothing on the device, raises no error and logs nothing (even at debug level).
Environment
- Home Assistant 2026.7.1 (WLED integration, python-wled as bundled)
- Device: WLED 16.0.0 (
release: ESP32, Gledopto controller), on an IoT VLAN
- Home Assistant runs on a different subnet than the device
- Device setting "Only allow update from same network/WiFi" enabled (this is a stock WLED security option)
Steps to reproduce
- Put a WLED device on a different subnet than Home Assistant, with "Only allow update from same network/WiFi" enabled (or reproduce with plain
curl from another subnet, see below).
- Trigger the firmware update from Home Assistant (
update.install on the WLED update entity).
- Observe:
in_progress flips to true and back to false within ~1 second, no exception, no log output, device untouched (version and uptime unchanged).
What the device actually answers to the upload (reproduced with curl from another subnet):
HTTP 200
<!DOCTYPE html> ... <h2>Access Denied</h2>Client is not on local subnet. ...
Since the status code is 200, upgrade() returns normally and the HA update entity reports the install as done — while the device still runs the old firmware. The update entity then flips back to "update available" on the next poll, which is how the user experiences it: "I've tried the update three times and it never completes", with no error anywhere.
Expected behavior
upgrade() should raise (WLEDUpgradeError) when the device rejects the upload.
Suggested fix
WLED's /update endpoint always answers 200 and communicates the outcome in the HTML body (<h2>Update successful!</h2> vs <h2>Access Denied</h2> / Update failed!). Two options, ideally both:
- Parse the response body of the POST to
/update and raise unless it contains the success marker.
- As a stronger check, poll
/json/info after the reboot window and verify ver actually changed.
Related but distinct: #1900 covers the OTA lock password; this issue is about the subnet restriction (and generally any rejection) being silently swallowed because the response body is ignored.
Happy to provide more diagnostics if useful. Thanks for the great library!
Summary
WLED.upgrade()reports success even when the device rejects the OTA upload. WLED answers rejected updates with an HTTP 200 HTML error page, andupgrade()only checks the HTTP status of the POST to/update— it never inspects the response body. The result in Home Assistant is an update that "completes" in under a second, changes nothing on the device, raises no error and logs nothing (even at debug level).Environment
release: ESP32, Gledopto controller), on an IoT VLANSteps to reproduce
curlfrom another subnet, see below).update.installon the WLED update entity).in_progressflips totrueand back tofalsewithin ~1 second, no exception, no log output, device untouched (version and uptime unchanged).What the device actually answers to the upload (reproduced with curl from another subnet):
Since the status code is 200,
upgrade()returns normally and the HA update entity reports the install as done — while the device still runs the old firmware. The update entity then flips back to "update available" on the next poll, which is how the user experiences it: "I've tried the update three times and it never completes", with no error anywhere.Expected behavior
upgrade()should raise (WLEDUpgradeError) when the device rejects the upload.Suggested fix
WLED's
/updateendpoint always answers 200 and communicates the outcome in the HTML body (<h2>Update successful!</h2>vs<h2>Access Denied</h2>/Update failed!). Two options, ideally both:/updateand raise unless it contains the success marker./json/infoafter the reboot window and verifyveractually changed.Related but distinct: #1900 covers the OTA lock password; this issue is about the subnet restriction (and generally any rejection) being silently swallowed because the response body is ignored.
Happy to provide more diagnostics if useful. Thanks for the great library!