1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 16:51:52 +00:00

Compare commits

...

8 Commits

Author SHA1 Message Date
Paulus Schoutsen
550aa7300c Address comments 2026-01-12 15:57:07 -05:00
Paulus Schoutsen
b48a185612 add test 2026-01-10 23:43:22 -05:00
pre-commit-ci-lite[bot]
16fad85e60 [pre-commit.ci lite] apply automatic fixes 2026-01-11 04:07:49 +00:00
Paulus Schoutsen
b32c785d7d Allow finding all devices as target that match mac suffix 2026-01-10 23:02:53 -05:00
esphomebot
da7680f7d9 Update webserver local assets to 20260110-013228 (#13113)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-01-09 16:38:01 -10:00
Douwe
cea2878b55 [water_heater] (3/4) Implement web_server for new water_heater component (#12511)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-01-09 15:25:42 -10:00
Jonathan Swoboda
e0ff7fdaa1 [esp32_hosted] Add HTTP-based coprocessor firmware update support (#13090)
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-09 17:36:56 -05:00
tomaszduda23
3c9b300c46 [CI] skip endpoint check due to test grouping (#13111) 2026-01-09 22:13:37 +00:00
16 changed files with 9721 additions and 8954 deletions

View File

@@ -34,6 +34,7 @@ from esphome.const import (
CONF_MDNS,
CONF_MQTT,
CONF_NAME,
CONF_NAME_ADD_MAC_SUFFIX,
CONF_OTA,
CONF_PASSWORD,
CONF_PLATFORM,
@@ -59,6 +60,7 @@ from esphome.util import (
run_external_process,
safe_print,
)
from esphome.zeroconf import discover_mdns_devices
_LOGGER = logging.getLogger(__name__)
@@ -232,22 +234,34 @@ def choose_upload_log_host(
(f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
]
def add_ota_options() -> None:
"""Add OTA options, using mDNS discovery if name_add_mac_suffix is enabled."""
if has_name_add_mac_suffix() and has_mdns() and has_non_ip_address():
# Discover devices via mDNS when name_add_mac_suffix is enabled
safe_print("Discovering devices...")
discovered = discover_mdns_devices(CORE.name)
for device_addr in discovered:
options.append((f"Over The Air ({device_addr})", device_addr))
if not discovered and has_resolvable_address():
# No devices found, show base address as fallback
options.append(
(f"Over The Air ({CORE.address}) (no devices found)", CORE.address)
)
elif has_resolvable_address():
options.append((f"Over The Air ({CORE.address})", CORE.address))
if has_mqtt_ip_lookup():
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
if purpose == Purpose.LOGGING:
if has_mqtt_logging():
mqtt_config = CORE.config[CONF_MQTT]
options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
if has_api():
if has_resolvable_address():
options.append((f"Over The Air ({CORE.address})", CORE.address))
if has_mqtt_ip_lookup():
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
add_ota_options()
elif purpose == Purpose.UPLOADING and has_ota():
if has_resolvable_address():
options.append((f"Over The Air ({CORE.address})", CORE.address))
if has_mqtt_ip_lookup():
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
add_ota_options()
if check_default is not None and check_default in [opt[1] for opt in options]:
return [check_default]
@@ -334,6 +348,14 @@ def has_resolvable_address() -> bool:
return not CORE.address.endswith(".local")
def has_name_add_mac_suffix() -> bool:
"""Check if name_add_mac_suffix is enabled in the config."""
if CORE.config is None:
return False
esphome_config = CORE.config.get(CONF_ESPHOME, {})
return esphome_config.get(CONF_NAME_ADD_MAC_SUFFIX, False)
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):
from esphome import mqtt

View File

@@ -7,83 +7,83 @@ namespace esphome::captive_portal {
#ifdef USE_CAPTIVE_PORTAL_GZIP
const uint8_t INDEX_GZ[] PROGMEM = {
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x95, 0x56, 0xeb, 0x6f, 0xd4, 0x38, 0x10, 0xff, 0xce,
0x5f, 0xe1, 0x33, 0x8f, 0x26, 0xd0, 0x3c, 0xb7, 0xdb, 0x96, 0x6c, 0x12, 0x04, 0xdc, 0x21, 0x90, 0x28, 0x20, 0xb5,
0x70, 0x1f, 0x10, 0x52, 0xbd, 0xc9, 0x64, 0x63, 0x9a, 0x38, 0x39, 0xdb, 0xfb, 0x62, 0xb5, 0xf7, 0xb7, 0xdf, 0x38,
0xc9, 0x6e, 0xb7, 0x15, 0x9c, 0xee, 0x5a, 0x35, 0x1d, 0xdb, 0xf3, 0xf8, 0xcd, 0x78, 0x1e, 0x8e, 0x7f, 0xcb, 0x9b,
0x4c, 0xaf, 0x5b, 0x20, 0xa5, 0xae, 0xab, 0x34, 0x36, 0x5f, 0x52, 0x31, 0x31, 0x4b, 0x40, 0xe0, 0x0a, 0x58, 0x9e,
0xc6, 0x35, 0x68, 0x46, 0xb2, 0x92, 0x49, 0x05, 0x3a, 0xf9, 0x7c, 0xf5, 0xc6, 0x39, 0x4f, 0xe3, 0x8a, 0x8b, 0x1b,
0x22, 0xa1, 0x4a, 0x78, 0xd6, 0x08, 0x52, 0x4a, 0x28, 0x92, 0x9c, 0x69, 0x16, 0xf1, 0x9a, 0xcd, 0x60, 0x10, 0x11,
0xac, 0x86, 0x64, 0xc1, 0x61, 0xd9, 0x36, 0x52, 0x13, 0xe4, 0xd3, 0x20, 0x74, 0x42, 0x97, 0x3c, 0xd7, 0x65, 0x92,
0xc3, 0x82, 0x67, 0xe0, 0x74, 0x8b, 0x63, 0x2e, 0xb8, 0xe6, 0xac, 0x72, 0x54, 0xc6, 0x2a, 0x48, 0x82, 0xe3, 0xb9,
0x02, 0xd9, 0x2d, 0xd8, 0x14, 0xd7, 0xa2, 0xa1, 0x69, 0xac, 0x32, 0xc9, 0x5b, 0x4d, 0x0c, 0xd4, 0xa4, 0x6e, 0xf2,
0x79, 0x05, 0xa9, 0xe7, 0x31, 0x85, 0x90, 0x94, 0xc7, 0x45, 0x0e, 0x2b, 0x77, 0xea, 0x67, 0x99, 0x0f, 0xe7, 0xe7,
0xee, 0x77, 0xf5, 0x00, 0x9d, 0x9a, 0xd7, 0x68, 0xcd, 0xad, 0x9a, 0x8c, 0x69, 0xde, 0x08, 0x57, 0x01, 0x93, 0x59,
0x99, 0x24, 0x09, 0x7d, 0xa1, 0xd8, 0x02, 0xe8, 0x93, 0x27, 0xd6, 0x9e, 0x69, 0x06, 0xfa, 0x8f, 0x0a, 0x0c, 0xa9,
0x5e, 0xad, 0xaf, 0xd8, 0xec, 0x03, 0x02, 0xb7, 0x28, 0x53, 0x3c, 0x07, 0x6a, 0x7f, 0xf5, 0xbf, 0xb9, 0x4a, 0xaf,
0x2b, 0x70, 0x73, 0xae, 0xda, 0x8a, 0xad, 0x13, 0x3a, 0x45, 0xad, 0x37, 0xd4, 0x9e, 0x14, 0x73, 0x91, 0x19, 0xe5,
0x44, 0x59, 0x60, 0x6f, 0x2a, 0x40, 0x78, 0xc9, 0x05, 0xd3, 0xa5, 0x5b, 0xb3, 0x95, 0xd5, 0x13, 0x5c, 0x58, 0xe1,
0x53, 0x0b, 0x9e, 0x05, 0xbe, 0x6f, 0x1f, 0x77, 0x1f, 0xdf, 0xf6, 0xf0, 0xff, 0x44, 0x82, 0x9e, 0x4b, 0x41, 0x98,
0x75, 0x1d, 0xb7, 0xc8, 0x49, 0xf2, 0x84, 0x5e, 0x04, 0x21, 0x09, 0x9e, 0xbb, 0xe1, 0xf8, 0xbd, 0x7b, 0x46, 0x4e,
0xf0, 0x7f, 0x76, 0xe6, 0x8c, 0x49, 0x70, 0x82, 0x9f, 0x30, 0x74, 0xc7, 0xc4, 0xff, 0x41, 0x49, 0xc1, 0xab, 0x2a,
0xa1, 0xa2, 0x11, 0x40, 0x89, 0xd2, 0xb2, 0xb9, 0x81, 0x84, 0x66, 0x73, 0x29, 0x11, 0xfb, 0xeb, 0xa6, 0x6a, 0x24,
0xf5, 0xd2, 0x07, 0xff, 0x4b, 0xa1, 0x96, 0x4c, 0xa8, 0xa2, 0x91, 0x75, 0x42, 0xbb, 0xe8, 0x5b, 0x8f, 0x36, 0x7a,
0x4b, 0xcc, 0xc7, 0x3e, 0x38, 0x74, 0x1a, 0xc9, 0x67, 0x5c, 0x24, 0xd4, 0x68, 0x3c, 0x47, 0x23, 0xd7, 0xf6, 0x76,
0xef, 0x3d, 0x33, 0xde, 0x0f, 0xfe, 0x34, 0xd6, 0xd7, 0xeb, 0x58, 0x2d, 0x66, 0x64, 0x55, 0x57, 0x42, 0x25, 0xb4,
0xd4, 0xba, 0x8d, 0x3c, 0x6f, 0xb9, 0x5c, 0xba, 0xcb, 0x91, 0xdb, 0xc8, 0x99, 0x17, 0xfa, 0xbe, 0xef, 0x21, 0x07,
0x25, 0x7d, 0x22, 0xd0, 0xf0, 0x84, 0x92, 0x12, 0xf8, 0xac, 0xd4, 0x1d, 0x9d, 0x3e, 0xda, 0xc0, 0x36, 0x36, 0x1c,
0xe9, 0xf5, 0xb7, 0x03, 0x2b, 0xfc, 0xc0, 0x0a, 0xbc, 0x60, 0x16, 0xdd, 0xb9, 0x79, 0xd4, 0xb9, 0x79, 0xc6, 0x42,
0x12, 0x12, 0xbf, 0xfb, 0x0d, 0x1d, 0x43, 0x0f, 0x2b, 0xe7, 0xde, 0x8a, 0x1c, 0xac, 0x0c, 0x55, 0x9f, 0x3a, 0xcf,
0xf7, 0xb2, 0x81, 0xd9, 0x59, 0x04, 0xfe, 0xed, 0x86, 0x11, 0x78, 0x7b, 0x7a, 0xb8, 0x76, 0xc2, 0x2f, 0x87, 0x0c,
0xc6, 0x5a, 0x19, 0x7c, 0x39, 0x65, 0x63, 0x32, 0x1e, 0x76, 0xc6, 0x8e, 0xa1, 0xf7, 0x2b, 0x32, 0x5e, 0x20, 0x47,
0xed, 0x9c, 0x3a, 0x63, 0x36, 0x22, 0xa3, 0x01, 0x08, 0x52, 0xb8, 0x7d, 0x8a, 0x82, 0x07, 0x7b, 0xce, 0xe8, 0xc7,
0x91, 0x97, 0x52, 0x3b, 0xa2, 0xf4, 0xd6, 0xf3, 0xe6, 0xd0, 0x73, 0xf7, 0x7b, 0x83, 0x39, 0x45, 0x29, 0x46, 0x06,
0x74, 0x56, 0x5a, 0xd4, 0xc3, 0xc2, 0x2a, 0xf8, 0x0c, 0xb3, 0xbe, 0x11, 0xd4, 0x76, 0x75, 0x09, 0xc2, 0xda, 0x89,
0x1a, 0x41, 0xe8, 0x4e, 0xac, 0xfb, 0x27, 0xda, 0xde, 0xec, 0xf3, 0x5f, 0x73, 0x8d, 0x65, 0xa6, 0x5d, 0x53, 0xb0,
0xc7, 0xfb, 0xdd, 0x69, 0x93, 0xaf, 0x7f, 0x51, 0x1a, 0x65, 0xd0, 0xd7, 0x05, 0x17, 0x02, 0xe4, 0x15, 0xac, 0xf0,
0xe6, 0x2e, 0x5e, 0xbe, 0x26, 0x2f, 0xf3, 0x5c, 0x82, 0x52, 0x11, 0xa1, 0xcf, 0x34, 0xd6, 0x40, 0xf6, 0xdf, 0x75,
0x05, 0x77, 0x74, 0xfd, 0xc9, 0xdf, 0x70, 0xf2, 0x01, 0xf4, 0xb2, 0x91, 0x37, 0x83, 0x36, 0x03, 0x6d, 0x62, 0x2a,
0x4c, 0x22, 0x4e, 0xd6, 0x2a, 0x57, 0x55, 0xd8, 0x3e, 0xac, 0xc0, 0x46, 0x3b, 0xed, 0xad, 0x57, 0x62, 0x17, 0xa8,
0xeb, 0x38, 0xe7, 0x0b, 0x92, 0x55, 0xd8, 0x21, 0xb0, 0x5c, 0x7a, 0x55, 0x94, 0x3c, 0x20, 0xdd, 0x4f, 0x23, 0x32,
0x94, 0xbe, 0x49, 0xe8, 0x4f, 0x3a, 0xc0, 0xab, 0xf5, 0xbb, 0xdc, 0x3a, 0x52, 0x58, 0xfb, 0x47, 0xb6, 0xbb, 0x60,
0xd5, 0x1c, 0x48, 0x42, 0x74, 0xc9, 0xd5, 0x2d, 0xc0, 0xc9, 0x2f, 0xc5, 0x5a, 0x75, 0x83, 0x52, 0x05, 0x1e, 0x2b,
0xcb, 0xa6, 0xe9, 0x60, 0x2e, 0x66, 0x7d, 0x83, 0xa4, 0x0f, 0xe9, 0x3d, 0x44, 0x4e, 0x05, 0x85, 0xde, 0xf3, 0x11,
0x2c, 0x3b, 0x65, 0x09, 0x57, 0xa2, 0x75, 0x7b, 0xbb, 0xdf, 0x8c, 0x55, 0xcb, 0xc4, 0x7d, 0x41, 0x03, 0xd0, 0x94,
0x0a, 0x36, 0x36, 0xa4, 0x4c, 0xbd, 0x20, 0xd3, 0xde, 0xa0, 0xc7, 0x76, 0xe4, 0xa3, 0x0d, 0x47, 0x8d, 0xa6, 0x5f,
0xed, 0x35, 0xc6, 0x1e, 0x86, 0x26, 0xbd, 0xde, 0xda, 0xb7, 0x7e, 0xfc, 0x35, 0x07, 0xb9, 0xbe, 0x84, 0x0a, 0x32,
0xdd, 0x48, 0x8b, 0x3e, 0x44, 0x2b, 0x98, 0x4a, 0x9d, 0xc3, 0x6f, 0xaf, 0x2e, 0xde, 0x27, 0x8d, 0x25, 0xed, 0xe3,
0x5f, 0x71, 0x9b, 0x51, 0xf0, 0x15, 0x47, 0xc1, 0xdf, 0xc9, 0x91, 0x19, 0x06, 0x47, 0xdf, 0x50, 0xb4, 0xf3, 0xf7,
0xfa, 0x76, 0x22, 0x98, 0x72, 0x7e, 0x86, 0x2d, 0xe1, 0xd8, 0x78, 0xe8, 0x9c, 0x8e, 0xed, 0x2d, 0xda, 0x47, 0x04,
0x88, 0xbb, 0xeb, 0xeb, 0xd8, 0xdf, 0x4d, 0x8b, 0x4d, 0x9f, 0x6e, 0xa6, 0xcd, 0xca, 0x51, 0xfc, 0x07, 0x17, 0xb3,
0x88, 0x8b, 0x12, 0x24, 0xd7, 0x5b, 0x84, 0x8b, 0x13, 0xa2, 0x9d, 0xeb, 0x4d, 0xcb, 0xf2, 0xdc, 0x9c, 0x8c, 0xdb,
0xd5, 0xa4, 0xc0, 0x79, 0x62, 0x38, 0x21, 0x0a, 0xa0, 0xde, 0xf6, 0xe7, 0x5d, 0x47, 0x89, 0x9e, 0x8f, 0x1f, 0x6f,
0x4d, 0xc2, 0x6d, 0x34, 0x5e, 0x96, 0xc3, 0x2a, 0x3e, 0x13, 0x51, 0x86, 0xc0, 0x41, 0xf6, 0x42, 0x05, 0xab, 0x79,
0xb5, 0x8e, 0x14, 0xf6, 0x36, 0x07, 0x07, 0x0d, 0x2f, 0xb6, 0xd3, 0xb9, 0xd6, 0x8d, 0x40, 0xdb, 0x32, 0x07, 0x19,
0xf9, 0x93, 0x9e, 0x70, 0x24, 0xcb, 0xf9, 0x5c, 0x45, 0xee, 0x48, 0x42, 0x3d, 0x99, 0xb2, 0xec, 0x66, 0x26, 0x9b,
0xb9, 0xc8, 0x9d, 0xcc, 0x74, 0xda, 0xe8, 0x61, 0x50, 0xb0, 0x11, 0x64, 0x93, 0x61, 0x55, 0x14, 0xc5, 0x04, 0x43,
0x01, 0x4e, 0xdf, 0xcb, 0xa2, 0xd0, 0x3d, 0x31, 0x62, 0x07, 0x30, 0xdd, 0xd0, 0x6c, 0xf4, 0x18, 0x71, 0x04, 0x3c,
0x9e, 0xec, 0xdc, 0xf1, 0x27, 0xd8, 0xc2, 0x15, 0x2a, 0x69, 0xb1, 0xb6, 0x11, 0xe6, 0xb6, 0x66, 0x5c, 0x1c, 0xa2,
0x37, 0x69, 0x32, 0x19, 0xc6, 0x0f, 0x86, 0xa5, 0x33, 0xd3, 0x0d, 0xa1, 0x09, 0x0e, 0x98, 0x7e, 0x86, 0x46, 0xe1,
0xa9, 0xdf, 0xae, 0xb6, 0xee, 0x90, 0x20, 0x9b, 0x1d, 0x77, 0x51, 0xc1, 0x6a, 0xf2, 0x7d, 0xae, 0x34, 0x2f, 0xd6,
0xce, 0x30, 0x83, 0x23, 0x4c, 0x16, 0x9c, 0xbd, 0x53, 0x64, 0x05, 0x10, 0x93, 0xce, 0x86, 0xc3, 0x35, 0xd4, 0x6a,
0x88, 0xd3, 0x5e, 0x4d, 0x97, 0xa0, 0x77, 0x75, 0xfd, 0x1b, 0xb7, 0xc9, 0xc5, 0x4d, 0xcd, 0x24, 0x8e, 0x0a, 0x67,
0xda, 0x60, 0x4c, 0xeb, 0xc8, 0x39, 0xc3, 0xbb, 0x1a, 0xb6, 0x8c, 0x32, 0xf4, 0x1c, 0x61, 0x76, 0xb3, 0x75, 0x17,
0xef, 0xa0, 0x5d, 0x11, 0xd5, 0x54, 0x3c, 0x1f, 0xf8, 0x3a, 0x16, 0xe2, 0xef, 0xc3, 0x13, 0xe0, 0x75, 0x13, 0xb3,
0xb7, 0x0b, 0xf5, 0x49, 0x71, 0xce, 0x02, 0xff, 0x27, 0x37, 0x92, 0x17, 0x45, 0x38, 0x2d, 0xf6, 0x91, 0x32, 0x63,
0xd2, 0x94, 0x46, 0x97, 0x5a, 0xb1, 0xd7, 0xbf, 0x66, 0x4c, 0x66, 0xe0, 0x03, 0x05, 0x23, 0x8c, 0xef, 0x9b, 0x80,
0xf0, 0x3c, 0xc1, 0x4e, 0x95, 0x1e, 0xb4, 0x2f, 0x64, 0x0c, 0x76, 0x47, 0x48, 0xdd, 0x69, 0x46, 0xfd, 0x59, 0x87,
0x3e, 0x7d, 0xdd, 0x60, 0x7d, 0x60, 0xdb, 0x11, 0x33, 0xa2, 0x1b, 0x32, 0x84, 0xc0, 0x75, 0xdd, 0x78, 0x2a, 0xd3,
0x4f, 0x15, 0x30, 0x05, 0x64, 0xc9, 0xb8, 0x76, 0xb1, 0x1a, 0x3b, 0xfe, 0xbe, 0x8e, 0x51, 0x29, 0xb2, 0xa6, 0x43,
0xc1, 0xc6, 0xe5, 0xa8, 0x37, 0x70, 0x09, 0xda, 0x68, 0x32, 0x06, 0x46, 0x69, 0x6c, 0x46, 0x2e, 0x61, 0x5d, 0x4b,
0x4b, 0xbc, 0x25, 0x2f, 0xb8, 0x79, 0xb2, 0xa4, 0x71, 0x97, 0xe4, 0x46, 0x83, 0x89, 0x73, 0xff, 0xbc, 0xea, 0xa8,
0x0a, 0xc4, 0x0c, 0x27, 0xe9, 0x28, 0x24, 0xe8, 0x76, 0x06, 0x65, 0x53, 0x61, 0x58, 0x93, 0xcb, 0xcb, 0x77, 0xbf,
0xa7, 0x06, 0xcc, 0xad, 0x1c, 0xf6, 0xa7, 0x5e, 0xcc, 0x10, 0x83, 0xd4, 0xe9, 0x49, 0xff, 0xa8, 0x6a, 0xb1, 0xbf,
0xa0, 0x07, 0xf9, 0x1d, 0x1d, 0x9f, 0x86, 0xcd, 0x5e, 0x4f, 0xf7, 0xd7, 0x95, 0x4a, 0x7a, 0x89, 0x80, 0x62, 0x6f,
0x58, 0xc4, 0x9e, 0x01, 0xdc, 0x9f, 0x97, 0x03, 0x1f, 0xc6, 0xe9, 0xe3, 0xd5, 0x4b, 0xf2, 0xb9, 0xc5, 0x26, 0x00,
0x7d, 0xd8, 0x3a, 0xaf, 0xf0, 0x65, 0x58, 0x36, 0x79, 0xf2, 0xe9, 0xe3, 0xe5, 0xd5, 0xde, 0xc3, 0x79, 0xc7, 0x44,
0x40, 0x64, 0xfd, 0xf3, 0x6e, 0x5e, 0x69, 0xde, 0x32, 0xa9, 0x3b, 0xb5, 0x8e, 0xe9, 0x22, 0x3b, 0x1f, 0xba, 0x73,
0x7c, 0x03, 0x41, 0xef, 0x46, 0x2f, 0x98, 0x92, 0x1d, 0xaa, 0x9d, 0xb5, 0x7b, 0xb8, 0xbc, 0xfe, 0xb6, 0xbd, 0xfe,
0xea, 0xbd, 0xee, 0xa5, 0xfb, 0x0f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00};
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e,
0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36,
0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf,
0x7e, 0xa0, 0x24, 0x7b, 0x9d, 0x45, 0x73, 0xb8, 0xb3, 0x60, 0x61, 0x38, 0xef, 0x19, 0xcd, 0x83, 0xc5, 0xdf, 0x2a,
0xc5, 0xec, 0xbe, 0x03, 0xd4, 0xd8, 0x56, 0x94, 0x85, 0x7b, 0x23, 0x41, 0xe5, 0x8a, 0x80, 0x2c, 0x8b, 0x06, 0x68,
0x55, 0x16, 0x2d, 0x58, 0x8a, 0x58, 0x43, 0xb5, 0x01, 0x4b, 0xfe, 0xbc, 0xff, 0x39, 0xb8, 0x2d, 0x0b, 0xc1, 0xe5,
0x1a, 0x69, 0x10, 0x84, 0x33, 0x25, 0x51, 0xa3, 0xa1, 0x26, 0x15, 0xb5, 0x34, 0xe3, 0x2d, 0x5d, 0xc1, 0x24, 0x22,
0x69, 0x0b, 0x64, 0xc3, 0x61, 0xdb, 0x29, 0x6d, 0x11, 0x53, 0xd2, 0x82, 0xb4, 0x04, 0x6f, 0x79, 0x65, 0x1b, 0x52,
0xc1, 0x86, 0x33, 0x08, 0x86, 0xc3, 0x35, 0x97, 0xdc, 0x72, 0x2a, 0x02, 0xc3, 0xa8, 0x00, 0x92, 0x5c, 0xf7, 0x06,
0xf4, 0x70, 0xa0, 0x4b, 0x01, 0x44, 0x2a, 0x5c, 0x16, 0x86, 0x69, 0xde, 0x59, 0xe4, 0x5c, 0x25, 0xad, 0xaa, 0x7a,
0x01, 0x65, 0x14, 0x51, 0x63, 0xc0, 0x9a, 0x88, 0xcb, 0x0a, 0x76, 0xe1, 0x32, 0x66, 0x2c, 0x86, 0xdb, 0xdb, 0xf0,
0xb3, 0x79, 0x56, 0x29, 0xd6, 0xb7, 0x20, 0x6d, 0x28, 0x14, 0xa3, 0x96, 0x2b, 0x19, 0x1a, 0xa0, 0x9a, 0x35, 0x84,
0x10, 0xfc, 0xa3, 0xa1, 0x1b, 0xc0, 0xdf, 0x7f, 0xef, 0x9d, 0x99, 0x56, 0x60, 0xff, 0x21, 0xc0, 0x81, 0xe6, 0xa7,
0xfd, 0x3d, 0x5d, 0xfd, 0x4e, 0x5b, 0xf0, 0x30, 0x35, 0xbc, 0x02, 0xec, 0x7f, 0x8c, 0x3f, 0x85, 0xc6, 0xee, 0x05,
0x84, 0x15, 0x37, 0x9d, 0xa0, 0x7b, 0x82, 0x97, 0x42, 0xb1, 0x35, 0xf6, 0xf3, 0xba, 0x97, 0xcc, 0x29, 0x47, 0xc6,
0x03, 0xff, 0x20, 0xc0, 0x22, 0x4b, 0xde, 0x51, 0xdb, 0x84, 0x2d, 0xdd, 0x79, 0x23, 0xc0, 0xa5, 0x97, 0xfe, 0xe0,
0xc1, 0xcb, 0x24, 0x8e, 0xfd, 0xeb, 0xe1, 0x15, 0xfb, 0x51, 0x12, 0xc7, 0xb9, 0x06, 0xdb, 0x6b, 0x89, 0xa8, 0xf7,
0x50, 0x74, 0xd4, 0x36, 0xa8, 0x22, 0xf8, 0x5d, 0x92, 0xa2, 0xe4, 0x75, 0x98, 0xce, 0x7f, 0x0b, 0x5f, 0xa1, 0x9b,
0x30, 0x9d, 0xb3, 0x57, 0xc1, 0x1c, 0x25, 0x37, 0xc1, 0x1c, 0xa5, 0x69, 0x38, 0x47, 0xf1, 0x17, 0x8c, 0x6a, 0x2e,
0x04, 0xc1, 0x52, 0x49, 0xc0, 0xc8, 0x58, 0xad, 0xd6, 0x40, 0x30, 0xeb, 0xb5, 0x06, 0x69, 0xdf, 0x2a, 0xa1, 0x34,
0x8e, 0xca, 0x67, 0xff, 0x97, 0x42, 0xab, 0xa9, 0x34, 0xb5, 0xd2, 0x2d, 0xc1, 0x43, 0xf6, 0xbd, 0x17, 0x07, 0x7b,
0x44, 0xee, 0xe5, 0x5f, 0x10, 0x03, 0xa5, 0xf9, 0x8a, 0x4b, 0x82, 0x9d, 0xc6, 0x5b, 0x1c, 0x95, 0x0f, 0xfe, 0xf1,
0x1c, 0x3d, 0x75, 0xd1, 0x4f, 0xf1, 0x28, 0xef, 0xe3, 0x43, 0x61, 0x36, 0x2b, 0xb4, 0x6b, 0x85, 0x34, 0x04, 0x37,
0xd6, 0x76, 0x59, 0x14, 0x6d, 0xb7, 0xdb, 0x70, 0x3b, 0x0b, 0x95, 0x5e, 0x45, 0x69, 0x1c, 0xc7, 0x91, 0xd9, 0xac,
0x30, 0x1a, 0x0b, 0x01, 0xa7, 0x37, 0x18, 0x35, 0xc0, 0x57, 0x8d, 0x1d, 0xe0, 0xf2, 0xc5, 0x01, 0x8e, 0x85, 0xe3,
0x28, 0x1f, 0x3e, 0x5d, 0x58, 0xe1, 0x17, 0x56, 0xe0, 0x47, 0xea, 0xe1, 0x53, 0x98, 0x57, 0x43, 0x98, 0xaf, 0x68,
0x8a, 0x52, 0x14, 0x0f, 0x4f, 0x1a, 0x38, 0x78, 0x3a, 0x05, 0x4f, 0x4e, 0xe8, 0xe2, 0xe4, 0xa0, 0x76, 0x11, 0xbc,
0x3e, 0xcb, 0x26, 0x0e, 0xb3, 0x49, 0xe2, 0x47, 0x84, 0x13, 0xf8, 0x65, 0x71, 0x79, 0x0e, 0xd2, 0x0f, 0x97, 0x0c,
0xce, 0x5a, 0x93, 0x7c, 0x58, 0xd0, 0x39, 0x9a, 0x4f, 0x98, 0x79, 0xe0, 0xe0, 0xf3, 0x09, 0xcd, 0x37, 0x69, 0x93,
0xb4, 0xc1, 0x22, 0x98, 0xd3, 0x19, 0x9a, 0x4d, 0x8e, 0xcc, 0xd0, 0x6c, 0x93, 0x36, 0x8b, 0x0f, 0x8b, 0x4b, 0x5c,
0x30, 0xfb, 0x72, 0x15, 0x95, 0xd8, 0xcf, 0x30, 0x7e, 0x8c, 0x5c, 0x5d, 0x46, 0x1e, 0x7e, 0x56, 0x5c, 0x7a, 0x18,
0xfb, 0xc7, 0x1a, 0x2c, 0x6b, 0x3c, 0x1c, 0x31, 0x25, 0x6b, 0xbe, 0x0a, 0x3f, 0x1b, 0x25, 0xb1, 0x1f, 0xda, 0x06,
0xa4, 0x77, 0x12, 0x75, 0x82, 0x30, 0x50, 0xbc, 0xa7, 0x14, 0xeb, 0x1f, 0xce, 0xf5, 0x6f, 0xb9, 0x15, 0x40, 0x6c,
0xe8, 0x1a, 0xf6, 0xfa, 0x8c, 0x5d, 0xaa, 0x6a, 0xff, 0x8d, 0xd6, 0x68, 0x92, 0xb1, 0x2f, 0xb8, 0x94, 0xa0, 0xef,
0x61, 0x67, 0x09, 0x7e, 0xf7, 0xe6, 0x2d, 0x7a, 0x53, 0x55, 0x1a, 0x8c, 0xc9, 0x10, 0x7e, 0x69, 0xc3, 0x96, 0xb2,
0xff, 0x5d, 0x57, 0xf2, 0x95, 0xae, 0x7f, 0xf2, 0x9f, 0x39, 0xfa, 0x1d, 0xec, 0x56, 0xe9, 0xf5, 0xa4, 0xcd, 0xb9,
0x96, 0xbb, 0x0e, 0xd3, 0xc4, 0x86, 0xb4, 0x33, 0xa1, 0x11, 0x9c, 0x81, 0x97, 0xf8, 0x61, 0x4b, 0xbb, 0xc7, 0xa8,
0xe4, 0x29, 0x51, 0x0f, 0x45, 0xc5, 0x37, 0x88, 0x09, 0x6a, 0x0c, 0xc1, 0x72, 0x54, 0x85, 0xd1, 0x33, 0x34, 0xfc,
0x94, 0x64, 0x82, 0xb3, 0x35, 0xc1, 0x7f, 0x31, 0x01, 0x7e, 0xda, 0xff, 0x5a, 0x79, 0x57, 0xc6, 0xf0, 0xea, 0xca,
0x0f, 0x37, 0x54, 0xf4, 0x80, 0x08, 0xb2, 0x0d, 0x37, 0x8f, 0x0e, 0xe6, 0xdf, 0x14, 0xeb, 0xcc, 0xfa, 0xca, 0x0f,
0x6b, 0xc5, 0x7a, 0xe3, 0xf9, 0xb8, 0x9c, 0xcc, 0x15, 0x74, 0x1c, 0x90, 0xf8, 0x39, 0x7e, 0xe2, 0x51, 0x20, 0xa0,
0xb6, 0x67, 0x3e, 0x84, 0x5e, 0x1c, 0x8c, 0x27, 0x43, 0x6d, 0x0c, 0xf7, 0x8f, 0x67, 0x64, 0x61, 0x3a, 0x2a, 0x9f,
0x0a, 0x3a, 0x07, 0x5d, 0xab, 0xc8, 0xd0, 0x41, 0xae, 0x5f, 0x3a, 0x2a, 0xcf, 0x06, 0x23, 0x7a, 0x02, 0x5f, 0x1c,
0xb8, 0x27, 0xdd, 0x14, 0x5c, 0x9f, 0x35, 0x16, 0x51, 0xc5, 0x37, 0xe5, 0xc3, 0xd1, 0x7f, 0x8c, 0xe3, 0x5f, 0x3d,
0xe8, 0xfd, 0x1d, 0x08, 0x60, 0x56, 0x69, 0x0f, 0x3f, 0x97, 0x60, 0xb1, 0x3f, 0x06, 0xfc, 0xcb, 0xfd, 0xbb, 0xdf,
0x88, 0xf2, 0xb4, 0x7f, 0xfd, 0x2d, 0x6e, 0xb7, 0x0a, 0x3e, 0x6a, 0x10, 0xff, 0x26, 0x57, 0x6e, 0x19, 0x5c, 0x7d,
0xc2, 0x7e, 0x38, 0xc4, 0xfb, 0xf0, 0xb8, 0x11, 0x5c, 0x3b, 0xbf, 0xdc, 0xb5, 0xe2, 0xda, 0x45, 0x18, 0x2c, 0xe6,
0xfe, 0xf1, 0xe1, 0xe8, 0x1f, 0xfd, 0xbc, 0x88, 0xc6, 0xb9, 0x5e, 0x16, 0xc3, 0x88, 0x2d, 0x7f, 0x38, 0x2c, 0xd5,
0x2e, 0x30, 0xfc, 0x0b, 0x97, 0xab, 0x8c, 0xcb, 0x06, 0x34, 0xb7, 0xc7, 0x8a, 0x6f, 0xae, 0xb9, 0xec, 0x7a, 0x7b,
0xe8, 0x68, 0x55, 0x39, 0xca, 0xbc, 0xdb, 0xe5, 0xb5, 0x92, 0xd6, 0x71, 0x42, 0x96, 0x40, 0x7b, 0x1c, 0xe9, 0xc3,
0x44, 0xc9, 0x5e, 0xcf, 0xbf, 0x3b, 0xba, 0x82, 0x3b, 0x58, 0xd8, 0xd9, 0x80, 0x0a, 0xbe, 0x92, 0x19, 0x03, 0x69,
0x41, 0x8f, 0x42, 0x35, 0x6d, 0xb9, 0xd8, 0x67, 0x86, 0x4a, 0x13, 0x18, 0xd0, 0xbc, 0x3e, 0x2e, 0x7b, 0x6b, 0x95,
0x3c, 0x2c, 0x95, 0xae, 0x40, 0x67, 0x71, 0x3e, 0x02, 0x81, 0xa6, 0x15, 0xef, 0x4d, 0x16, 0xce, 0x34, 0xb4, 0xf9,
0x92, 0xb2, 0xf5, 0x4a, 0xab, 0x5e, 0x56, 0x01, 0x73, 0x93, 0x36, 0x7b, 0x9e, 0xd4, 0x74, 0x06, 0x2c, 0x9f, 0x4e,
0x75, 0x5d, 0xe7, 0x82, 0x4b, 0x08, 0xc6, 0x59, 0x96, 0xa5, 0xe1, 0x8d, 0x13, 0xbb, 0x70, 0x33, 0x4c, 0x1d, 0x62,
0xf4, 0x31, 0x89, 0xe3, 0xef, 0xf2, 0x53, 0x38, 0x71, 0xce, 0x7a, 0x6d, 0x94, 0xce, 0x3a, 0xc5, 0x9d, 0x9b, 0xc7,
0x96, 0x72, 0x79, 0xe9, 0xbd, 0x2b, 0x93, 0x7c, 0x5a, 0x3f, 0x19, 0x97, 0x83, 0x99, 0x61, 0x09, 0xe5, 0x2d, 0x97,
0xe3, 0x0e, 0xcd, 0xd2, 0x45, 0xdc, 0xed, 0x8e, 0xe1, 0x54, 0x20, 0x87, 0x13, 0x77, 0x2d, 0x60, 0x97, 0x7f, 0xee,
0x8d, 0xe5, 0xf5, 0x3e, 0x98, 0x76, 0x70, 0x66, 0x3a, 0xca, 0x20, 0x58, 0x82, 0xdd, 0x02, 0xc8, 0x7c, 0xb0, 0x11,
0x70, 0x0b, 0xad, 0x99, 0xf2, 0x74, 0x56, 0x33, 0x14, 0xe8, 0xd7, 0xba, 0xfe, 0x1b, 0xb7, 0xab, 0xc5, 0x43, 0x4b,
0xf5, 0x8a, 0xcb, 0x60, 0xa9, 0xac, 0x55, 0x6d, 0x16, 0xbc, 0xea, 0x76, 0xf9, 0x84, 0x72, 0xca, 0xb2, 0xc4, 0xb9,
0x39, 0xec, 0xd6, 0x53, 0xbe, 0x93, 0x6e, 0x87, 0x8c, 0x12, 0xbc, 0x9a, 0xf8, 0x06, 0x16, 0x14, 0x9f, 0xd3, 0x93,
0xcc, 0xbb, 0x1d, 0x72, 0xb8, 0x53, 0xaa, 0x6f, 0xea, 0x5b, 0x9a, 0xc4, 0x7f, 0xf1, 0x45, 0xaa, 0xba, 0x4e, 0x97,
0xf5, 0x39, 0x53, 0x6e, 0x4d, 0xba, 0xd6, 0x18, 0x4a, 0xab, 0x88, 0xc6, 0xdb, 0x8c, 0xab, 0x8c, 0xb2, 0x70, 0x19,
0x2e, 0x8b, 0x26, 0x41, 0xbc, 0x22, 0x2d, 0x65, 0xe5, 0xc5, 0xf8, 0x2a, 0xa2, 0x26, 0x39, 0x91, 0x9a, 0xa4, 0xfc,
0x6a, 0x18, 0x8d, 0xb4, 0xc1, 0xfb, 0xf2, 0xad, 0x92, 0x12, 0x98, 0xe5, 0x72, 0x85, 0xac, 0x42, 0x53, 0x0a, 0xc2,
0x30, 0x2c, 0x96, 0xba, 0x7c, 0x2f, 0x80, 0x1a, 0x40, 0x5b, 0xca, 0x6d, 0x58, 0x44, 0x23, 0xff, 0xd8, 0xc7, 0xbc,
0x22, 0x12, 0x6c, 0x39, 0x35, 0x6c, 0xd1, 0xcc, 0x46, 0x03, 0x77, 0x60, 0x9d, 0x26, 0x67, 0x60, 0x56, 0x16, 0x6e,
0xe5, 0x22, 0x3a, 0x8c, 0x34, 0x12, 0x6d, 0x79, 0xcd, 0xdd, 0x95, 0xa5, 0x2c, 0x86, 0x22, 0x77, 0x1a, 0x5c, 0x9e,
0xc7, 0xeb, 0xd5, 0x00, 0x09, 0x90, 0x2b, 0xdb, 0x90, 0x59, 0x8a, 0x3a, 0x41, 0x19, 0x34, 0x4a, 0x54, 0xa0, 0xc9,
0xdd, 0xdd, 0xaf, 0x7f, 0x2f, 0x9d, 0x33, 0x8f, 0x72, 0x9d, 0x59, 0x8f, 0x62, 0x0e, 0x98, 0xa4, 0x16, 0x37, 0xe3,
0xa5, 0xaa, 0xa3, 0xc6, 0x6c, 0x95, 0xae, 0xbe, 0xd2, 0xf1, 0x7e, 0x42, 0x8e, 0x7a, 0x86, 0xff, 0xd0, 0x2a, 0xe5,
0x1d, 0xdd, 0x40, 0x11, 0x4d, 0x87, 0x22, 0x72, 0x0e, 0x8f, 0xf4, 0x66, 0xe2, 0x6b, 0x92, 0xf2, 0x8f, 0xfb, 0x37,
0xe8, 0xcf, 0xae, 0xa2, 0x16, 0xc6, 0xb4, 0x0d, 0x51, 0xb5, 0x60, 0x1b, 0x55, 0x91, 0xf7, 0x7f, 0xdc, 0xdd, 0x9f,
0x23, 0xec, 0x07, 0x26, 0x04, 0x92, 0x8d, 0xd7, 0xbb, 0x5e, 0x58, 0xde, 0x51, 0x6d, 0x07, 0xb5, 0x81, 0x9b, 0x22,
0xa7, 0x18, 0x06, 0x7a, 0xcd, 0x05, 0x8c, 0x61, 0x8c, 0x82, 0x25, 0x3a, 0x79, 0x75, 0xb2, 0xf6, 0xc4, 0xaf, 0x68,
0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00};
#else // Brotli (default, smaller)
const uint8_t INDEX_BR[] PROGMEM = {

View File

@@ -4,18 +4,24 @@ from typing import Any
import esphome.codegen as cg
from esphome.components import esp32, update
import esphome.config_validation as cv
from esphome.const import CONF_PATH, CONF_RAW_DATA_ID
from esphome.core import CORE, HexInt
from esphome.const import CONF_ID, CONF_PATH, CONF_SOURCE, CONF_TYPE
from esphome.core import CORE, ID, HexInt
CODEOWNERS = ["@swoboda1337"]
AUTO_LOAD = ["sha256", "watchdog"]
AUTO_LOAD = ["sha256", "watchdog", "json"]
DEPENDENCIES = ["esp32_hosted"]
CONF_SHA256 = "sha256"
CONF_HTTP_REQUEST_ID = "http_request_id"
TYPE_EMBEDDED = "embedded"
TYPE_HTTP = "http"
esp32_hosted_ns = cg.esphome_ns.namespace("esp32_hosted")
http_request_ns = cg.esphome_ns.namespace("http_request")
HttpRequestComponent = http_request_ns.class_("HttpRequestComponent", cg.Component)
Esp32HostedUpdate = esp32_hosted_ns.class_(
"Esp32HostedUpdate", update.UpdateEntity, cg.Component
"Esp32HostedUpdate", update.UpdateEntity, cg.PollingComponent
)
@@ -30,12 +36,29 @@ def _validate_sha256(value: Any) -> str:
return value
BASE_SCHEMA = update.update_schema(Esp32HostedUpdate, device_class="firmware").extend(
cv.polling_component_schema("6h")
)
EMBEDDED_SCHEMA = BASE_SCHEMA.extend(
{
cv.Required(CONF_PATH): cv.file_,
cv.Required(CONF_SHA256): _validate_sha256,
}
)
HTTP_SCHEMA = BASE_SCHEMA.extend(
{
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
cv.Required(CONF_SOURCE): cv.url,
}
)
CONFIG_SCHEMA = cv.All(
update.update_schema(Esp32HostedUpdate, device_class="firmware").extend(
cv.typed_schema(
{
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
cv.Required(CONF_PATH): cv.file_,
cv.Required(CONF_SHA256): _validate_sha256,
TYPE_EMBEDDED: EMBEDDED_SCHEMA,
TYPE_HTTP: HTTP_SCHEMA,
}
),
esp32.only_on_variant(
@@ -48,6 +71,9 @@ CONFIG_SCHEMA = cv.All(
def _validate_firmware(config: dict[str, Any]) -> None:
if config[CONF_TYPE] != TYPE_EMBEDDED:
return
path = CORE.relative_config_path(config[CONF_PATH])
with open(path, "rb") as f:
firmware_data = f.read()
@@ -65,14 +91,22 @@ FINAL_VALIDATE_SCHEMA = _validate_firmware
async def to_code(config: dict[str, Any]) -> None:
var = await update.new_update(config)
path = config[CONF_PATH]
with open(CORE.relative_config_path(path), "rb") as f:
firmware_data = f.read()
rhs = [HexInt(x) for x in firmware_data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
if config[CONF_TYPE] == TYPE_EMBEDDED:
path = config[CONF_PATH]
with open(CORE.relative_config_path(path), "rb") as f:
firmware_data = f.read()
rhs = [HexInt(x) for x in firmware_data]
arr_id = ID(f"{config[CONF_ID]}_data", is_declaration=True, type=cg.uint8)
prog_arr = cg.progmem_array(arr_id, rhs)
sha256_bytes = bytes.fromhex(config[CONF_SHA256])
cg.add(var.set_firmware_sha256([HexInt(b) for b in sha256_bytes]))
cg.add(var.set_firmware_data(prog_arr))
cg.add(var.set_firmware_size(len(firmware_data)))
else:
http_request_var = await cg.get_variable(config[CONF_HTTP_REQUEST_ID])
cg.add(var.set_http_request_parent(http_request_var))
cg.add(var.set_source_url(config[CONF_SOURCE]))
cg.add_define("USE_ESP32_HOSTED_HTTP_UPDATE")
sha256_bytes = bytes.fromhex(config[CONF_SHA256])
cg.add(var.set_firmware_sha256([HexInt(b) for b in sha256_bytes]))
cg.add(var.set_firmware_data(prog_arr))
cg.add(var.set_firmware_size(len(firmware_data)))
await cg.register_component(var, config)

View File

@@ -7,6 +7,12 @@
#include <esp_image_format.h>
#include <esp_app_desc.h>
#include <esp_hosted.h>
#include <esp_hosted_host_fw_ver.h>
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
#include "esphome/components/json/json_util.h"
#include "esphome/components/network/util.h"
#endif
extern "C" {
#include <esp_hosted_ota.h>
@@ -16,18 +22,50 @@ namespace esphome::esp32_hosted {
static const char *const TAG = "esp32_hosted.update";
// older coprocessor firmware versions have a 1500-byte limit per RPC call
// Older coprocessor firmware versions have a 1500-byte limit per RPC call
constexpr size_t CHUNK_SIZE = 1500;
// Compile-time version string from esp_hosted_host_fw_ver.h macros
#define STRINGIFY_(x) #x
#define STRINGIFY(x) STRINGIFY_(x)
static const char *const ESP_HOSTED_VERSION_STR = STRINGIFY(ESP_HOSTED_VERSION_MAJOR_1) "." STRINGIFY(
ESP_HOSTED_VERSION_MINOR_1) "." STRINGIFY(ESP_HOSTED_VERSION_PATCH_1);
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
// Parse version string "major.minor.patch" into components
// Returns true if parsing succeeded
static bool parse_version(const std::string &version_str, int &major, int &minor, int &patch) {
major = minor = patch = 0;
if (sscanf(version_str.c_str(), "%d.%d.%d", &major, &minor, &patch) >= 2) {
return true;
}
return false;
}
// Compare two versions, returns:
// -1 if v1 < v2
// 0 if v1 == v2
// 1 if v1 > v2
static int compare_versions(int major1, int minor1, int patch1, int major2, int minor2, int patch2) {
if (major1 != major2)
return major1 < major2 ? -1 : 1;
if (minor1 != minor2)
return minor1 < minor2 ? -1 : 1;
if (patch1 != patch2)
return patch1 < patch2 ? -1 : 1;
return 0;
}
#endif
void Esp32HostedUpdate::setup() {
this->update_info_.title = "ESP32 Hosted Coprocessor";
// if wifi is not present, connect to the coprocessor
#ifndef USE_WIFI
// If WiFi is not present, connect to the coprocessor
esp_hosted_connect_to_slave(); // NOLINT
#endif
// get coprocessor version
// Get coprocessor version
esp_hosted_coprocessor_fwver_t ver_info;
if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) {
this->update_info_.current_version = str_sprintf("%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1);
@@ -36,7 +74,8 @@ void Esp32HostedUpdate::setup() {
}
ESP_LOGD(TAG, "Coprocessor version: %s", this->update_info_.current_version.c_str());
// get image version
#ifndef USE_ESP32_HOSTED_HTTP_UPDATE
// Embedded mode: get image version from embedded firmware
const int app_desc_offset = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t);
if (this->firmware_size_ >= app_desc_offset + sizeof(esp_app_desc_t)) {
esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset);
@@ -64,58 +103,272 @@ void Esp32HostedUpdate::setup() {
this->state_ = update::UPDATE_STATE_NO_UPDATE;
}
// publish state
// Publish state
this->status_clear_error();
this->publish_state();
#else
// HTTP mode: retry initial check every 10s until network is ready (max 6 attempts)
// Only if update interval is > 1 minute to avoid redundant checks
if (this->get_update_interval() > 60000) {
this->set_retry("initial_check", 10000, 6, [this](uint8_t) {
if (!network::is_connected()) {
return RetryResult::RETRY;
}
this->check();
return RetryResult::DONE;
});
}
#endif
}
void Esp32HostedUpdate::dump_config() {
ESP_LOGCONFIG(TAG,
"ESP32 Hosted Update:\n"
" Current Version: %s\n"
" Latest Version: %s\n"
" Latest Size: %zu bytes",
this->update_info_.current_version.c_str(), this->update_info_.latest_version.c_str(),
" Host Library Version: %s\n"
" Coprocessor Version: %s\n"
" Latest Version: %s",
ESP_HOSTED_VERSION_STR, this->update_info_.current_version.c_str(),
this->update_info_.latest_version.c_str());
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
ESP_LOGCONFIG(TAG,
" Mode: HTTP\n"
" Source URL: %s",
this->source_url_.c_str());
#else
ESP_LOGCONFIG(TAG,
" Mode: Embedded\n"
" Firmware Size: %zu bytes",
this->firmware_size_);
#endif
}
void Esp32HostedUpdate::perform(bool force) {
if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
ESP_LOGW(TAG, "Update not available");
void Esp32HostedUpdate::check() {
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
if (!network::is_connected()) {
ESP_LOGD(TAG, "Network not connected, skipping update check");
return;
}
if (!this->fetch_manifest_()) {
return;
}
// Compare versions
if (this->update_info_.latest_version.empty() ||
this->update_info_.latest_version == this->update_info_.current_version) {
this->state_ = update::UPDATE_STATE_NO_UPDATE;
} else {
this->state_ = update::UPDATE_STATE_AVAILABLE;
}
this->update_info_.has_progress = false;
this->update_info_.progress = 0.0f;
this->status_clear_error();
this->publish_state();
#endif
}
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
bool Esp32HostedUpdate::fetch_manifest_() {
ESP_LOGD(TAG, "Fetching manifest");
auto container = this->http_request_parent_->get(this->source_url_);
if (container == nullptr || container->status_code != 200) {
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this->source_url_.c_str());
this->status_set_error(LOG_STR("Failed to fetch manifest"));
return false;
}
// Read manifest JSON into string (manifest is small, ~1KB max)
std::string json_str;
json_str.reserve(container->content_length);
uint8_t buf[256];
while (container->get_bytes_read() < container->content_length) {
int read = container->read(buf, sizeof(buf));
if (read > 0) {
json_str.append(reinterpret_cast<char *>(buf), read);
}
yield();
}
container->end();
// Parse JSON manifest
// Format: {"versions": [{"version": "2.7.0", "url": "...", "sha256": "..."}]}
// Only consider versions <= host library version to avoid compatibility issues
bool valid = json::parse_json(json_str, [this](JsonObject root) -> bool {
if (!root["versions"].is<JsonArray>()) {
ESP_LOGE(TAG, "Manifest does not contain 'versions' array");
return false;
}
JsonArray versions = root["versions"].as<JsonArray>();
if (versions.size() == 0) {
ESP_LOGE(TAG, "Manifest 'versions' array is empty");
return false;
}
// Find the highest version that is compatible with the host library
// (version <= host version to avoid upgrading coprocessor ahead of host)
int best_major = -1, best_minor = -1, best_patch = -1;
std::string best_version, best_url, best_sha256;
for (JsonObject entry : versions) {
if (!entry["version"].is<const char *>() || !entry["url"].is<const char *>() ||
!entry["sha256"].is<const char *>()) {
continue; // Skip malformed entries
}
std::string ver_str = entry["version"].as<std::string>();
int major, minor, patch;
if (!parse_version(ver_str, major, minor, patch)) {
ESP_LOGW(TAG, "Failed to parse version: %s", ver_str.c_str());
continue;
}
// Check if this version is compatible (not newer than host)
if (compare_versions(major, minor, patch, ESP_HOSTED_VERSION_MAJOR_1, ESP_HOSTED_VERSION_MINOR_1,
ESP_HOSTED_VERSION_PATCH_1) > 0) {
continue;
}
// Check if this is better than our current best
if (best_major < 0 || compare_versions(major, minor, patch, best_major, best_minor, best_patch) > 0) {
best_major = major;
best_minor = minor;
best_patch = patch;
best_version = ver_str;
best_url = entry["url"].as<std::string>();
best_sha256 = entry["sha256"].as<std::string>();
}
}
if (best_major < 0) {
ESP_LOGW(TAG, "No compatible firmware version found (host is %s)", ESP_HOSTED_VERSION_STR);
return false;
}
this->update_info_.latest_version = best_version;
this->firmware_url_ = best_url;
// Parse SHA256 hex string to bytes
if (!parse_hex(best_sha256, this->firmware_sha256_.data(), 32)) {
ESP_LOGE(TAG, "Invalid SHA256: %s", best_sha256.c_str());
return false;
}
ESP_LOGD(TAG, "Best compatible version: %s", this->update_info_.latest_version.c_str());
return true;
});
if (!valid) {
ESP_LOGE(TAG, "Failed to parse manifest JSON");
this->status_set_error(LOG_STR("Failed to parse manifest"));
return false;
}
return true;
}
bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
ESP_LOGI(TAG, "Downloading firmware");
auto container = this->http_request_parent_->get(this->firmware_url_);
if (container == nullptr || container->status_code != 200) {
ESP_LOGE(TAG, "Failed to fetch firmware");
this->status_set_error(LOG_STR("Failed to fetch firmware"));
return false;
}
size_t total_size = container->content_length;
ESP_LOGI(TAG, "Firmware size: %zu bytes", total_size);
// Begin OTA on coprocessor
esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err));
container->end();
this->status_set_error(LOG_STR("Failed to begin OTA"));
return false;
}
// Stream firmware to coprocessor while computing SHA256
// Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
alignas(32) sha256::SHA256 hasher;
hasher.init();
uint8_t buffer[CHUNK_SIZE];
while (container->get_bytes_read() < total_size) {
int read = container->read(buffer, sizeof(buffer));
// Feed watchdog and give other tasks a chance to run
App.feed_wdt();
yield();
// Exit loop if no data available (stream closed or end of data)
if (read <= 0) {
if (read < 0) {
ESP_LOGE(TAG, "Stream closed with error");
esp_hosted_slave_ota_end(); // NOLINT
container->end();
this->status_set_error(LOG_STR("Download failed"));
return false;
}
// read == 0: no more data available, exit loop
break;
}
hasher.add(buffer, read);
err = esp_hosted_slave_ota_write(buffer, read); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
esp_hosted_slave_ota_end(); // NOLINT
container->end();
this->status_set_error(LOG_STR("Failed to write OTA data"));
return false;
}
}
container->end();
// Verify SHA256
hasher.calculate();
if (!hasher.equals_bytes(this->firmware_sha256_.data())) {
ESP_LOGE(TAG, "SHA256 mismatch");
esp_hosted_slave_ota_end(); // NOLINT
this->status_set_error(LOG_STR("SHA256 verification failed"));
return false;
}
ESP_LOGI(TAG, "SHA256 verified successfully");
return true;
}
#else
bool Esp32HostedUpdate::write_embedded_firmware_to_coprocessor_() {
if (this->firmware_data_ == nullptr || this->firmware_size_ == 0) {
ESP_LOGE(TAG, "No firmware data available");
return;
this->status_set_error(LOG_STR("No firmware data available"));
return false;
}
// ESP32-S3 hardware SHA acceleration requires 32-byte DMA alignment (IDF 5.5.x+)
// Verify SHA256 before writing
// Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
alignas(32) sha256::SHA256 hasher;
hasher.init();
hasher.add(this->firmware_data_, this->firmware_size_);
hasher.calculate();
if (!hasher.equals_bytes(this->firmware_sha256_.data())) {
ESP_LOGE(TAG, "SHA256 mismatch");
this->status_set_error(LOG_STR("SHA256 verification failed"));
this->publish_state();
return;
return false;
}
ESP_LOGI(TAG, "Starting OTA update (%zu bytes)", this->firmware_size_);
watchdog::WatchdogManager watchdog(20000);
update::UpdateState prev_state = this->state_;
this->state_ = update::UPDATE_STATE_INSTALLING;
this->update_info_.has_progress = false;
this->publish_state();
esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err));
this->state_ = prev_state;
this->status_set_error(LOG_STR("Failed to begin OTA"));
this->publish_state();
return;
return false;
}
uint8_t chunk[CHUNK_SIZE];
@@ -128,42 +381,68 @@ void Esp32HostedUpdate::perform(bool force) {
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
esp_hosted_slave_ota_end(); // NOLINT
this->state_ = prev_state;
this->status_set_error(LOG_STR("Failed to write OTA data"));
this->publish_state();
return;
return false;
}
data_ptr += chunk_size;
remaining -= chunk_size;
App.feed_wdt();
}
err = esp_hosted_slave_ota_end(); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err));
return true;
}
#endif
void Esp32HostedUpdate::perform(bool force) {
if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
ESP_LOGW(TAG, "Update not available");
return;
}
update::UpdateState prev_state = this->state_;
this->state_ = update::UPDATE_STATE_INSTALLING;
this->update_info_.has_progress = false;
this->publish_state();
watchdog::WatchdogManager watchdog(60000);
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
if (!this->stream_firmware_to_coprocessor_())
#else
if (!this->write_embedded_firmware_to_coprocessor_())
#endif
{
this->state_ = prev_state;
this->publish_state();
return;
}
// End OTA and activate new firmware
esp_err_t end_err = esp_hosted_slave_ota_end(); // NOLINT
if (end_err != ESP_OK) {
ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(end_err));
this->state_ = prev_state;
this->status_set_error(LOG_STR("Failed to end OTA"));
this->publish_state();
return;
}
// activate new firmware
err = esp_hosted_slave_ota_activate(); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err));
esp_err_t activate_err = esp_hosted_slave_ota_activate(); // NOLINT
if (activate_err != ESP_OK) {
ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(activate_err));
this->state_ = prev_state;
this->status_set_error(LOG_STR("Failed to activate OTA"));
this->publish_state();
return;
}
// update state
// Update state
ESP_LOGI(TAG, "OTA update successful");
this->state_ = update::UPDATE_STATE_NO_UPDATE;
this->status_clear_error();
this->publish_state();
// schedule a restart to ensure everything is in sync
// Schedule a restart to ensure everything is in sync
ESP_LOGI(TAG, "Restarting in 1 second");
this->set_timeout(1000, []() { App.safe_reboot(); });
}

View File

@@ -5,26 +5,55 @@
#include "esphome/core/component.h"
#include "esphome/components/update/update_entity.h"
#include <array>
#include <string>
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
#include "esphome/components/http_request/http_request.h"
#endif
namespace esphome::esp32_hosted {
class Esp32HostedUpdate : public update::UpdateEntity, public Component {
class Esp32HostedUpdate : public update::UpdateEntity, public PollingComponent {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void update() override { this->check(); } // PollingComponent - delegates to check()
void perform(bool force) override;
void check() override {}
void check() override;
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
// HTTP mode setters
void set_source_url(const std::string &url) { this->source_url_ = url; }
void set_http_request_parent(http_request::HttpRequestComponent *parent) { this->http_request_parent_ = parent; }
#else
// Embedded mode setters
void set_firmware_data(const uint8_t *data) { this->firmware_data_ = data; }
void set_firmware_size(size_t size) { this->firmware_size_ = size; }
void set_firmware_sha256(const std::array<uint8_t, 32> &sha256) { this->firmware_sha256_ = sha256; }
#endif
protected:
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
// HTTP mode members
http_request::HttpRequestComponent *http_request_parent_{nullptr};
std::string source_url_;
std::string firmware_url_;
// HTTP mode helpers
bool fetch_manifest_();
bool stream_firmware_to_coprocessor_();
#else
// Embedded mode members
const uint8_t *firmware_data_{nullptr};
size_t firmware_size_{0};
std::array<uint8_t, 32> firmware_sha256_;
// Embedded mode helper
bool write_embedded_firmware_to_coprocessor_();
#endif
std::array<uint8_t, 32> firmware_sha256_{};
};
} // namespace esphome::esp32_hosted

View File

@@ -136,7 +136,7 @@ bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont
#ifdef USE_WATER_HEATER
bool ListEntitiesIterator::on_water_heater(water_heater::WaterHeater *obj) {
// Water heater web_server support not yet implemented - this stub acknowledges the entity
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::water_heater_all_json_generator);
return true;
}
#endif

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -28,6 +28,10 @@
#include "esphome/components/climate/climate.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
#ifdef USE_WEBSERVER_LOCAL
#if USE_WEBSERVER_VERSION == 2
#include "server_index_v2.h"
@@ -558,7 +562,7 @@ static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const c
// Helper to get request detail parameter
static JsonDetail get_request_detail(AsyncWebServerRequest *request) {
auto *param = request->getParam("detail");
auto *param = request->getParam(ESPHOME_F("detail"));
return (param && param->value() == "all") ? DETAIL_ALL : DETAIL_STATE;
}
@@ -837,10 +841,10 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
}
auto call = is_on ? obj->turn_on() : obj->turn_off();
parse_int_param_(request, "speed_level", call, &decltype(call)::set_speed);
parse_int_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed);
if (request->hasParam("oscillation")) {
auto speed = request->getParam("oscillation")->value();
if (request->hasParam(ESPHOME_F("oscillation"))) {
auto speed = request->getParam(ESPHOME_F("oscillation"))->value();
auto val = parse_on_off(speed.c_str());
switch (val) {
case PARSE_ON:
@@ -920,20 +924,20 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
if (is_on) {
// Parse color parameters
parse_light_param_(request, "brightness", call, &decltype(call)::set_brightness, 255.0f);
parse_light_param_(request, "r", call, &decltype(call)::set_red, 255.0f);
parse_light_param_(request, "g", call, &decltype(call)::set_green, 255.0f);
parse_light_param_(request, "b", call, &decltype(call)::set_blue, 255.0f);
parse_light_param_(request, "white_value", call, &decltype(call)::set_white, 255.0f);
parse_light_param_(request, "color_temp", call, &decltype(call)::set_color_temperature);
parse_light_param_(request, ESPHOME_F("brightness"), call, &decltype(call)::set_brightness, 255.0f);
parse_light_param_(request, ESPHOME_F("r"), call, &decltype(call)::set_red, 255.0f);
parse_light_param_(request, ESPHOME_F("g"), call, &decltype(call)::set_green, 255.0f);
parse_light_param_(request, ESPHOME_F("b"), call, &decltype(call)::set_blue, 255.0f);
parse_light_param_(request, ESPHOME_F("white_value"), call, &decltype(call)::set_white, 255.0f);
parse_light_param_(request, ESPHOME_F("color_temp"), call, &decltype(call)::set_color_temperature);
// Parse timing parameters
parse_light_param_uint_(request, "flash", call, &decltype(call)::set_flash_length, 1000);
parse_light_param_uint_(request, ESPHOME_F("flash"), call, &decltype(call)::set_flash_length, 1000);
}
parse_light_param_uint_(request, "transition", call, &decltype(call)::set_transition_length, 1000);
parse_light_param_uint_(request, ESPHOME_F("transition"), call, &decltype(call)::set_transition_length, 1000);
if (is_on) {
parse_string_param_(request, "effect", call, &decltype(call)::set_effect);
parse_string_param_(request, ESPHOME_F("effect"), call, &decltype(call)::set_effect);
}
this->defer([call]() mutable { call.perform(); });
@@ -1016,14 +1020,14 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
}
auto traits = obj->get_traits();
if ((request->hasParam("position") && !traits.get_supports_position()) ||
(request->hasParam("tilt") && !traits.get_supports_tilt())) {
if ((request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) ||
(request->hasParam(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) {
request->send(409);
return;
}
parse_float_param_(request, "position", call, &decltype(call)::set_position);
parse_float_param_(request, "tilt", call, &decltype(call)::set_tilt);
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
parse_float_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt);
this->defer([call]() mutable { call.perform(); });
request->send(200);
@@ -1082,7 +1086,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
}
auto call = obj->make_call();
parse_float_param_(request, "value", call, &decltype(call)::set_value);
parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
this->defer([call]() mutable { call.perform(); });
request->send(200);
@@ -1150,12 +1154,12 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
auto call = obj->make_call();
if (!request->hasParam("value")) {
if (!request->hasParam(ESPHOME_F("value"))) {
request->send(409);
return;
}
parse_string_param_(request, "value", call, &decltype(call)::set_date);
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date);
this->defer([call]() mutable { call.perform(); });
request->send(200);
@@ -1214,12 +1218,12 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
auto call = obj->make_call();
if (!request->hasParam("value")) {
if (!request->hasParam(ESPHOME_F("value"))) {
request->send(409);
return;
}
parse_string_param_(request, "value", call, &decltype(call)::set_time);
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time);
this->defer([call]() mutable { call.perform(); });
request->send(200);
@@ -1277,12 +1281,12 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
auto call = obj->make_call();
if (!request->hasParam("value")) {
if (!request->hasParam(ESPHOME_F("value"))) {
request->send(409);
return;
}
parse_string_param_(request, "value", call, &decltype(call)::set_datetime);
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime);
this->defer([call]() mutable { call.perform(); });
request->send(200);
@@ -1342,7 +1346,7 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
}
auto call = obj->make_call();
parse_string_param_(request, "value", call, &decltype(call)::set_value);
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
this->defer([call]() mutable { call.perform(); });
request->send(200);
@@ -1400,7 +1404,7 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
}
auto call = obj->make_call();
parse_string_param_(request, "option", call, &decltype(call)::set_option);
parse_string_param_(request, ESPHOME_F("option"), call, &decltype(call)::set_option);
this->defer([call]() mutable { call.perform(); });
request->send(200);
@@ -1460,14 +1464,15 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
auto call = obj->make_call();
// Parse string mode parameters
parse_string_param_(request, "mode", call, &decltype(call)::set_mode);
parse_string_param_(request, "fan_mode", call, &decltype(call)::set_fan_mode);
parse_string_param_(request, "swing_mode", call, &decltype(call)::set_swing_mode);
parse_string_param_(request, ESPHOME_F("mode"), call, &decltype(call)::set_mode);
parse_string_param_(request, ESPHOME_F("fan_mode"), call, &decltype(call)::set_fan_mode);
parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode);
// Parse temperature parameters
parse_float_param_(request, "target_temperature_high", call, &decltype(call)::set_target_temperature_high);
parse_float_param_(request, "target_temperature_low", call, &decltype(call)::set_target_temperature_low);
parse_float_param_(request, "target_temperature", call, &decltype(call)::set_target_temperature);
parse_float_param_(request, ESPHOME_F("target_temperature_high"), call,
&decltype(call)::set_target_temperature_high);
parse_float_param_(request, ESPHOME_F("target_temperature_low"), call, &decltype(call)::set_target_temperature_low);
parse_float_param_(request, ESPHOME_F("target_temperature"), call, &decltype(call)::set_target_temperature);
this->defer([call]() mutable { call.perform(); });
request->send(200);
@@ -1706,12 +1711,12 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
}
auto traits = obj->get_traits();
if (request->hasParam("position") && !traits.get_supports_position()) {
if (request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) {
request->send(409);
return;
}
parse_float_param_(request, "position", call, &decltype(call)::set_position);
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
this->defer([call]() mutable { call.perform(); });
request->send(200);
@@ -1764,7 +1769,7 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
}
auto call = obj->make_call();
parse_string_param_(request, "code", call, &decltype(call)::set_code);
parse_string_param_(request, ESPHOME_F("code"), call, &decltype(call)::set_code);
// Lookup table for alarm control panel methods
static const struct {
@@ -1825,6 +1830,117 @@ std::string WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmContr
}
#endif
#ifdef USE_WATER_HEATER
void WebServer::on_water_heater_update(water_heater::WaterHeater *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", water_heater_state_json_generator);
}
void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (water_heater::WaterHeater *obj : App.get_water_heaters()) {
auto entity_match = match.match_entity(obj);
if (!entity_match.matched)
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
std::string data = this->water_heater_json_(obj, detail);
request->send(200, "application/json", data.c_str());
return;
}
if (!match.method_equals("set")) {
request->send(404);
return;
}
auto call = obj->make_call();
// Use base class reference for template deduction (make_call returns WaterHeaterCallInternal)
water_heater::WaterHeaterCall &base_call = call;
// Parse mode parameter
parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode);
// Parse temperature parameters
parse_float_param_(request, ESPHOME_F("target_temperature"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature);
parse_float_param_(request, ESPHOME_F("target_temperature_low"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature_low);
parse_float_param_(request, ESPHOME_F("target_temperature_high"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature_high);
// Parse away mode parameter
parse_bool_param_(request, ESPHOME_F("away"), base_call, &water_heater::WaterHeaterCall::set_away);
// Parse on/off parameter
parse_bool_param_(request, ESPHOME_F("is_on"), base_call, &water_heater::WaterHeaterCall::set_on);
this->defer([call]() mutable { call.perform(); });
request->send(200);
return;
}
request->send(404);
}
std::string WebServer::water_heater_state_json_generator(WebServer *web_server, void *source) {
return web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_STATE);
}
std::string WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_ALL);
}
std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) {
json::JsonBuilder builder;
JsonObject root = builder.root();
char buf[PSTR_LOCAL_SIZE];
const auto mode = obj->get_mode();
const char *mode_s = PSTR_LOCAL(water_heater::water_heater_mode_to_string(mode));
set_json_icon_state_value(root, obj, "water_heater", mode_s, mode, start_config);
auto traits = obj->get_traits();
if (start_config == DETAIL_ALL) {
JsonArray modes = root[ESPHOME_F("modes")].to<JsonArray>();
for (auto m : traits.get_supported_modes())
modes.add(PSTR_LOCAL(water_heater::water_heater_mode_to_string(m)));
this->add_sorting_info_(root, obj);
}
if (traits.get_supports_current_temperature()) {
float current = obj->get_current_temperature();
if (!std::isnan(current))
root[ESPHOME_F("current_temperature")] = current;
}
if (traits.get_supports_two_point_target_temperature()) {
float low = obj->get_target_temperature_low();
float high = obj->get_target_temperature_high();
if (!std::isnan(low))
root[ESPHOME_F("target_temperature_low")] = low;
if (!std::isnan(high))
root[ESPHOME_F("target_temperature_high")] = high;
} else {
float target = obj->get_target_temperature();
if (!std::isnan(target))
root[ESPHOME_F("target_temperature")] = target;
}
root[ESPHOME_F("min_temperature")] = traits.get_min_temperature();
root[ESPHOME_F("max_temperature")] = traits.get_max_temperature();
root[ESPHOME_F("step")] = traits.get_target_temperature_step();
if (traits.get_supports_away_mode()) {
root[ESPHOME_F("away")] = obj->is_away();
}
if (traits.has_feature_flags(water_heater::WATER_HEATER_SUPPORTS_ON_OFF)) {
root[ESPHOME_F("is_on")] = obj->is_on();
}
return builder.serialize();
}
#endif
#ifdef USE_EVENT
void WebServer::on_event(event::Event *obj) {
if (!this->include_internal_ && obj->is_internal())
@@ -2060,6 +2176,9 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) const {
#endif
#ifdef USE_UPDATE
"update",
#endif
#ifdef USE_WATER_HEATER
"water_heater",
#endif
};
@@ -2220,6 +2339,11 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
else if (match.domain_equals("update")) {
this->handle_update_request(request, match);
}
#endif
#ifdef USE_WATER_HEATER
else if (match.domain_equals("water_heater")) {
this->handle_water_heater_request(request, match);
}
#endif
else {
// No matching handler found - send 404

View File

@@ -35,6 +35,13 @@ extern const size_t ESPHOME_WEBSERVER_JS_INCLUDE_SIZE;
namespace esphome::web_server {
// Type for parameter names that can be stored in flash on ESP8266
#ifdef USE_ESP8266
using ParamNameType = const __FlashStringHelper *;
#else
using ParamNameType = const char *;
#endif
/// Result of matching a URL against an entity
struct EntityMatchResult {
bool matched; ///< True if entity matched the URL
@@ -429,6 +436,16 @@ class WebServer : public Controller,
static std::string alarm_control_panel_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_WATER_HEATER
void on_water_heater_update(water_heater::WaterHeater *obj) override;
/// Handle a water_heater request under '/water_heater/<id>/<mode/set>'.
void handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match);
static std::string water_heater_state_json_generator(WebServer *web_server, void *source);
static std::string water_heater_all_json_generator(WebServer *web_server, void *source);
#endif
#ifdef USE_EVENT
void on_event(event::Event *obj) override;
@@ -472,7 +489,7 @@ class WebServer : public Controller,
#ifdef USE_LIGHT
// Helper to parse and apply a float parameter with optional scaling
template<typename T, typename Ret>
void parse_light_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(float),
void parse_light_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float),
float scale = 1.0f) {
if (request->hasParam(param_name)) {
auto value = parse_number<float>(request->getParam(param_name)->value().c_str());
@@ -484,7 +501,7 @@ class WebServer : public Controller,
// Helper to parse and apply a uint32_t parameter with optional scaling
template<typename T, typename Ret>
void parse_light_param_uint_(AsyncWebServerRequest *request, const char *param_name, T &call,
void parse_light_param_uint_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
Ret (T::*setter)(uint32_t), uint32_t scale = 1) {
if (request->hasParam(param_name)) {
auto value = parse_number<uint32_t>(request->getParam(param_name)->value().c_str());
@@ -497,7 +514,7 @@ class WebServer : public Controller,
// Generic helper to parse and apply a float parameter
template<typename T, typename Ret>
void parse_float_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(float)) {
void parse_float_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float)) {
if (request->hasParam(param_name)) {
auto value = parse_number<float>(request->getParam(param_name)->value().c_str());
if (value.has_value()) {
@@ -508,7 +525,7 @@ class WebServer : public Controller,
// Generic helper to parse and apply an int parameter
template<typename T, typename Ret>
void parse_int_param_(AsyncWebServerRequest *request, const char *param_name, T &call, Ret (T::*setter)(int)) {
void parse_int_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(int)) {
if (request->hasParam(param_name)) {
auto value = parse_number<int>(request->getParam(param_name)->value().c_str());
if (value.has_value()) {
@@ -519,7 +536,7 @@ class WebServer : public Controller,
// Generic helper to parse and apply a string parameter
template<typename T, typename Ret>
void parse_string_param_(AsyncWebServerRequest *request, const char *param_name, T &call,
void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
Ret (T::*setter)(const std::string &)) {
if (request->hasParam(param_name)) {
// .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string
@@ -528,6 +545,28 @@ class WebServer : public Controller,
}
}
// Generic helper to parse and apply a bool parameter
// Accepts: "on", "true", "1" (case-insensitive) as true
// Accepts: "off", "false", "0" (case-insensitive) as false
// Invalid values are ignored (setter not called)
template<typename T, typename Ret>
void parse_bool_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(bool)) {
if (request->hasParam(param_name)) {
auto param_value = request->getParam(param_name)->value();
// First check on/off (default), then true/false (custom)
auto val = parse_on_off(param_value.c_str());
if (val == PARSE_NONE) {
val = parse_on_off(param_value.c_str(), "true", "false");
}
if (val == PARSE_ON || param_value == "1") {
(call.*setter)(true);
} else if (val == PARSE_OFF || param_value == "0") {
(call.*setter)(false);
}
// PARSE_NONE/PARSE_TOGGLE: ignore invalid values
}
}
web_server_base::WebServerBase *base_;
#ifdef USE_ESP32
AsyncEventSource events_{"/events", this};
@@ -606,6 +645,9 @@ class WebServer : public Controller,
#ifdef USE_EVENT
std::string event_json_(event::Event *obj, const std::string &event_type, JsonDetail start_config);
#endif
#ifdef USE_WATER_HEATER
std::string water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config);
#endif
#ifdef USE_UPDATE
std::string update_json_(update::UpdateEntity *obj, JsonDetail start_config);
#endif

View File

@@ -232,6 +232,13 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
}
#endif
#ifdef USE_WATER_HEATER
for (auto *obj : App.get_water_heaters()) {
if (this->include_internal_ || !obj->is_internal())
write_row(stream, obj, "water_heater", "");
}
#endif
stream->print(ESPHOME_F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/\">ESPHome Web API</a> for "
"REST API documentation.</p>"));
#if defined(USE_WEBSERVER_OTA) && !defined(USE_WEBSERVER_OTA_DISABLED)

View File

@@ -69,7 +69,7 @@ def validate_number_of_ep(config: ConfigType) -> None:
raise cv.Invalid(
"Single endpoint is not supported https://github.com/Koenkk/zigbee2mqtt/issues/29888"
)
if count > CONF_MAX_EP_NUMBER:
if count > CONF_MAX_EP_NUMBER and not CORE.testing_mode:
raise cv.Invalid(f"Maximum number of end points is {CONF_MAX_EP_NUMBER}")

View File

@@ -4,10 +4,12 @@ import asyncio
from collections.abc import Callable
from dataclasses import dataclass
import logging
import time
from zeroconf import (
AddressResolver,
IPVersion,
ServiceBrowser,
ServiceInfo,
ServiceStateChange,
Zeroconf,
@@ -200,3 +202,53 @@ class AsyncEsphomeZeroconf(AsyncZeroconf):
) and (addresses := info.parsed_scoped_addresses(IPVersion.All)):
return addresses
return None
def discover_mdns_devices(base_name: str, timeout: float = 5.0) -> list[str]:
"""Discover ESPHome devices via mDNS that match the base name pattern.
When name_add_mac_suffix is enabled, devices advertise as <base_name>-<mac>.local.
This function discovers all such devices on the network.
Args:
base_name: The base device name (without MAC suffix)
timeout: How long to wait for mDNS responses (default 5 seconds)
Returns:
List of discovered device addresses (e.g., ['device-abc123.local'])
"""
discovered: list[str] = []
prefix = f"{base_name}-"
def on_service_state_change(
zeroconf: Zeroconf,
service_type: str,
name: str,
state_change: ServiceStateChange,
) -> None:
if state_change in (ServiceStateChange.Added, ServiceStateChange.Updated):
# Extract device name from service name (removes service type suffix)
device_name = name.partition(".")[0]
# Check if this device matches our base name pattern
if device_name.startswith(prefix) and device_name not in discovered:
discovered.append(device_name)
try:
zc = Zeroconf()
except Exception as err:
_LOGGER.warning("mDNS discovery failed to initialize: %s", err)
return []
browser: ServiceBrowser | None = None
try:
browser = ServiceBrowser(
zc, ESPHOME_SERVICE_TYPE, handlers=[on_service_state_change]
)
# Wait for discovery
time.sleep(timeout)
finally:
if browser is not None:
browser.cancel()
zc.close()
return [f"{name}.local" for name in sorted(discovered)]

View File

@@ -3,5 +3,6 @@
update:
- platform: esp32_hosted
name: "Coprocessor Firmware Update"
type: embedded
path: $component_dir/test_firmware.bin
sha256: de2f256064a0af797747c2b97505dc0b9f3df0de4f489eac731c23ae9ca9cc31

View File

@@ -0,0 +1,10 @@
<<: !include common.yaml
http_request:
update:
- platform: esp32_hosted
name: "Coprocessor Firmware Update"
type: http
source: https://esphome.github.io/esp-hosted-firmware/manifest/esp32c6.json
update_interval: 6h

View File

@@ -14,6 +14,7 @@ from unittest.mock import MagicMock, Mock, patch
import pytest
from pytest import CaptureFixture
from zeroconf import ServiceStateChange
from esphome import platformio_api
from esphome.__main__ import (
@@ -31,6 +32,7 @@ from esphome.__main__ import (
has_mqtt,
has_mqtt_ip_lookup,
has_mqtt_logging,
has_name_add_mac_suffix,
has_non_ip_address,
has_resolvable_address,
mqtt_get_ip,
@@ -52,6 +54,7 @@ from esphome.const import (
CONF_MDNS,
CONF_MQTT,
CONF_NAME,
CONF_NAME_ADD_MAC_SUFFIX,
CONF_OTA,
CONF_PASSWORD,
CONF_PLATFORM,
@@ -68,6 +71,7 @@ from esphome.const import (
PLATFORM_RP2040,
)
from esphome.core import CORE, EsphomeError
from esphome.zeroconf import discover_mdns_devices
def strip_ansi_codes(text: str) -> str:
@@ -1654,6 +1658,110 @@ def test_has_resolvable_address() -> None:
assert has_resolvable_address() is False
def test_has_name_add_mac_suffix() -> None:
"""Test has_name_add_mac_suffix function."""
# Test with name_add_mac_suffix enabled
setup_core(config={CONF_ESPHOME: {CONF_NAME_ADD_MAC_SUFFIX: True}})
assert has_name_add_mac_suffix() is True
# Test with name_add_mac_suffix disabled
setup_core(config={CONF_ESPHOME: {CONF_NAME_ADD_MAC_SUFFIX: False}})
assert has_name_add_mac_suffix() is False
# Test with name_add_mac_suffix not set (defaults to False)
setup_core(config={CONF_ESPHOME: {}})
assert has_name_add_mac_suffix() is False
# Test with no esphome config
setup_core(config={})
assert has_name_add_mac_suffix() is False
# Test with no config at all
CORE.config = None
assert has_name_add_mac_suffix() is False
@pytest.fixture
def mock_mdns_discovery() -> Generator[MagicMock]:
"""Fixture to mock mDNS discovery infrastructure."""
with (
patch("esphome.zeroconf.Zeroconf") as mock_zeroconf_class,
patch("esphome.zeroconf.ServiceBrowser") as mock_browser_class,
patch("esphome.zeroconf.time.sleep"),
):
mock_zc = MagicMock()
mock_zeroconf_class.return_value = mock_zc
mock_browser = MagicMock()
# Store references for test access
mock_zc._mock_browser_class = mock_browser_class
mock_zc._mock_browser = mock_browser
yield mock_zc
@pytest.mark.parametrize(
("discovered_services", "base_name", "expected"),
[
# Test matching devices with filtering
(
[
("mydevice-abc123._esphomelib._tcp.local.", ServiceStateChange.Added),
("mydevice-def456._esphomelib._tcp.local.", ServiceStateChange.Added),
(
"otherdevice-xyz789._esphomelib._tcp.local.",
ServiceStateChange.Added,
),
],
"mydevice",
["mydevice-abc123.local", "mydevice-def456.local"],
),
# Test no matches
(
[
(
"otherdevice-abc123._esphomelib._tcp.local.",
ServiceStateChange.Added,
),
],
"mydevice",
[],
),
# Test deduplication (same device Added then Updated)
(
[
("mydevice-abc123._esphomelib._tcp.local.", ServiceStateChange.Added),
("mydevice-abc123._esphomelib._tcp.local.", ServiceStateChange.Updated),
],
"mydevice",
["mydevice-abc123.local"],
),
],
ids=["matching_with_filter", "no_matches", "deduplication"],
)
def test_discover_mdns_devices(
mock_mdns_discovery: MagicMock,
discovered_services: list[tuple[str, ServiceStateChange]],
base_name: str,
expected: list[str],
) -> None:
"""Test discover_mdns_devices function with various scenarios."""
mock_browser = mock_mdns_discovery._mock_browser
def capture_callback(zc, service_type, handlers):
callback = handlers[0]
for service_name, state_change in discovered_services:
callback(mock_mdns_discovery, service_type, service_name, state_change)
return mock_browser
mock_mdns_discovery._mock_browser_class.side_effect = capture_callback
result = discover_mdns_devices(base_name, timeout=0.1)
assert result == expected
mock_browser.cancel.assert_called_once()
mock_mdns_discovery.close.assert_called_once()
def test_command_wizard(tmp_path: Path) -> None:
"""Test command_wizard function."""
config_file = tmp_path / "test.yaml"