mirror of
https://github.com/esphome/esphome.git
synced 2025-08-09 11:57:46 +00:00
Merge branch 'idf_webserver_ota' into integration
This commit is contained in:
commit
ce294ce0c1
@ -248,6 +248,7 @@ esphome/components/libretiny_pwm/* @kuba2k2
|
|||||||
esphome/components/light/* @esphome/core
|
esphome/components/light/* @esphome/core
|
||||||
esphome/components/lightwaverf/* @max246
|
esphome/components/lightwaverf/* @max246
|
||||||
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
|
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
|
||||||
|
esphome/components/ln882x/* @lamauny
|
||||||
esphome/components/lock/* @esphome/core
|
esphome/components/lock/* @esphome/core
|
||||||
esphome/components/logger/* @esphome/core
|
esphome/components/logger/* @esphome/core
|
||||||
esphome/components/logger/select/* @clydebarrow
|
esphome/components/logger/select/* @clydebarrow
|
||||||
|
@ -34,11 +34,9 @@ from esphome.const import (
|
|||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_SUBSTITUTIONS,
|
CONF_SUBSTITUTIONS,
|
||||||
CONF_TOPIC,
|
CONF_TOPIC,
|
||||||
PLATFORM_BK72XX,
|
|
||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
PLATFORM_RP2040,
|
PLATFORM_RP2040,
|
||||||
PLATFORM_RTL87XX,
|
|
||||||
SECRETS_FILES,
|
SECRETS_FILES,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, EsphomeError, coroutine
|
from esphome.core import CORE, EsphomeError, coroutine
|
||||||
@ -354,7 +352,7 @@ def upload_program(config, args, host):
|
|||||||
if CORE.target_platform in (PLATFORM_RP2040):
|
if CORE.target_platform in (PLATFORM_RP2040):
|
||||||
return upload_using_platformio(config, args.device)
|
return upload_using_platformio(config, args.device)
|
||||||
|
|
||||||
if CORE.target_platform in (PLATFORM_BK72XX, PLATFORM_RTL87XX):
|
if CORE.is_libretiny:
|
||||||
return upload_using_platformio(config, host)
|
return upload_using_platformio(config, host)
|
||||||
|
|
||||||
return 1 # Unknown target platform
|
return 1 # Unknown target platform
|
||||||
|
@ -1537,6 +1537,8 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
|
|||||||
resp.manufacturer = "Raspberry Pi";
|
resp.manufacturer = "Raspberry Pi";
|
||||||
#elif defined(USE_BK72XX)
|
#elif defined(USE_BK72XX)
|
||||||
resp.manufacturer = "Beken";
|
resp.manufacturer = "Beken";
|
||||||
|
#elif defined(USE_LN882X)
|
||||||
|
resp.manufacturer = "Lightning";
|
||||||
#elif defined(USE_RTL87XX)
|
#elif defined(USE_RTL87XX)
|
||||||
resp.manufacturer = "Realtek";
|
resp.manufacturer = "Realtek";
|
||||||
#elif defined(USE_HOST)
|
#elif defined(USE_HOST)
|
||||||
|
@ -5,6 +5,7 @@ from esphome.const import (
|
|||||||
PLATFORM_BK72XX,
|
PLATFORM_BK72XX,
|
||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
|
PLATFORM_LN882X,
|
||||||
PLATFORM_RTL87XX,
|
PLATFORM_RTL87XX,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, coroutine_with_priority
|
from esphome.core import CORE, coroutine_with_priority
|
||||||
@ -14,7 +15,15 @@ CODEOWNERS = ["@OttoWinter"]
|
|||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema({}),
|
cv.Schema({}),
|
||||||
cv.only_with_arduino,
|
cv.only_with_arduino,
|
||||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]),
|
cv.only_on(
|
||||||
|
[
|
||||||
|
PLATFORM_ESP32,
|
||||||
|
PLATFORM_ESP8266,
|
||||||
|
PLATFORM_BK72XX,
|
||||||
|
PLATFORM_LN882X,
|
||||||
|
PLATFORM_RTL87XX,
|
||||||
|
]
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from esphome.const import (
|
|||||||
PLATFORM_BK72XX,
|
PLATFORM_BK72XX,
|
||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
|
PLATFORM_LN882X,
|
||||||
PLATFORM_RTL87XX,
|
PLATFORM_RTL87XX,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, coroutine_with_priority
|
from esphome.core import CORE, coroutine_with_priority
|
||||||
@ -27,7 +28,15 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA),
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]),
|
cv.only_on(
|
||||||
|
[
|
||||||
|
PLATFORM_ESP32,
|
||||||
|
PLATFORM_ESP8266,
|
||||||
|
PLATFORM_BK72XX,
|
||||||
|
PLATFORM_LN882X,
|
||||||
|
PLATFORM_RTL87XX,
|
||||||
|
]
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -47,7 +47,9 @@ void CaptivePortal::start() {
|
|||||||
this->base_->init();
|
this->base_->init();
|
||||||
if (!this->initialized_) {
|
if (!this->initialized_) {
|
||||||
this->base_->add_handler(this);
|
this->base_->add_handler(this);
|
||||||
|
#ifdef USE_WEBSERVER_OTA
|
||||||
this->base_->add_ota_handler();
|
this->base_->add_ota_handler();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
|
@ -100,6 +100,7 @@ CONFIG_SCHEMA = (
|
|||||||
esp32=3232,
|
esp32=3232,
|
||||||
rp2040=2040,
|
rp2040=2040,
|
||||||
bk72xx=8892,
|
bk72xx=8892,
|
||||||
|
ln882x=8820,
|
||||||
rtl87xx=8892,
|
rtl87xx=8892,
|
||||||
): cv.port,
|
): cv.port,
|
||||||
cv.Optional(CONF_PASSWORD): cv.string,
|
cv.Optional(CONF_PASSWORD): cv.string,
|
||||||
|
@ -50,6 +50,7 @@ KEY_FAMILY = "family"
|
|||||||
|
|
||||||
# COMPONENTS - auto-generated! Do not modify this block.
|
# COMPONENTS - auto-generated! Do not modify this block.
|
||||||
COMPONENT_BK72XX = "bk72xx"
|
COMPONENT_BK72XX = "bk72xx"
|
||||||
|
COMPONENT_LN882X = "ln882x"
|
||||||
COMPONENT_RTL87XX = "rtl87xx"
|
COMPONENT_RTL87XX = "rtl87xx"
|
||||||
# COMPONENTS - end
|
# COMPONENTS - end
|
||||||
|
|
||||||
@ -58,6 +59,7 @@ FAMILY_BK7231N = "BK7231N"
|
|||||||
FAMILY_BK7231Q = "BK7231Q"
|
FAMILY_BK7231Q = "BK7231Q"
|
||||||
FAMILY_BK7231T = "BK7231T"
|
FAMILY_BK7231T = "BK7231T"
|
||||||
FAMILY_BK7251 = "BK7251"
|
FAMILY_BK7251 = "BK7251"
|
||||||
|
FAMILY_LN882H = "LN882H"
|
||||||
FAMILY_RTL8710B = "RTL8710B"
|
FAMILY_RTL8710B = "RTL8710B"
|
||||||
FAMILY_RTL8720C = "RTL8720C"
|
FAMILY_RTL8720C = "RTL8720C"
|
||||||
FAMILIES = [
|
FAMILIES = [
|
||||||
@ -65,6 +67,7 @@ FAMILIES = [
|
|||||||
FAMILY_BK7231Q,
|
FAMILY_BK7231Q,
|
||||||
FAMILY_BK7231T,
|
FAMILY_BK7231T,
|
||||||
FAMILY_BK7251,
|
FAMILY_BK7251,
|
||||||
|
FAMILY_LN882H,
|
||||||
FAMILY_RTL8710B,
|
FAMILY_RTL8710B,
|
||||||
FAMILY_RTL8720C,
|
FAMILY_RTL8720C,
|
||||||
]
|
]
|
||||||
@ -73,6 +76,7 @@ FAMILY_FRIENDLY = {
|
|||||||
FAMILY_BK7231Q: "BK7231Q",
|
FAMILY_BK7231Q: "BK7231Q",
|
||||||
FAMILY_BK7231T: "BK7231T",
|
FAMILY_BK7231T: "BK7231T",
|
||||||
FAMILY_BK7251: "BK7251",
|
FAMILY_BK7251: "BK7251",
|
||||||
|
FAMILY_LN882H: "LN882H",
|
||||||
FAMILY_RTL8710B: "RTL8710B",
|
FAMILY_RTL8710B: "RTL8710B",
|
||||||
FAMILY_RTL8720C: "RTL8720C",
|
FAMILY_RTL8720C: "RTL8720C",
|
||||||
}
|
}
|
||||||
@ -81,6 +85,7 @@ FAMILY_COMPONENT = {
|
|||||||
FAMILY_BK7231Q: COMPONENT_BK72XX,
|
FAMILY_BK7231Q: COMPONENT_BK72XX,
|
||||||
FAMILY_BK7231T: COMPONENT_BK72XX,
|
FAMILY_BK7231T: COMPONENT_BK72XX,
|
||||||
FAMILY_BK7251: COMPONENT_BK72XX,
|
FAMILY_BK7251: COMPONENT_BK72XX,
|
||||||
|
FAMILY_LN882H: COMPONENT_LN882X,
|
||||||
FAMILY_RTL8710B: COMPONENT_RTL87XX,
|
FAMILY_RTL8710B: COMPONENT_RTL87XX,
|
||||||
FAMILY_RTL8720C: COMPONENT_RTL87XX,
|
FAMILY_RTL8720C: COMPONENT_RTL87XX,
|
||||||
}
|
}
|
||||||
|
@ -94,6 +94,7 @@ PIN_SCHEMA_EXTRA = f"libretiny.BASE_PIN_SCHEMA.extend({VAR_PIN_SCHEMA})"
|
|||||||
COMPONENT_MAP = {
|
COMPONENT_MAP = {
|
||||||
"rtl87xx": "realtek-amb",
|
"rtl87xx": "realtek-amb",
|
||||||
"bk72xx": "beken-72xx",
|
"bk72xx": "beken-72xx",
|
||||||
|
"ln882x": "lightning-ln882x",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
52
esphome/components/ln882x/__init__.py
Normal file
52
esphome/components/ln882x/__init__.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# This file was auto-generated by libretiny/generate_components.py
|
||||||
|
# Do not modify its contents.
|
||||||
|
# For custom pin validators, put validate_pin() or validate_usage()
|
||||||
|
# in gpio.py file in this directory.
|
||||||
|
# For changing schema/pin schema, put COMPONENT_SCHEMA or COMPONENT_PIN_SCHEMA
|
||||||
|
# in schema.py file in this directory.
|
||||||
|
|
||||||
|
from esphome import pins
|
||||||
|
from esphome.components import libretiny
|
||||||
|
from esphome.components.libretiny.const import (
|
||||||
|
COMPONENT_LN882X,
|
||||||
|
KEY_COMPONENT_DATA,
|
||||||
|
KEY_LIBRETINY,
|
||||||
|
LibreTinyComponent,
|
||||||
|
)
|
||||||
|
from esphome.core import CORE
|
||||||
|
|
||||||
|
from .boards import LN882X_BOARD_PINS, LN882X_BOARDS
|
||||||
|
|
||||||
|
CODEOWNERS = ["@lamauny"]
|
||||||
|
AUTO_LOAD = ["libretiny"]
|
||||||
|
IS_TARGET_PLATFORM = True
|
||||||
|
|
||||||
|
COMPONENT_DATA = LibreTinyComponent(
|
||||||
|
name=COMPONENT_LN882X,
|
||||||
|
boards=LN882X_BOARDS,
|
||||||
|
board_pins=LN882X_BOARD_PINS,
|
||||||
|
pin_validation=None,
|
||||||
|
usage_validation=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_core_data(config):
|
||||||
|
CORE.data[KEY_LIBRETINY] = {}
|
||||||
|
CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] = COMPONENT_DATA
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = libretiny.BASE_SCHEMA
|
||||||
|
|
||||||
|
PIN_SCHEMA = libretiny.gpio.BASE_PIN_SCHEMA
|
||||||
|
|
||||||
|
CONFIG_SCHEMA.prepend_extra(_set_core_data)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
return await libretiny.component_to_code(config)
|
||||||
|
|
||||||
|
|
||||||
|
@pins.PIN_SCHEMA_REGISTRY.register("ln882x", PIN_SCHEMA)
|
||||||
|
async def pin_to_code(config):
|
||||||
|
return await libretiny.gpio.component_pin_to_code(config)
|
285
esphome/components/ln882x/boards.py
Normal file
285
esphome/components/ln882x/boards.py
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
# This file was auto-generated by libretiny/generate_components.py
|
||||||
|
# Do not modify its contents.
|
||||||
|
|
||||||
|
from esphome.components.libretiny.const import FAMILY_LN882H
|
||||||
|
|
||||||
|
LN882X_BOARDS = {
|
||||||
|
"wl2s": {
|
||||||
|
"name": "WL2S Wi-Fi/BLE Module",
|
||||||
|
"family": FAMILY_LN882H,
|
||||||
|
},
|
||||||
|
"ln-02": {
|
||||||
|
"name": "LN-02 Wi-Fi/BLE Module",
|
||||||
|
"family": FAMILY_LN882H,
|
||||||
|
},
|
||||||
|
"generic-ln882hki": {
|
||||||
|
"name": "Generic - LN882HKI",
|
||||||
|
"family": FAMILY_LN882H,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
LN882X_BOARD_PINS = {
|
||||||
|
"wl2s": {
|
||||||
|
"WIRE0_SCL_0": 7,
|
||||||
|
"WIRE0_SCL_1": 12,
|
||||||
|
"WIRE0_SCL_2": 3,
|
||||||
|
"WIRE0_SCL_3": 10,
|
||||||
|
"WIRE0_SCL_4": 2,
|
||||||
|
"WIRE0_SCL_5": 0,
|
||||||
|
"WIRE0_SCL_6": 19,
|
||||||
|
"WIRE0_SCL_7": 11,
|
||||||
|
"WIRE0_SCL_8": 9,
|
||||||
|
"WIRE0_SCL_9": 24,
|
||||||
|
"WIRE0_SCL_10": 25,
|
||||||
|
"WIRE0_SCL_11": 5,
|
||||||
|
"WIRE0_SCL_12": 1,
|
||||||
|
"WIRE0_SDA_0": 7,
|
||||||
|
"WIRE0_SDA_1": 12,
|
||||||
|
"WIRE0_SDA_2": 3,
|
||||||
|
"WIRE0_SDA_3": 10,
|
||||||
|
"WIRE0_SDA_4": 2,
|
||||||
|
"WIRE0_SDA_5": 0,
|
||||||
|
"WIRE0_SDA_6": 19,
|
||||||
|
"WIRE0_SDA_7": 11,
|
||||||
|
"WIRE0_SDA_8": 9,
|
||||||
|
"WIRE0_SDA_9": 24,
|
||||||
|
"WIRE0_SDA_10": 25,
|
||||||
|
"WIRE0_SDA_11": 5,
|
||||||
|
"WIRE0_SDA_12": 1,
|
||||||
|
"SERIAL0_RX": 3,
|
||||||
|
"SERIAL0_TX": 2,
|
||||||
|
"SERIAL1_RX": 24,
|
||||||
|
"SERIAL1_TX": 25,
|
||||||
|
"ADC2": 0,
|
||||||
|
"ADC3": 1,
|
||||||
|
"ADC5": 19,
|
||||||
|
"PA00": 0,
|
||||||
|
"PA0": 0,
|
||||||
|
"PA01": 1,
|
||||||
|
"PA1": 1,
|
||||||
|
"PA02": 2,
|
||||||
|
"PA2": 2,
|
||||||
|
"PA03": 3,
|
||||||
|
"PA3": 3,
|
||||||
|
"PA05": 5,
|
||||||
|
"PA5": 5,
|
||||||
|
"PA07": 7,
|
||||||
|
"PA7": 7,
|
||||||
|
"PA09": 9,
|
||||||
|
"PA9": 9,
|
||||||
|
"PA10": 10,
|
||||||
|
"PA11": 11,
|
||||||
|
"PA12": 12,
|
||||||
|
"PB03": 19,
|
||||||
|
"PB3": 19,
|
||||||
|
"PB08": 24,
|
||||||
|
"PB8": 24,
|
||||||
|
"PB09": 25,
|
||||||
|
"PB9": 25,
|
||||||
|
"RX0": 3,
|
||||||
|
"RX1": 24,
|
||||||
|
"SCL0": 1,
|
||||||
|
"SDA0": 1,
|
||||||
|
"TX0": 2,
|
||||||
|
"TX1": 25,
|
||||||
|
"D0": 7,
|
||||||
|
"D1": 12,
|
||||||
|
"D2": 3,
|
||||||
|
"D3": 10,
|
||||||
|
"D4": 2,
|
||||||
|
"D5": 0,
|
||||||
|
"D6": 19,
|
||||||
|
"D7": 11,
|
||||||
|
"D8": 9,
|
||||||
|
"D9": 24,
|
||||||
|
"D10": 25,
|
||||||
|
"D11": 5,
|
||||||
|
"D12": 1,
|
||||||
|
"A0": 0,
|
||||||
|
"A1": 19,
|
||||||
|
"A2": 1,
|
||||||
|
},
|
||||||
|
"ln-02": {
|
||||||
|
"WIRE0_SCL_0": 11,
|
||||||
|
"WIRE0_SCL_1": 19,
|
||||||
|
"WIRE0_SCL_2": 3,
|
||||||
|
"WIRE0_SCL_3": 24,
|
||||||
|
"WIRE0_SCL_4": 2,
|
||||||
|
"WIRE0_SCL_5": 25,
|
||||||
|
"WIRE0_SCL_6": 1,
|
||||||
|
"WIRE0_SCL_7": 0,
|
||||||
|
"WIRE0_SCL_8": 9,
|
||||||
|
"WIRE0_SDA_0": 11,
|
||||||
|
"WIRE0_SDA_1": 19,
|
||||||
|
"WIRE0_SDA_2": 3,
|
||||||
|
"WIRE0_SDA_3": 24,
|
||||||
|
"WIRE0_SDA_4": 2,
|
||||||
|
"WIRE0_SDA_5": 25,
|
||||||
|
"WIRE0_SDA_6": 1,
|
||||||
|
"WIRE0_SDA_7": 0,
|
||||||
|
"WIRE0_SDA_8": 9,
|
||||||
|
"SERIAL0_RX": 3,
|
||||||
|
"SERIAL0_TX": 2,
|
||||||
|
"SERIAL1_RX": 24,
|
||||||
|
"SERIAL1_TX": 25,
|
||||||
|
"ADC2": 0,
|
||||||
|
"ADC3": 1,
|
||||||
|
"ADC5": 19,
|
||||||
|
"PA00": 0,
|
||||||
|
"PA0": 0,
|
||||||
|
"PA01": 1,
|
||||||
|
"PA1": 1,
|
||||||
|
"PA02": 2,
|
||||||
|
"PA2": 2,
|
||||||
|
"PA03": 3,
|
||||||
|
"PA3": 3,
|
||||||
|
"PA09": 9,
|
||||||
|
"PA9": 9,
|
||||||
|
"PA11": 11,
|
||||||
|
"PB03": 19,
|
||||||
|
"PB3": 19,
|
||||||
|
"PB08": 24,
|
||||||
|
"PB8": 24,
|
||||||
|
"PB09": 25,
|
||||||
|
"PB9": 25,
|
||||||
|
"RX0": 3,
|
||||||
|
"RX1": 24,
|
||||||
|
"SCL0": 9,
|
||||||
|
"SDA0": 9,
|
||||||
|
"TX0": 2,
|
||||||
|
"TX1": 25,
|
||||||
|
"D0": 11,
|
||||||
|
"D1": 19,
|
||||||
|
"D2": 3,
|
||||||
|
"D3": 24,
|
||||||
|
"D4": 2,
|
||||||
|
"D5": 25,
|
||||||
|
"D6": 1,
|
||||||
|
"D7": 0,
|
||||||
|
"D8": 9,
|
||||||
|
"A0": 19,
|
||||||
|
"A1": 1,
|
||||||
|
"A2": 0,
|
||||||
|
},
|
||||||
|
"generic-ln882hki": {
|
||||||
|
"WIRE0_SCL_0": 0,
|
||||||
|
"WIRE0_SCL_1": 1,
|
||||||
|
"WIRE0_SCL_2": 2,
|
||||||
|
"WIRE0_SCL_3": 3,
|
||||||
|
"WIRE0_SCL_4": 4,
|
||||||
|
"WIRE0_SCL_5": 5,
|
||||||
|
"WIRE0_SCL_6": 6,
|
||||||
|
"WIRE0_SCL_7": 7,
|
||||||
|
"WIRE0_SCL_8": 8,
|
||||||
|
"WIRE0_SCL_9": 9,
|
||||||
|
"WIRE0_SCL_10": 10,
|
||||||
|
"WIRE0_SCL_11": 11,
|
||||||
|
"WIRE0_SCL_12": 12,
|
||||||
|
"WIRE0_SCL_13": 19,
|
||||||
|
"WIRE0_SCL_14": 20,
|
||||||
|
"WIRE0_SCL_15": 21,
|
||||||
|
"WIRE0_SCL_16": 22,
|
||||||
|
"WIRE0_SCL_17": 23,
|
||||||
|
"WIRE0_SCL_18": 24,
|
||||||
|
"WIRE0_SCL_19": 25,
|
||||||
|
"WIRE0_SDA_0": 0,
|
||||||
|
"WIRE0_SDA_1": 1,
|
||||||
|
"WIRE0_SDA_2": 2,
|
||||||
|
"WIRE0_SDA_3": 3,
|
||||||
|
"WIRE0_SDA_4": 4,
|
||||||
|
"WIRE0_SDA_5": 5,
|
||||||
|
"WIRE0_SDA_6": 6,
|
||||||
|
"WIRE0_SDA_7": 7,
|
||||||
|
"WIRE0_SDA_8": 8,
|
||||||
|
"WIRE0_SDA_9": 9,
|
||||||
|
"WIRE0_SDA_10": 10,
|
||||||
|
"WIRE0_SDA_11": 11,
|
||||||
|
"WIRE0_SDA_12": 12,
|
||||||
|
"WIRE0_SDA_13": 19,
|
||||||
|
"WIRE0_SDA_14": 20,
|
||||||
|
"WIRE0_SDA_15": 21,
|
||||||
|
"WIRE0_SDA_16": 22,
|
||||||
|
"WIRE0_SDA_17": 23,
|
||||||
|
"WIRE0_SDA_18": 24,
|
||||||
|
"WIRE0_SDA_19": 25,
|
||||||
|
"SERIAL0_RX": 3,
|
||||||
|
"SERIAL0_TX": 2,
|
||||||
|
"SERIAL1_RX": 24,
|
||||||
|
"SERIAL1_TX": 25,
|
||||||
|
"ADC2": 0,
|
||||||
|
"ADC3": 1,
|
||||||
|
"ADC4": 4,
|
||||||
|
"ADC5": 19,
|
||||||
|
"ADC6": 20,
|
||||||
|
"ADC7": 21,
|
||||||
|
"PA00": 0,
|
||||||
|
"PA0": 0,
|
||||||
|
"PA01": 1,
|
||||||
|
"PA1": 1,
|
||||||
|
"PA02": 2,
|
||||||
|
"PA2": 2,
|
||||||
|
"PA03": 3,
|
||||||
|
"PA3": 3,
|
||||||
|
"PA04": 4,
|
||||||
|
"PA4": 4,
|
||||||
|
"PA05": 5,
|
||||||
|
"PA5": 5,
|
||||||
|
"PA06": 6,
|
||||||
|
"PA6": 6,
|
||||||
|
"PA07": 7,
|
||||||
|
"PA7": 7,
|
||||||
|
"PA08": 8,
|
||||||
|
"PA8": 8,
|
||||||
|
"PA09": 9,
|
||||||
|
"PA9": 9,
|
||||||
|
"PA10": 10,
|
||||||
|
"PA11": 11,
|
||||||
|
"PA12": 12,
|
||||||
|
"PB03": 19,
|
||||||
|
"PB3": 19,
|
||||||
|
"PB04": 20,
|
||||||
|
"PB4": 20,
|
||||||
|
"PB05": 21,
|
||||||
|
"PB5": 21,
|
||||||
|
"PB06": 22,
|
||||||
|
"PB6": 22,
|
||||||
|
"PB07": 23,
|
||||||
|
"PB7": 23,
|
||||||
|
"PB08": 24,
|
||||||
|
"PB8": 24,
|
||||||
|
"PB09": 25,
|
||||||
|
"PB9": 25,
|
||||||
|
"RX0": 3,
|
||||||
|
"RX1": 24,
|
||||||
|
"TX0": 2,
|
||||||
|
"TX1": 25,
|
||||||
|
"D0": 0,
|
||||||
|
"D1": 1,
|
||||||
|
"D2": 2,
|
||||||
|
"D3": 3,
|
||||||
|
"D4": 4,
|
||||||
|
"D5": 5,
|
||||||
|
"D6": 6,
|
||||||
|
"D7": 7,
|
||||||
|
"D8": 8,
|
||||||
|
"D9": 9,
|
||||||
|
"D10": 10,
|
||||||
|
"D11": 11,
|
||||||
|
"D12": 12,
|
||||||
|
"D13": 19,
|
||||||
|
"D14": 20,
|
||||||
|
"D15": 21,
|
||||||
|
"D16": 22,
|
||||||
|
"D17": 23,
|
||||||
|
"D18": 24,
|
||||||
|
"D19": 25,
|
||||||
|
"A2": 0,
|
||||||
|
"A3": 1,
|
||||||
|
"A4": 4,
|
||||||
|
"A5": 19,
|
||||||
|
"A6": 20,
|
||||||
|
"A7": 21,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
BOARDS = LN882X_BOARDS
|
@ -16,7 +16,11 @@ from esphome.components.esp32.const import (
|
|||||||
VARIANT_ESP32S3,
|
VARIANT_ESP32S3,
|
||||||
)
|
)
|
||||||
from esphome.components.libretiny import get_libretiny_component, get_libretiny_family
|
from esphome.components.libretiny import get_libretiny_component, get_libretiny_family
|
||||||
from esphome.components.libretiny.const import COMPONENT_BK72XX, COMPONENT_RTL87XX
|
from esphome.components.libretiny.const import (
|
||||||
|
COMPONENT_BK72XX,
|
||||||
|
COMPONENT_LN882X,
|
||||||
|
COMPONENT_RTL87XX,
|
||||||
|
)
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ARGS,
|
CONF_ARGS,
|
||||||
@ -35,6 +39,7 @@ from esphome.const import (
|
|||||||
PLATFORM_BK72XX,
|
PLATFORM_BK72XX,
|
||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
|
PLATFORM_LN882X,
|
||||||
PLATFORM_RP2040,
|
PLATFORM_RP2040,
|
||||||
PLATFORM_RTL87XX,
|
PLATFORM_RTL87XX,
|
||||||
)
|
)
|
||||||
@ -100,6 +105,7 @@ UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1]
|
|||||||
|
|
||||||
UART_SELECTION_LIBRETINY = {
|
UART_SELECTION_LIBRETINY = {
|
||||||
COMPONENT_BK72XX: [DEFAULT, UART1, UART2],
|
COMPONENT_BK72XX: [DEFAULT, UART1, UART2],
|
||||||
|
COMPONENT_LN882X: [DEFAULT, UART0, UART1, UART2],
|
||||||
COMPONENT_RTL87XX: [DEFAULT, UART0, UART1, UART2],
|
COMPONENT_RTL87XX: [DEFAULT, UART0, UART1, UART2],
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,6 +223,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
esp32_p4_idf=USB_SERIAL_JTAG,
|
esp32_p4_idf=USB_SERIAL_JTAG,
|
||||||
rp2040=USB_CDC,
|
rp2040=USB_CDC,
|
||||||
bk72xx=DEFAULT,
|
bk72xx=DEFAULT,
|
||||||
|
ln882x=DEFAULT,
|
||||||
rtl87xx=DEFAULT,
|
rtl87xx=DEFAULT,
|
||||||
): cv.All(
|
): cv.All(
|
||||||
cv.only_on(
|
cv.only_on(
|
||||||
@ -225,6 +232,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
PLATFORM_RP2040,
|
PLATFORM_RP2040,
|
||||||
PLATFORM_BK72XX,
|
PLATFORM_BK72XX,
|
||||||
|
PLATFORM_LN882X,
|
||||||
PLATFORM_RTL87XX,
|
PLATFORM_RTL87XX,
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include <esp_ota_ops.h>
|
#include <esp_ota_ops.h>
|
||||||
#include <esp_task_wdt.h>
|
#include <esp_task_wdt.h>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||||
#include <spi_flash_mmap.h>
|
#include <spi_flash_mmap.h>
|
||||||
@ -17,6 +18,9 @@ namespace ota {
|
|||||||
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::IDFOTABackend>(); }
|
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::IDFOTABackend>(); }
|
||||||
|
|
||||||
OTAResponseTypes IDFOTABackend::begin(size_t image_size) {
|
OTAResponseTypes IDFOTABackend::begin(size_t image_size) {
|
||||||
|
// Reset MD5 validation state
|
||||||
|
this->md5_set_ = false;
|
||||||
|
|
||||||
this->partition_ = esp_ota_get_next_update_partition(nullptr);
|
this->partition_ = esp_ota_get_next_update_partition(nullptr);
|
||||||
if (this->partition_ == nullptr) {
|
if (this->partition_ == nullptr) {
|
||||||
return OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION;
|
return OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION;
|
||||||
@ -67,7 +71,10 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size) {
|
|||||||
return OTA_RESPONSE_OK;
|
return OTA_RESPONSE_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
void IDFOTABackend::set_update_md5(const char *expected_md5) { memcpy(this->expected_bin_md5_, expected_md5, 32); }
|
void IDFOTABackend::set_update_md5(const char *expected_md5) {
|
||||||
|
memcpy(this->expected_bin_md5_, expected_md5, 32);
|
||||||
|
this->md5_set_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) {
|
OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) {
|
||||||
esp_err_t err = esp_ota_write(this->update_handle_, data, len);
|
esp_err_t err = esp_ota_write(this->update_handle_, data, len);
|
||||||
@ -85,10 +92,15 @@ OTAResponseTypes IDFOTABackend::write(uint8_t *data, size_t len) {
|
|||||||
|
|
||||||
OTAResponseTypes IDFOTABackend::end() {
|
OTAResponseTypes IDFOTABackend::end() {
|
||||||
this->md5_.calculate();
|
this->md5_.calculate();
|
||||||
if (!this->md5_.equals_hex(this->expected_bin_md5_)) {
|
|
||||||
this->abort();
|
// Only validate MD5 if one was provided
|
||||||
return OTA_RESPONSE_ERROR_MD5_MISMATCH;
|
if (this->md5_set_) {
|
||||||
|
if (!this->md5_.equals_hex(this->expected_bin_md5_)) {
|
||||||
|
this->abort();
|
||||||
|
return OTA_RESPONSE_ERROR_MD5_MISMATCH;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t err = esp_ota_end(this->update_handle_);
|
esp_err_t err = esp_ota_end(this->update_handle_);
|
||||||
this->update_handle_ = 0;
|
this->update_handle_ = 0;
|
||||||
if (err == ESP_OK) {
|
if (err == ESP_OK) {
|
||||||
|
@ -12,6 +12,7 @@ namespace ota {
|
|||||||
|
|
||||||
class IDFOTABackend : public OTABackend {
|
class IDFOTABackend : public OTABackend {
|
||||||
public:
|
public:
|
||||||
|
IDFOTABackend() : md5_set_(false), expected_bin_md5_{} {}
|
||||||
OTAResponseTypes begin(size_t image_size) override;
|
OTAResponseTypes begin(size_t image_size) override;
|
||||||
void set_update_md5(const char *md5) override;
|
void set_update_md5(const char *md5) override;
|
||||||
OTAResponseTypes write(uint8_t *data, size_t len) override;
|
OTAResponseTypes write(uint8_t *data, size_t len) override;
|
||||||
@ -24,6 +25,7 @@ class IDFOTABackend : public OTABackend {
|
|||||||
const esp_partition_t *partition_;
|
const esp_partition_t *partition_;
|
||||||
md5::MD5Digest md5_{};
|
md5::MD5Digest md5_{};
|
||||||
char expected_bin_md5_[32];
|
char expected_bin_md5_[32];
|
||||||
|
bool md5_set_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ota
|
} // namespace ota
|
||||||
|
@ -94,6 +94,7 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
|
|||||||
esp32="10000b",
|
esp32="10000b",
|
||||||
esp8266="1000b",
|
esp8266="1000b",
|
||||||
bk72xx="1000b",
|
bk72xx="1000b",
|
||||||
|
ln882x="1000b",
|
||||||
rtl87xx="1000b",
|
rtl87xx="1000b",
|
||||||
): cv.validate_bytes,
|
): cv.validate_bytes,
|
||||||
cv.Optional(CONF_FILTER, default="50us"): cv.All(
|
cv.Optional(CONF_FILTER, default="50us"): cv.All(
|
||||||
|
@ -7,6 +7,7 @@ from esphome.const import (
|
|||||||
PLATFORM_BK72XX,
|
PLATFORM_BK72XX,
|
||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
|
PLATFORM_LN882X,
|
||||||
PLATFORM_RP2040,
|
PLATFORM_RP2040,
|
||||||
PLATFORM_RTL87XX,
|
PLATFORM_RTL87XX,
|
||||||
)
|
)
|
||||||
@ -33,6 +34,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
PLATFORM_RP2040,
|
PLATFORM_RP2040,
|
||||||
PLATFORM_BK72XX,
|
PLATFORM_BK72XX,
|
||||||
|
PLATFORM_LN882X,
|
||||||
PLATFORM_RTL87XX,
|
PLATFORM_RTL87XX,
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
@ -16,6 +16,7 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
esp32=IMPLEMENTATION_BSD_SOCKETS,
|
esp32=IMPLEMENTATION_BSD_SOCKETS,
|
||||||
rp2040=IMPLEMENTATION_LWIP_TCP,
|
rp2040=IMPLEMENTATION_LWIP_TCP,
|
||||||
bk72xx=IMPLEMENTATION_LWIP_SOCKETS,
|
bk72xx=IMPLEMENTATION_LWIP_SOCKETS,
|
||||||
|
ln882x=IMPLEMENTATION_LWIP_SOCKETS,
|
||||||
rtl87xx=IMPLEMENTATION_LWIP_SOCKETS,
|
rtl87xx=IMPLEMENTATION_LWIP_SOCKETS,
|
||||||
host=IMPLEMENTATION_BSD_SOCKETS,
|
host=IMPLEMENTATION_BSD_SOCKETS,
|
||||||
): cv.one_of(
|
): cv.one_of(
|
||||||
|
@ -28,6 +28,7 @@ from esphome.const import (
|
|||||||
PLATFORM_BK72XX,
|
PLATFORM_BK72XX,
|
||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
|
PLATFORM_LN882X,
|
||||||
PLATFORM_RTL87XX,
|
PLATFORM_RTL87XX,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE, coroutine_with_priority
|
from esphome.core import CORE, coroutine_with_priority
|
||||||
@ -71,12 +72,6 @@ def validate_local(config):
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def validate_ota(config):
|
|
||||||
if CORE.using_esp_idf and config[CONF_OTA]:
|
|
||||||
raise cv.Invalid("Enabling 'ota' is not supported for IDF framework yet")
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def validate_sorting_groups(config):
|
def validate_sorting_groups(config):
|
||||||
if CONF_SORTING_GROUPS in config and config[CONF_VERSION] != 3:
|
if CONF_SORTING_GROUPS in config and config[CONF_VERSION] != 3:
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
@ -174,23 +169,23 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
web_server_base.WebServerBase
|
web_server_base.WebServerBase
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean,
|
cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean,
|
||||||
cv.SplitDefault(
|
cv.Optional(CONF_OTA, default=True): cv.boolean,
|
||||||
CONF_OTA,
|
|
||||||
esp8266=True,
|
|
||||||
esp32_arduino=True,
|
|
||||||
esp32_idf=False,
|
|
||||||
bk72xx=True,
|
|
||||||
rtl87xx=True,
|
|
||||||
): cv.boolean,
|
|
||||||
cv.Optional(CONF_LOG, default=True): cv.boolean,
|
cv.Optional(CONF_LOG, default=True): cv.boolean,
|
||||||
cv.Optional(CONF_LOCAL): cv.boolean,
|
cv.Optional(CONF_LOCAL): cv.boolean,
|
||||||
cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group),
|
cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group),
|
||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA),
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]),
|
cv.only_on(
|
||||||
|
[
|
||||||
|
PLATFORM_ESP32,
|
||||||
|
PLATFORM_ESP8266,
|
||||||
|
PLATFORM_BK72XX,
|
||||||
|
PLATFORM_LN882X,
|
||||||
|
PLATFORM_RTL87XX,
|
||||||
|
]
|
||||||
|
),
|
||||||
default_url,
|
default_url,
|
||||||
validate_local,
|
validate_local,
|
||||||
validate_ota,
|
|
||||||
validate_sorting_groups,
|
validate_sorting_groups,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -276,6 +271,8 @@ async def to_code(config):
|
|||||||
cg.add(var.set_css_url(config[CONF_CSS_URL]))
|
cg.add(var.set_css_url(config[CONF_CSS_URL]))
|
||||||
cg.add(var.set_js_url(config[CONF_JS_URL]))
|
cg.add(var.set_js_url(config[CONF_JS_URL]))
|
||||||
cg.add(var.set_allow_ota(config[CONF_OTA]))
|
cg.add(var.set_allow_ota(config[CONF_OTA]))
|
||||||
|
if config[CONF_OTA] and "ota" in CORE.config:
|
||||||
|
cg.add_define("USE_WEBSERVER_OTA")
|
||||||
cg.add(var.set_expose_log(config[CONF_LOG]))
|
cg.add(var.set_expose_log(config[CONF_LOG]))
|
||||||
if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]:
|
if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]:
|
||||||
cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS")
|
cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS")
|
||||||
|
@ -299,8 +299,10 @@ void WebServer::setup() {
|
|||||||
#endif
|
#endif
|
||||||
this->base_->add_handler(this);
|
this->base_->add_handler(this);
|
||||||
|
|
||||||
|
#ifdef USE_WEBSERVER_OTA
|
||||||
if (this->allow_ota_)
|
if (this->allow_ota_)
|
||||||
this->base_->add_ota_handler();
|
this->base_->add_ota_handler();
|
||||||
|
#endif
|
||||||
|
|
||||||
// doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly
|
// doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly
|
||||||
// getting a lot of events
|
// getting a lot of events
|
||||||
@ -2030,6 +2032,10 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// No matching handler found - send 404
|
||||||
|
ESP_LOGV(TAG, "Request for unknown URL: %s", request->url().c_str());
|
||||||
|
request->send(404, "text/plain", "Not Found");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WebServer::isRequestHandlerTrivial() const { return false; }
|
bool WebServer::isRequestHandlerTrivial() const { return false; }
|
||||||
|
@ -14,6 +14,10 @@
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)
|
||||||
|
#include "esphome/components/ota/ota_backend.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace web_server_base {
|
namespace web_server_base {
|
||||||
|
|
||||||
@ -31,6 +35,33 @@ void WebServerBase::add_handler(AsyncWebHandler *handler) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_WEBSERVER_OTA
|
||||||
|
void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
|
||||||
|
const uint32_t now = millis();
|
||||||
|
if (now - this->last_ota_progress_ > 1000) {
|
||||||
|
if (request->contentLength() != 0) {
|
||||||
|
float percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
|
||||||
|
ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
|
||||||
|
} else {
|
||||||
|
ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
|
||||||
|
}
|
||||||
|
this->last_ota_progress_ = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OTARequestHandler::schedule_ota_reboot_() {
|
||||||
|
ESP_LOGI(TAG, "OTA update successful!");
|
||||||
|
this->parent_->set_timeout(100, [this]() {
|
||||||
|
ESP_LOGI(TAG, "Performing OTA reboot now");
|
||||||
|
App.safe_reboot();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void OTARequestHandler::ota_init_(const char *filename) {
|
||||||
|
ESP_LOGI(TAG, "OTA Update Start: %s", filename);
|
||||||
|
this->ota_read_length_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
void report_ota_error() {
|
void report_ota_error() {
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
StreamString ss;
|
StreamString ss;
|
||||||
@ -44,8 +75,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin
|
|||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
bool success;
|
bool success;
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
ESP_LOGI(TAG, "OTA Update Start: %s", filename.c_str());
|
this->ota_init_(filename.c_str());
|
||||||
this->ota_read_length_ = 0;
|
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ESP8266
|
||||||
Update.runAsync(true);
|
Update.runAsync(true);
|
||||||
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
|
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
|
||||||
@ -72,31 +102,67 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this->ota_read_length_ += len;
|
this->ota_read_length_ += len;
|
||||||
|
this->report_ota_progress_(request);
|
||||||
const uint32_t now = millis();
|
|
||||||
if (now - this->last_ota_progress_ > 1000) {
|
|
||||||
if (request->contentLength() != 0) {
|
|
||||||
float percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
|
|
||||||
ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
|
|
||||||
} else {
|
|
||||||
ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
|
|
||||||
}
|
|
||||||
this->last_ota_progress_ = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (final) {
|
if (final) {
|
||||||
if (Update.end(true)) {
|
if (Update.end(true)) {
|
||||||
ESP_LOGI(TAG, "OTA update successful!");
|
this->schedule_ota_reboot_();
|
||||||
this->parent_->set_timeout(100, []() { App.safe_reboot(); });
|
|
||||||
} else {
|
} else {
|
||||||
report_ota_error();
|
report_ota_error();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif // USE_ARDUINO
|
||||||
|
|
||||||
|
#ifdef USE_ESP_IDF
|
||||||
|
// ESP-IDF implementation
|
||||||
|
if (index == 0 && !this->ota_backend_) {
|
||||||
|
// Initialize OTA on first call
|
||||||
|
this->ota_init_(filename.c_str());
|
||||||
|
this->ota_success_ = false;
|
||||||
|
|
||||||
|
auto backend = ota::make_ota_backend();
|
||||||
|
if (backend->begin(0) != ota::OTA_RESPONSE_OK) {
|
||||||
|
ESP_LOGE(TAG, "OTA begin failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->ota_backend_ = backend.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *backend = static_cast<ota::OTABackend *>(this->ota_backend_);
|
||||||
|
if (!backend) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process data
|
||||||
|
if (len > 0) {
|
||||||
|
if (backend->write(data, len) != ota::OTA_RESPONSE_OK) {
|
||||||
|
ESP_LOGE(TAG, "OTA write failed");
|
||||||
|
backend->abort();
|
||||||
|
delete backend;
|
||||||
|
this->ota_backend_ = nullptr;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->ota_read_length_ += len;
|
||||||
|
this->report_ota_progress_(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finalize
|
||||||
|
if (final) {
|
||||||
|
this->ota_success_ = (backend->end() == ota::OTA_RESPONSE_OK);
|
||||||
|
if (this->ota_success_) {
|
||||||
|
this->schedule_ota_reboot_();
|
||||||
|
} else {
|
||||||
|
ESP_LOGE(TAG, "OTA end failed");
|
||||||
|
}
|
||||||
|
delete backend;
|
||||||
|
this->ota_backend_ = nullptr;
|
||||||
|
}
|
||||||
|
#endif // USE_ESP_IDF
|
||||||
}
|
}
|
||||||
|
|
||||||
void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) {
|
void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) {
|
||||||
#ifdef USE_ARDUINO
|
|
||||||
AsyncWebServerResponse *response;
|
AsyncWebServerResponse *response;
|
||||||
|
#ifdef USE_ARDUINO
|
||||||
if (!Update.hasError()) {
|
if (!Update.hasError()) {
|
||||||
response = request->beginResponse(200, "text/plain", "Update Successful!");
|
response = request->beginResponse(200, "text/plain", "Update Successful!");
|
||||||
} else {
|
} else {
|
||||||
@ -105,16 +171,21 @@ void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) {
|
|||||||
Update.printError(ss);
|
Update.printError(ss);
|
||||||
response = request->beginResponse(200, "text/plain", ss);
|
response = request->beginResponse(200, "text/plain", ss);
|
||||||
}
|
}
|
||||||
|
#endif // USE_ARDUINO
|
||||||
|
#ifdef USE_ESP_IDF
|
||||||
|
// Send response based on the OTA result
|
||||||
|
request->send(200, "text/plain", this->ota_success_ ? "Update Successful!" : "Update Failed!");
|
||||||
|
return;
|
||||||
|
#endif // USE_ESP_IDF
|
||||||
response->addHeader("Connection", "close");
|
response->addHeader("Connection", "close");
|
||||||
request->send(response);
|
request->send(response);
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebServerBase::add_ota_handler() {
|
void WebServerBase::add_ota_handler() {
|
||||||
#ifdef USE_ARDUINO
|
|
||||||
this->add_handler(new OTARequestHandler(this)); // NOLINT
|
this->add_handler(new OTARequestHandler(this)); // NOLINT
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
float WebServerBase::get_setup_priority() const {
|
float WebServerBase::get_setup_priority() const {
|
||||||
// Before WiFi (captive portal)
|
// Before WiFi (captive portal)
|
||||||
return setup_priority::WIFI + 2.0f;
|
return setup_priority::WIFI + 2.0f;
|
||||||
|
@ -110,13 +110,17 @@ class WebServerBase : public Component {
|
|||||||
|
|
||||||
void add_handler(AsyncWebHandler *handler);
|
void add_handler(AsyncWebHandler *handler);
|
||||||
|
|
||||||
|
#ifdef USE_WEBSERVER_OTA
|
||||||
void add_ota_handler();
|
void add_ota_handler();
|
||||||
|
#endif
|
||||||
|
|
||||||
void set_port(uint16_t port) { port_ = port; }
|
void set_port(uint16_t port) { port_ = port; }
|
||||||
uint16_t get_port() const { return port_; }
|
uint16_t get_port() const { return port_; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
#ifdef USE_WEBSERVER_OTA
|
||||||
friend class OTARequestHandler;
|
friend class OTARequestHandler;
|
||||||
|
#endif
|
||||||
|
|
||||||
int initialized_{0};
|
int initialized_{0};
|
||||||
uint16_t port_{80};
|
uint16_t port_{80};
|
||||||
@ -125,6 +129,7 @@ class WebServerBase : public Component {
|
|||||||
internal::Credentials credentials_;
|
internal::Credentials credentials_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#ifdef USE_WEBSERVER_OTA
|
||||||
class OTARequestHandler : public AsyncWebHandler {
|
class OTARequestHandler : public AsyncWebHandler {
|
||||||
public:
|
public:
|
||||||
OTARequestHandler(WebServerBase *parent) : parent_(parent) {}
|
OTARequestHandler(WebServerBase *parent) : parent_(parent) {}
|
||||||
@ -139,10 +144,21 @@ class OTARequestHandler : public AsyncWebHandler {
|
|||||||
bool isRequestHandlerTrivial() const override { return false; }
|
bool isRequestHandlerTrivial() const override { return false; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void report_ota_progress_(AsyncWebServerRequest *request);
|
||||||
|
void schedule_ota_reboot_();
|
||||||
|
void ota_init_(const char *filename);
|
||||||
|
|
||||||
uint32_t last_ota_progress_{0};
|
uint32_t last_ota_progress_{0};
|
||||||
uint32_t ota_read_length_{0};
|
uint32_t ota_read_length_{0};
|
||||||
WebServerBase *parent_;
|
WebServerBase *parent_;
|
||||||
|
|
||||||
|
private:
|
||||||
|
#ifdef USE_ESP_IDF
|
||||||
|
void *ota_backend_{nullptr};
|
||||||
|
bool ota_success_{false};
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
#endif // USE_WEBSERVER_OTA
|
||||||
|
|
||||||
} // namespace web_server_base
|
} // namespace web_server_base
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_OTA, CONF_WEB_SERVER
|
||||||
|
from esphome.core import CORE
|
||||||
|
|
||||||
CODEOWNERS = ["@dentra"]
|
CODEOWNERS = ["@dentra"]
|
||||||
|
|
||||||
@ -12,3 +14,9 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
# Increase the maximum supported size of headers section in HTTP request packet to be processed by the server
|
# Increase the maximum supported size of headers section in HTTP request packet to be processed by the server
|
||||||
add_idf_sdkconfig_option("CONFIG_HTTPD_MAX_REQ_HDR_LEN", 1024)
|
add_idf_sdkconfig_option("CONFIG_HTTPD_MAX_REQ_HDR_LEN", 1024)
|
||||||
|
|
||||||
|
# Check if web_server component has OTA enabled
|
||||||
|
web_server_config = CORE.config.get(CONF_WEB_SERVER, {})
|
||||||
|
if web_server_config and web_server_config[CONF_OTA] and "ota" in CORE.config:
|
||||||
|
# Add multipart parser component for ESP-IDF OTA support
|
||||||
|
add_idf_component(name="zorxx/multipart-parser", ref="1.0.1")
|
||||||
|
254
esphome/components/web_server_idf/multipart.cpp
Normal file
254
esphome/components/web_server_idf/multipart.cpp
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)
|
||||||
|
#include "multipart.h"
|
||||||
|
#include "utils.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include <cstring>
|
||||||
|
#include "multipart_parser.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace web_server_idf {
|
||||||
|
|
||||||
|
static const char *const TAG = "multipart";
|
||||||
|
|
||||||
|
// ========== MultipartReader Implementation ==========
|
||||||
|
|
||||||
|
MultipartReader::MultipartReader(const std::string &boundary) {
|
||||||
|
// Initialize settings with callbacks
|
||||||
|
memset(&settings_, 0, sizeof(settings_));
|
||||||
|
settings_.on_header_field = on_header_field;
|
||||||
|
settings_.on_header_value = on_header_value;
|
||||||
|
settings_.on_part_data = on_part_data;
|
||||||
|
settings_.on_part_data_end = on_part_data_end;
|
||||||
|
|
||||||
|
ESP_LOGV(TAG, "Initializing multipart parser with boundary: '%s' (len: %zu)", boundary.c_str(), boundary.length());
|
||||||
|
|
||||||
|
// Create parser with boundary
|
||||||
|
parser_ = multipart_parser_init(boundary.c_str(), &settings_);
|
||||||
|
if (parser_) {
|
||||||
|
multipart_parser_set_data(parser_, this);
|
||||||
|
} else {
|
||||||
|
ESP_LOGE(TAG, "Failed to initialize multipart parser");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MultipartReader::~MultipartReader() {
|
||||||
|
if (parser_) {
|
||||||
|
multipart_parser_free(parser_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t MultipartReader::parse(const char *data, size_t len) {
|
||||||
|
if (!parser_) {
|
||||||
|
ESP_LOGE(TAG, "Parser not initialized");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t parsed = multipart_parser_execute(parser_, data, len);
|
||||||
|
|
||||||
|
if (parsed != len) {
|
||||||
|
ESP_LOGW(TAG, "Parser consumed %zu of %zu bytes - possible error", parsed, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MultipartReader::process_header_(const char *value, size_t length) {
|
||||||
|
// Process the completed header (field + value pair)
|
||||||
|
std::string value_str(value, length);
|
||||||
|
|
||||||
|
if (str_startswith_case_insensitive(current_header_field_, "content-disposition")) {
|
||||||
|
// Parse name and filename from Content-Disposition
|
||||||
|
current_part_.name = extract_header_param(value_str, "name");
|
||||||
|
current_part_.filename = extract_header_param(value_str, "filename");
|
||||||
|
} else if (str_startswith_case_insensitive(current_header_field_, "content-type")) {
|
||||||
|
current_part_.content_type = str_trim(value_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear field for next header
|
||||||
|
current_header_field_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
int MultipartReader::on_header_field(multipart_parser *parser, const char *at, size_t length) {
|
||||||
|
MultipartReader *reader = static_cast<MultipartReader *>(multipart_parser_get_data(parser));
|
||||||
|
reader->current_header_field_.assign(at, length);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int MultipartReader::on_header_value(multipart_parser *parser, const char *at, size_t length) {
|
||||||
|
MultipartReader *reader = static_cast<MultipartReader *>(multipart_parser_get_data(parser));
|
||||||
|
reader->process_header_(at, length);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int MultipartReader::on_part_data(multipart_parser *parser, const char *at, size_t length) {
|
||||||
|
MultipartReader *reader = static_cast<MultipartReader *>(multipart_parser_get_data(parser));
|
||||||
|
// Only process file uploads
|
||||||
|
if (reader->has_file() && reader->data_callback_) {
|
||||||
|
// IMPORTANT: The 'at' pointer points to data within the parser's input buffer.
|
||||||
|
// This data is only valid during this callback. The callback handler MUST
|
||||||
|
// process or copy the data immediately - it cannot store the pointer for
|
||||||
|
// later use as the buffer will be overwritten.
|
||||||
|
reader->data_callback_(reinterpret_cast<const uint8_t *>(at), length);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int MultipartReader::on_part_data_end(multipart_parser *parser) {
|
||||||
|
MultipartReader *reader = static_cast<MultipartReader *>(multipart_parser_get_data(parser));
|
||||||
|
ESP_LOGV(TAG, "Part data end");
|
||||||
|
if (reader->part_complete_callback_) {
|
||||||
|
reader->part_complete_callback_();
|
||||||
|
}
|
||||||
|
// Clear part info for next part
|
||||||
|
reader->current_part_ = Part{};
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Utility Functions ==========
|
||||||
|
|
||||||
|
// Case-insensitive string prefix check
|
||||||
|
bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix) {
|
||||||
|
if (str.length() < prefix.length()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return str_ncmp_ci(str.c_str(), prefix.c_str(), prefix.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract a parameter value from a header line
|
||||||
|
// Handles both quoted and unquoted values
|
||||||
|
std::string extract_header_param(const std::string &header, const std::string ¶m) {
|
||||||
|
size_t search_pos = 0;
|
||||||
|
|
||||||
|
while (search_pos < header.length()) {
|
||||||
|
// Look for param name
|
||||||
|
const char *found = stristr(header.c_str() + search_pos, param.c_str());
|
||||||
|
if (!found) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
size_t pos = found - header.c_str();
|
||||||
|
|
||||||
|
// Check if this is a word boundary (not part of another parameter)
|
||||||
|
if (pos > 0 && header[pos - 1] != ' ' && header[pos - 1] != ';' && header[pos - 1] != '\t') {
|
||||||
|
search_pos = pos + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move past param name
|
||||||
|
pos += param.length();
|
||||||
|
|
||||||
|
// Skip whitespace and find '='
|
||||||
|
while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos >= header.length() || header[pos] != '=') {
|
||||||
|
search_pos = pos;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pos++; // Skip '='
|
||||||
|
|
||||||
|
// Skip whitespace after '='
|
||||||
|
while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos >= header.length()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if value is quoted
|
||||||
|
if (header[pos] == '"') {
|
||||||
|
pos++;
|
||||||
|
size_t end = header.find('"', pos);
|
||||||
|
if (end != std::string::npos) {
|
||||||
|
return header.substr(pos, end - pos);
|
||||||
|
}
|
||||||
|
// Malformed - no closing quote
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unquoted value - find the end (semicolon, comma, or end of string)
|
||||||
|
size_t end = pos;
|
||||||
|
while (end < header.length() && header[end] != ';' && header[end] != ',' && header[end] != ' ' &&
|
||||||
|
header[end] != '\t') {
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return header.substr(pos, end - pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse boundary from Content-Type header
|
||||||
|
// Returns true if boundary found, false otherwise
|
||||||
|
// boundary_start and boundary_len will point to the boundary value
|
||||||
|
bool parse_multipart_boundary(const char *content_type, const char **boundary_start, size_t *boundary_len) {
|
||||||
|
if (!content_type) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for multipart/form-data (case-insensitive)
|
||||||
|
if (!stristr(content_type, "multipart/form-data")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for boundary parameter
|
||||||
|
const char *b = stristr(content_type, "boundary=");
|
||||||
|
if (!b) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *start = b + 9; // Skip "boundary="
|
||||||
|
|
||||||
|
// Skip whitespace
|
||||||
|
while (*start == ' ' || *start == '\t') {
|
||||||
|
start++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!*start) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find end of boundary
|
||||||
|
const char *end = start;
|
||||||
|
if (*end == '"') {
|
||||||
|
// Quoted boundary
|
||||||
|
start++;
|
||||||
|
end++;
|
||||||
|
while (*end && *end != '"') {
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
*boundary_len = end - start;
|
||||||
|
} else {
|
||||||
|
// Unquoted boundary
|
||||||
|
while (*end && *end != ' ' && *end != ';' && *end != '\r' && *end != '\n' && *end != '\t') {
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
*boundary_len = end - start;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*boundary_len == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*boundary_start = start;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim whitespace from both ends of a string
|
||||||
|
std::string str_trim(const std::string &str) {
|
||||||
|
size_t start = str.find_first_not_of(" \t\r\n");
|
||||||
|
if (start == std::string::npos) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
size_t end = str.find_last_not_of(" \t\r\n");
|
||||||
|
return str.substr(start, end - start + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace web_server_idf
|
||||||
|
} // namespace esphome
|
||||||
|
#endif // defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)
|
85
esphome/components/web_server_idf/multipart.h
Normal file
85
esphome/components/web_server_idf/multipart.h
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#if defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)
|
||||||
|
|
||||||
|
#include <esp_http_server.h>
|
||||||
|
#include <multipart_parser.h>
|
||||||
|
#include <string>
|
||||||
|
#include <functional>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace web_server_idf {
|
||||||
|
|
||||||
|
// Wrapper around zorxx/multipart-parser for ESP-IDF OTA uploads
|
||||||
|
class MultipartReader {
|
||||||
|
public:
|
||||||
|
struct Part {
|
||||||
|
std::string name;
|
||||||
|
std::string filename;
|
||||||
|
std::string content_type;
|
||||||
|
};
|
||||||
|
|
||||||
|
// IMPORTANT: The data pointer in DataCallback is only valid during the callback!
|
||||||
|
// The multipart parser passes pointers to its internal buffer which will be
|
||||||
|
// overwritten after the callback returns. Callbacks MUST process or copy the
|
||||||
|
// data immediately - storing the pointer for deferred processing will result
|
||||||
|
// in use-after-free bugs.
|
||||||
|
using DataCallback = std::function<void(const uint8_t *data, size_t len)>;
|
||||||
|
using PartCompleteCallback = std::function<void()>;
|
||||||
|
|
||||||
|
explicit MultipartReader(const std::string &boundary);
|
||||||
|
~MultipartReader();
|
||||||
|
|
||||||
|
// Set callbacks for handling data
|
||||||
|
void set_data_callback(DataCallback callback) { data_callback_ = callback; }
|
||||||
|
void set_part_complete_callback(PartCompleteCallback callback) { part_complete_callback_ = callback; }
|
||||||
|
|
||||||
|
// Parse incoming data
|
||||||
|
size_t parse(const char *data, size_t len);
|
||||||
|
|
||||||
|
// Get current part info
|
||||||
|
const Part &get_current_part() const { return current_part_; }
|
||||||
|
|
||||||
|
// Check if we found a file upload
|
||||||
|
bool has_file() const { return !current_part_.filename.empty(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static int on_header_field(multipart_parser *parser, const char *at, size_t length);
|
||||||
|
static int on_header_value(multipart_parser *parser, const char *at, size_t length);
|
||||||
|
static int on_part_data(multipart_parser *parser, const char *at, size_t length);
|
||||||
|
static int on_part_data_end(multipart_parser *parser);
|
||||||
|
|
||||||
|
multipart_parser *parser_{nullptr};
|
||||||
|
multipart_parser_settings settings_{};
|
||||||
|
|
||||||
|
Part current_part_;
|
||||||
|
std::string current_header_field_;
|
||||||
|
|
||||||
|
DataCallback data_callback_;
|
||||||
|
PartCompleteCallback part_complete_callback_;
|
||||||
|
|
||||||
|
void process_header_(const char *value, size_t length);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== Utility Functions ==========
|
||||||
|
|
||||||
|
// Case-insensitive string prefix check
|
||||||
|
bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix);
|
||||||
|
|
||||||
|
// Extract a parameter value from a header line
|
||||||
|
// Handles both quoted and unquoted values
|
||||||
|
std::string extract_header_param(const std::string &header, const std::string ¶m);
|
||||||
|
|
||||||
|
// Parse boundary from Content-Type header
|
||||||
|
// Returns true if boundary found, false otherwise
|
||||||
|
// boundary_start and boundary_len will point to the boundary value
|
||||||
|
bool parse_multipart_boundary(const char *content_type, const char **boundary_start, size_t *boundary_len);
|
||||||
|
|
||||||
|
// Trim whitespace from both ends of a string
|
||||||
|
std::string str_trim(const std::string &str);
|
||||||
|
|
||||||
|
} // namespace web_server_idf
|
||||||
|
} // namespace esphome
|
||||||
|
#endif // defined(USE_ESP_IDF) && defined(USE_WEBSERVER_OTA)
|
@ -1,5 +1,7 @@
|
|||||||
#ifdef USE_ESP_IDF
|
#ifdef USE_ESP_IDF
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cctype>
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "http_parser.h"
|
#include "http_parser.h"
|
||||||
@ -88,6 +90,36 @@ optional<std::string> query_key_value(const std::string &query_url, const std::s
|
|||||||
return {val.get()};
|
return {val.get()};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function for case-insensitive string region comparison
|
||||||
|
bool str_ncmp_ci(const char *s1, const char *s2, size_t n) {
|
||||||
|
for (size_t i = 0; i < n; i++) {
|
||||||
|
if (!char_equals_ci(s1[i], s2[i])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case-insensitive string search (like strstr but case-insensitive)
|
||||||
|
const char *stristr(const char *haystack, const char *needle) {
|
||||||
|
if (!haystack) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t needle_len = strlen(needle);
|
||||||
|
if (needle_len == 0) {
|
||||||
|
return haystack;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const char *p = haystack; *p; p++) {
|
||||||
|
if (str_ncmp_ci(p, needle, needle_len)) {
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace web_server_idf
|
} // namespace web_server_idf
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
#endif // USE_ESP_IDF
|
#endif // USE_ESP_IDF
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
#ifdef USE_ESP_IDF
|
#ifdef USE_ESP_IDF
|
||||||
|
|
||||||
#include <esp_http_server.h>
|
#include <esp_http_server.h>
|
||||||
|
#include <string>
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
@ -12,6 +13,15 @@ optional<std::string> request_get_header(httpd_req_t *req, const char *name);
|
|||||||
optional<std::string> request_get_url_query(httpd_req_t *req);
|
optional<std::string> request_get_url_query(httpd_req_t *req);
|
||||||
optional<std::string> query_key_value(const std::string &query_url, const std::string &key);
|
optional<std::string> query_key_value(const std::string &query_url, const std::string &key);
|
||||||
|
|
||||||
|
// Helper function for case-insensitive character comparison
|
||||||
|
inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); }
|
||||||
|
|
||||||
|
// Helper function for case-insensitive string region comparison
|
||||||
|
bool str_ncmp_ci(const char *s1, const char *s2, size_t n);
|
||||||
|
|
||||||
|
// Case-insensitive string search (like strstr but case-insensitive)
|
||||||
|
const char *stristr(const char *haystack, const char *needle);
|
||||||
|
|
||||||
} // namespace web_server_idf
|
} // namespace web_server_idf
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
#endif // USE_ESP_IDF
|
#endif // USE_ESP_IDF
|
||||||
|
@ -1,16 +1,25 @@
|
|||||||
#ifdef USE_ESP_IDF
|
#ifdef USE_ESP_IDF
|
||||||
|
|
||||||
#include <cstdarg>
|
#include <cstdarg>
|
||||||
|
#include <memory>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
#include "esp_tls_crypto.h"
|
#include "esp_tls_crypto.h"
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
#include "web_server_idf.h"
|
#include "web_server_idf.h"
|
||||||
|
|
||||||
|
#ifdef USE_WEBSERVER_OTA
|
||||||
|
#include <multipart_parser.h>
|
||||||
|
#include "multipart.h" // For parse_multipart_boundary and other utils
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef USE_WEBSERVER
|
#ifdef USE_WEBSERVER
|
||||||
#include "esphome/components/web_server/web_server.h"
|
#include "esphome/components/web_server/web_server.h"
|
||||||
#include "esphome/components/web_server/list_entities.h"
|
#include "esphome/components/web_server/list_entities.h"
|
||||||
@ -72,18 +81,32 @@ void AsyncWebServer::begin() {
|
|||||||
esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) {
|
esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) {
|
||||||
ESP_LOGVV(TAG, "Enter AsyncWebServer::request_post_handler. uri=%s", r->uri);
|
ESP_LOGVV(TAG, "Enter AsyncWebServer::request_post_handler. uri=%s", r->uri);
|
||||||
auto content_type = request_get_header(r, "Content-Type");
|
auto content_type = request_get_header(r, "Content-Type");
|
||||||
if (content_type.has_value() && *content_type != "application/x-www-form-urlencoded") {
|
|
||||||
ESP_LOGW(TAG, "Only application/x-www-form-urlencoded supported for POST request");
|
|
||||||
// fallback to get handler to support backward compatibility
|
|
||||||
return AsyncWebServer::request_handler(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!request_has_header(r, "Content-Length")) {
|
if (!request_has_header(r, "Content-Length")) {
|
||||||
ESP_LOGW(TAG, "Content length is requred for post: %s", r->uri);
|
ESP_LOGW(TAG, "Content length is required for post: %s", r->uri);
|
||||||
httpd_resp_send_err(r, HTTPD_411_LENGTH_REQUIRED, nullptr);
|
httpd_resp_send_err(r, HTTPD_411_LENGTH_REQUIRED, nullptr);
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (content_type.has_value()) {
|
||||||
|
const char *content_type_char = content_type.value().c_str();
|
||||||
|
|
||||||
|
// Check most common case first
|
||||||
|
if (stristr(content_type_char, "application/x-www-form-urlencoded") != nullptr) {
|
||||||
|
// Normal form data - proceed with regular handling
|
||||||
|
#ifdef USE_WEBSERVER_OTA
|
||||||
|
} else if (stristr(content_type_char, "multipart/form-data") != nullptr) {
|
||||||
|
auto *server = static_cast<AsyncWebServer *>(r->user_ctx);
|
||||||
|
return server->handle_multipart_upload_(r, content_type_char);
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Unsupported content type for POST: %s", content_type_char);
|
||||||
|
// fallback to get handler to support backward compatibility
|
||||||
|
return AsyncWebServer::request_handler(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle regular form data
|
||||||
if (r->content_len > HTTPD_MAX_REQ_HDR_LEN) {
|
if (r->content_len > HTTPD_MAX_REQ_HDR_LEN) {
|
||||||
ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len);
|
ESP_LOGW(TAG, "Request size is to big: %zu", r->content_len);
|
||||||
httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
|
httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
|
||||||
@ -539,6 +562,97 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_WEBSERVER_OTA
|
||||||
|
esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *content_type) {
|
||||||
|
static constexpr size_t MULTIPART_CHUNK_SIZE = 1460; // Match Arduino AsyncWebServer buffer size
|
||||||
|
static constexpr size_t YIELD_INTERVAL_BYTES = 16 * 1024; // Yield every 16KB to prevent watchdog
|
||||||
|
|
||||||
|
// Parse boundary and create reader
|
||||||
|
const char *boundary_start;
|
||||||
|
size_t boundary_len;
|
||||||
|
if (!parse_multipart_boundary(content_type, &boundary_start, &boundary_len)) {
|
||||||
|
ESP_LOGE(TAG, "Failed to parse multipart boundary");
|
||||||
|
httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncWebServerRequest req(r);
|
||||||
|
AsyncWebHandler *handler = nullptr;
|
||||||
|
for (auto *h : this->handlers_) {
|
||||||
|
if (h->canHandle(&req)) {
|
||||||
|
handler = h;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!handler) {
|
||||||
|
ESP_LOGW(TAG, "No handler found for OTA request");
|
||||||
|
httpd_resp_send_err(r, HTTPD_404_NOT_FOUND, nullptr);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload state
|
||||||
|
std::string filename;
|
||||||
|
size_t index = 0;
|
||||||
|
// Create reader on heap to reduce stack usage
|
||||||
|
auto reader = std::make_unique<MultipartReader>("--" + std::string(boundary_start, boundary_len));
|
||||||
|
|
||||||
|
// Configure callbacks
|
||||||
|
reader->set_data_callback([&](const uint8_t *data, size_t len) {
|
||||||
|
if (!reader->has_file() || !len)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (filename.empty()) {
|
||||||
|
filename = reader->get_current_part().filename;
|
||||||
|
ESP_LOGV(TAG, "Processing file: '%s'", filename.c_str());
|
||||||
|
handler->handleUpload(&req, filename, 0, nullptr, 0, false); // Start
|
||||||
|
}
|
||||||
|
|
||||||
|
handler->handleUpload(&req, filename, index, const_cast<uint8_t *>(data), len, false);
|
||||||
|
index += len;
|
||||||
|
});
|
||||||
|
|
||||||
|
reader->set_part_complete_callback([&]() {
|
||||||
|
if (index > 0) {
|
||||||
|
handler->handleUpload(&req, filename, index, nullptr, 0, true); // End
|
||||||
|
filename.clear();
|
||||||
|
index = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process data
|
||||||
|
std::unique_ptr<char[]> buffer(new char[MULTIPART_CHUNK_SIZE]);
|
||||||
|
size_t bytes_since_yield = 0;
|
||||||
|
|
||||||
|
for (size_t remaining = r->content_len; remaining > 0;) {
|
||||||
|
int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, MULTIPART_CHUNK_SIZE));
|
||||||
|
|
||||||
|
if (recv_len <= 0) {
|
||||||
|
httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
|
||||||
|
nullptr);
|
||||||
|
return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader->parse(buffer.get(), recv_len) != static_cast<size_t>(recv_len)) {
|
||||||
|
ESP_LOGW(TAG, "Multipart parser error");
|
||||||
|
httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
remaining -= recv_len;
|
||||||
|
bytes_since_yield += recv_len;
|
||||||
|
|
||||||
|
if (bytes_since_yield > YIELD_INTERVAL_BYTES) {
|
||||||
|
vTaskDelay(1);
|
||||||
|
bytes_since_yield = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handler->handleRequest(&req);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
#endif // USE_WEBSERVER_OTA
|
||||||
|
|
||||||
} // namespace web_server_idf
|
} // namespace web_server_idf
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
|
@ -204,6 +204,9 @@ class AsyncWebServer {
|
|||||||
static esp_err_t request_handler(httpd_req_t *r);
|
static esp_err_t request_handler(httpd_req_t *r);
|
||||||
static esp_err_t request_post_handler(httpd_req_t *r);
|
static esp_err_t request_post_handler(httpd_req_t *r);
|
||||||
esp_err_t request_handler_(AsyncWebServerRequest *request) const;
|
esp_err_t request_handler_(AsyncWebServerRequest *request) const;
|
||||||
|
#ifdef USE_WEBSERVER_OTA
|
||||||
|
esp_err_t handle_multipart_upload_(httpd_req_t *r, const char *content_type);
|
||||||
|
#endif
|
||||||
std::vector<AsyncWebHandler *> handlers_;
|
std::vector<AsyncWebHandler *> handlers_;
|
||||||
std::function<void(AsyncWebServerRequest *request)> on_not_found_{};
|
std::function<void(AsyncWebServerRequest *request)> on_not_found_{};
|
||||||
};
|
};
|
||||||
|
@ -309,6 +309,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
rp2040="light",
|
rp2040="light",
|
||||||
bk72xx="none",
|
bk72xx="none",
|
||||||
rtl87xx="none",
|
rtl87xx="none",
|
||||||
|
ln882x="light",
|
||||||
): cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
|
): cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
|
||||||
cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean,
|
cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
||||||
|
@ -12,6 +12,7 @@ PLATFORM_ESP32 = "esp32"
|
|||||||
PLATFORM_ESP8266 = "esp8266"
|
PLATFORM_ESP8266 = "esp8266"
|
||||||
PLATFORM_HOST = "host"
|
PLATFORM_HOST = "host"
|
||||||
PLATFORM_LIBRETINY_OLDSTYLE = "libretiny"
|
PLATFORM_LIBRETINY_OLDSTYLE = "libretiny"
|
||||||
|
PLATFORM_LN882X = "ln882x"
|
||||||
PLATFORM_RP2040 = "rp2040"
|
PLATFORM_RP2040 = "rp2040"
|
||||||
PLATFORM_RTL87XX = "rtl87xx"
|
PLATFORM_RTL87XX = "rtl87xx"
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ from esphome.const import (
|
|||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
PLATFORM_HOST,
|
PLATFORM_HOST,
|
||||||
|
PLATFORM_LN882X,
|
||||||
PLATFORM_RP2040,
|
PLATFORM_RP2040,
|
||||||
PLATFORM_RTL87XX,
|
PLATFORM_RTL87XX,
|
||||||
)
|
)
|
||||||
@ -661,9 +662,13 @@ class EsphomeCore:
|
|||||||
def is_rtl87xx(self):
|
def is_rtl87xx(self):
|
||||||
return self.target_platform == PLATFORM_RTL87XX
|
return self.target_platform == PLATFORM_RTL87XX
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_ln882x(self):
|
||||||
|
return self.target_platform == PLATFORM_LN882X
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_libretiny(self):
|
def is_libretiny(self):
|
||||||
return self.is_bk72xx or self.is_rtl87xx
|
return self.is_bk72xx or self.is_rtl87xx or self.is_ln882x
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_host(self):
|
def is_host(self):
|
||||||
|
@ -153,6 +153,7 @@
|
|||||||
#define USE_SPI
|
#define USE_SPI
|
||||||
#define USE_VOICE_ASSISTANT
|
#define USE_VOICE_ASSISTANT
|
||||||
#define USE_WEBSERVER
|
#define USE_WEBSERVER
|
||||||
|
#define USE_WEBSERVER_OTA
|
||||||
#define USE_WEBSERVER_PORT 80 // NOLINT
|
#define USE_WEBSERVER_PORT 80 // NOLINT
|
||||||
#define USE_WEBSERVER_SORTING
|
#define USE_WEBSERVER_SORTING
|
||||||
#define USE_WIFI_11KV_SUPPORT
|
#define USE_WIFI_11KV_SUPPORT
|
||||||
|
@ -639,7 +639,11 @@ class DownloadListRequestHandler(BaseHandler):
|
|||||||
|
|
||||||
if platform.upper() in ESP32_VARIANTS:
|
if platform.upper() in ESP32_VARIANTS:
|
||||||
platform = "esp32"
|
platform = "esp32"
|
||||||
elif platform in (const.PLATFORM_RTL87XX, const.PLATFORM_BK72XX):
|
elif platform in (
|
||||||
|
const.PLATFORM_RTL87XX,
|
||||||
|
const.PLATFORM_BK72XX,
|
||||||
|
const.PLATFORM_LN882X,
|
||||||
|
):
|
||||||
platform = "libretiny"
|
platform = "libretiny"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -837,6 +841,10 @@ class BoardsRequestHandler(BaseHandler):
|
|||||||
from esphome.components.bk72xx.boards import BOARDS as BK72XX_BOARDS
|
from esphome.components.bk72xx.boards import BOARDS as BK72XX_BOARDS
|
||||||
|
|
||||||
boards = BK72XX_BOARDS
|
boards = BK72XX_BOARDS
|
||||||
|
elif platform == const.PLATFORM_LN882X:
|
||||||
|
from esphome.components.ln882x.boards import BOARDS as LN882X_BOARDS
|
||||||
|
|
||||||
|
boards = LN882X_BOARDS
|
||||||
elif platform == const.PLATFORM_RTL87XX:
|
elif platform == const.PLATFORM_RTL87XX:
|
||||||
from esphome.components.rtl87xx.boards import BOARDS as RTL87XX_BOARDS
|
from esphome.components.rtl87xx.boards import BOARDS as RTL87XX_BOARDS
|
||||||
|
|
||||||
|
@ -17,3 +17,5 @@ dependencies:
|
|||||||
version: 2.0.11
|
version: 2.0.11
|
||||||
rules:
|
rules:
|
||||||
- if: "target in [esp32h2, esp32p4]"
|
- if: "target in [esp32h2, esp32p4]"
|
||||||
|
zorxx/multipart-parser:
|
||||||
|
version: 1.0.1
|
||||||
|
@ -83,6 +83,11 @@ bk72xx:
|
|||||||
board: {board}
|
board: {board}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
LN882X_CONFIG = """
|
||||||
|
ln882x:
|
||||||
|
board: {board}
|
||||||
|
"""
|
||||||
|
|
||||||
RTL87XX_CONFIG = """
|
RTL87XX_CONFIG = """
|
||||||
rtl87xx:
|
rtl87xx:
|
||||||
board: {board}
|
board: {board}
|
||||||
@ -93,6 +98,7 @@ HARDWARE_BASE_CONFIGS = {
|
|||||||
"ESP32": ESP32_CONFIG,
|
"ESP32": ESP32_CONFIG,
|
||||||
"RP2040": RP2040_CONFIG,
|
"RP2040": RP2040_CONFIG,
|
||||||
"BK72XX": BK72XX_CONFIG,
|
"BK72XX": BK72XX_CONFIG,
|
||||||
|
"LN882X": LN882X_CONFIG,
|
||||||
"RTL87XX": RTL87XX_CONFIG,
|
"RTL87XX": RTL87XX_CONFIG,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,7 +163,7 @@ def wizard_file(**kwargs):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# pylint: disable=consider-using-f-string
|
# pylint: disable=consider-using-f-string
|
||||||
if kwargs["platform"] in ["ESP8266", "ESP32", "BK72XX", "RTL87XX"]:
|
if kwargs["platform"] in ["ESP8266", "ESP32", "BK72XX", "LN882X", "RTL87XX"]:
|
||||||
config += """
|
config += """
|
||||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
ap:
|
ap:
|
||||||
@ -181,6 +187,7 @@ def wizard_write(path, **kwargs):
|
|||||||
from esphome.components.bk72xx import boards as bk72xx_boards
|
from esphome.components.bk72xx import boards as bk72xx_boards
|
||||||
from esphome.components.esp32 import boards as esp32_boards
|
from esphome.components.esp32 import boards as esp32_boards
|
||||||
from esphome.components.esp8266 import boards as esp8266_boards
|
from esphome.components.esp8266 import boards as esp8266_boards
|
||||||
|
from esphome.components.ln882x import boards as ln882x_boards
|
||||||
from esphome.components.rp2040 import boards as rp2040_boards
|
from esphome.components.rp2040 import boards as rp2040_boards
|
||||||
from esphome.components.rtl87xx import boards as rtl87xx_boards
|
from esphome.components.rtl87xx import boards as rtl87xx_boards
|
||||||
|
|
||||||
@ -200,6 +207,8 @@ def wizard_write(path, **kwargs):
|
|||||||
platform = "RP2040"
|
platform = "RP2040"
|
||||||
elif board in bk72xx_boards.BOARDS:
|
elif board in bk72xx_boards.BOARDS:
|
||||||
platform = "BK72XX"
|
platform = "BK72XX"
|
||||||
|
elif board in ln882x_boards.BOARDS:
|
||||||
|
platform = "LN882X"
|
||||||
elif board in rtl87xx_boards.BOARDS:
|
elif board in rtl87xx_boards.BOARDS:
|
||||||
platform = "RTL87XX"
|
platform = "RTL87XX"
|
||||||
else:
|
else:
|
||||||
@ -253,6 +262,7 @@ def wizard(path):
|
|||||||
from esphome.components.bk72xx import boards as bk72xx_boards
|
from esphome.components.bk72xx import boards as bk72xx_boards
|
||||||
from esphome.components.esp32 import boards as esp32_boards
|
from esphome.components.esp32 import boards as esp32_boards
|
||||||
from esphome.components.esp8266 import boards as esp8266_boards
|
from esphome.components.esp8266 import boards as esp8266_boards
|
||||||
|
from esphome.components.ln882x import boards as ln882x_boards
|
||||||
from esphome.components.rp2040 import boards as rp2040_boards
|
from esphome.components.rp2040 import boards as rp2040_boards
|
||||||
from esphome.components.rtl87xx import boards as rtl87xx_boards
|
from esphome.components.rtl87xx import boards as rtl87xx_boards
|
||||||
|
|
||||||
@ -325,7 +335,7 @@ def wizard(path):
|
|||||||
"firmwares for it."
|
"firmwares for it."
|
||||||
)
|
)
|
||||||
|
|
||||||
wizard_platforms = ["ESP32", "ESP8266", "BK72XX", "RTL87XX", "RP2040"]
|
wizard_platforms = ["ESP32", "ESP8266", "BK72XX", "LN882X", "RTL87XX", "RP2040"]
|
||||||
safe_print(
|
safe_print(
|
||||||
"Please choose one of the supported microcontrollers "
|
"Please choose one of the supported microcontrollers "
|
||||||
"(Use ESP8266 for Sonoff devices)."
|
"(Use ESP8266 for Sonoff devices)."
|
||||||
@ -361,7 +371,7 @@ def wizard(path):
|
|||||||
board_link = (
|
board_link = (
|
||||||
"https://www.raspberrypi.com/documentation/microcontrollers/rp2040.html"
|
"https://www.raspberrypi.com/documentation/microcontrollers/rp2040.html"
|
||||||
)
|
)
|
||||||
elif platform in ["BK72XX", "RTL87XX"]:
|
elif platform in ["BK72XX", "LN882X", "RTL87XX"]:
|
||||||
board_link = "https://docs.libretiny.eu/docs/status/supported/"
|
board_link = "https://docs.libretiny.eu/docs/status/supported/"
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("Unknown platform!")
|
raise NotImplementedError("Unknown platform!")
|
||||||
@ -384,6 +394,9 @@ def wizard(path):
|
|||||||
elif platform == "BK72XX":
|
elif platform == "BK72XX":
|
||||||
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "cb2s")}".')
|
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "cb2s")}".')
|
||||||
boards_list = bk72xx_boards.BOARDS.items()
|
boards_list = bk72xx_boards.BOARDS.items()
|
||||||
|
elif platform == "LN882X":
|
||||||
|
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "wl2s")}".')
|
||||||
|
boards_list = ln882x_boards.BOARDS.items()
|
||||||
elif platform == "RTL87XX":
|
elif platform == "RTL87XX":
|
||||||
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "wr3")}".')
|
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "wr3")}".')
|
||||||
boards_list = rtl87xx_boards.BOARDS.items()
|
boards_list = rtl87xx_boards.BOARDS.items()
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
; It's *not* used during runtime.
|
; It's *not* used during runtime.
|
||||||
|
|
||||||
[platformio]
|
[platformio]
|
||||||
default_envs = esp8266-arduino, esp32-arduino, esp32-idf, bk72xx-arduino
|
default_envs = esp8266-arduino, esp32-arduino, esp32-idf, bk72xx-arduino, ln882h-arduino
|
||||||
; Ideally, we want src_dir to be the root directory of the repository, to mimic the runtime build
|
; Ideally, we want src_dir to be the root directory of the repository, to mimic the runtime build
|
||||||
; environment as best as possible. Unfortunately, the ESP-IDF toolchain really doesn't like this
|
; environment as best as possible. Unfortunately, the ESP-IDF toolchain really doesn't like this
|
||||||
; being the root directory. Instead, set esphome/ as the source directory, all our sources are in
|
; being the root directory. Instead, set esphome/ as the source directory, all our sources are in
|
||||||
@ -530,6 +530,17 @@ build_flags =
|
|||||||
build_unflags =
|
build_unflags =
|
||||||
${common.build_unflags}
|
${common.build_unflags}
|
||||||
|
|
||||||
|
[env:ln882h-arduino]
|
||||||
|
extends = common:libretiny-arduino
|
||||||
|
board = generic-ln882hki
|
||||||
|
build_flags =
|
||||||
|
${common:libretiny-arduino.build_flags}
|
||||||
|
${flags:runtime.build_flags}
|
||||||
|
-DUSE_LN882X
|
||||||
|
-DUSE_LIBRETINY_VARIANT_LN882H
|
||||||
|
build_unflags =
|
||||||
|
${common.build_unflags}
|
||||||
|
|
||||||
[env:rtl87xxb-arduino]
|
[env:rtl87xxb-arduino]
|
||||||
extends = common:libretiny-arduino
|
extends = common:libretiny-arduino
|
||||||
board = generic-rtl8710bn-2mb-788k
|
board = generic-rtl8710bn-2mb-788k
|
||||||
|
4
tests/components/adc/test.ln882x-ard.yaml
Normal file
4
tests/components/adc/test.ln882x-ard.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
sensor:
|
||||||
|
- platform: adc
|
||||||
|
pin: PA0
|
||||||
|
name: Basic ADC Test
|
2
tests/components/binary_sensor/test.ln882x-ard.yaml
Normal file
2
tests/components/binary_sensor/test.ln882x-ard.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
packages:
|
||||||
|
common: !include common.yaml
|
1
tests/components/debug/test.ln882x-ard.yaml
Normal file
1
tests/components/debug/test.ln882x-ard.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
2
tests/components/homeassistant/test.ln882x-ard.yaml
Normal file
2
tests/components/homeassistant/test.ln882x-ard.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
packages:
|
||||||
|
common: !include common.yaml
|
1
tests/components/script/test.ln882x-ard.yaml
Normal file
1
tests/components/script/test.ln882x-ard.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
1
tests/components/sntp/test.ln882x-ard.yaml
Normal file
1
tests/components/sntp/test.ln882x-ard.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
2
tests/components/switch/test.ln882x-ard.yaml
Normal file
2
tests/components/switch/test.ln882x-ard.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
packages:
|
||||||
|
common: !include common.yaml
|
1
tests/components/syslog/test.ln882x-ard.yaml
Normal file
1
tests/components/syslog/test.ln882x-ard.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
<<: !include common.yaml
|
2
tests/components/template/test.ln882x-ard.yaml
Normal file
2
tests/components/template/test.ln882x-ard.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
packages:
|
||||||
|
common: !include common.yaml
|
9
tests/components/web_server/test_no_ota.esp32-idf.yaml
Normal file
9
tests/components/web_server/test_no_ota.esp32-idf.yaml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
packages:
|
||||||
|
device_base: !include common.yaml
|
||||||
|
|
||||||
|
# No OTA component defined for this test
|
||||||
|
|
||||||
|
web_server:
|
||||||
|
port: 8080
|
||||||
|
version: 2
|
||||||
|
ota: false
|
32
tests/components/web_server/test_ota.esp32-idf.yaml
Normal file
32
tests/components/web_server/test_ota.esp32-idf.yaml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Test configuration for ESP-IDF web server with OTA enabled
|
||||||
|
esphome:
|
||||||
|
name: test-web-server-ota-idf
|
||||||
|
|
||||||
|
# Force ESP-IDF framework
|
||||||
|
esp32:
|
||||||
|
board: esp32dev
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
|
||||||
|
packages:
|
||||||
|
device_base: !include common.yaml
|
||||||
|
|
||||||
|
# Enable OTA for multipart upload testing
|
||||||
|
ota:
|
||||||
|
- platform: esphome
|
||||||
|
password: "test_ota_password"
|
||||||
|
|
||||||
|
# Web server with OTA enabled
|
||||||
|
web_server:
|
||||||
|
port: 8080
|
||||||
|
version: 2
|
||||||
|
ota: true
|
||||||
|
include_internal: true
|
||||||
|
|
||||||
|
# Enable debug logging for OTA
|
||||||
|
logger:
|
||||||
|
level: DEBUG
|
||||||
|
logs:
|
||||||
|
web_server: VERBOSE
|
||||||
|
web_server_idf: VERBOSE
|
||||||
|
|
11
tests/components/web_server/test_ota_disabled.esp32-idf.yaml
Normal file
11
tests/components/web_server/test_ota_disabled.esp32-idf.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
packages:
|
||||||
|
device_base: !include common.yaml
|
||||||
|
|
||||||
|
# OTA is configured but web_server OTA is disabled
|
||||||
|
ota:
|
||||||
|
- platform: esphome
|
||||||
|
|
||||||
|
web_server:
|
||||||
|
port: 8080
|
||||||
|
version: 2
|
||||||
|
ota: false
|
@ -0,0 +1,15 @@
|
|||||||
|
esphome:
|
||||||
|
name: componenttestespln882x
|
||||||
|
friendly_name: $component_name
|
||||||
|
|
||||||
|
ln882x:
|
||||||
|
board: generic-ln882hki
|
||||||
|
|
||||||
|
logger:
|
||||||
|
level: VERY_VERBOSE
|
||||||
|
|
||||||
|
packages:
|
||||||
|
component_under_test: !include
|
||||||
|
file: $component_test_file
|
||||||
|
vars:
|
||||||
|
component_test_file: $component_test_file
|
@ -20,6 +20,7 @@ from esphome.const import (
|
|||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
PLATFORM_HOST,
|
PLATFORM_HOST,
|
||||||
|
PLATFORM_LN882X,
|
||||||
PLATFORM_RP2040,
|
PLATFORM_RP2040,
|
||||||
PLATFORM_RTL87XX,
|
PLATFORM_RTL87XX,
|
||||||
)
|
)
|
||||||
@ -214,7 +215,8 @@ def hex_int__valid(value):
|
|||||||
("arduino", PLATFORM_RP2040, None, "20", "20", "20", "20"),
|
("arduino", PLATFORM_RP2040, None, "20", "20", "20", "20"),
|
||||||
("arduino", PLATFORM_BK72XX, None, "21", "21", "21", "21"),
|
("arduino", PLATFORM_BK72XX, None, "21", "21", "21", "21"),
|
||||||
("arduino", PLATFORM_RTL87XX, None, "22", "22", "22", "22"),
|
("arduino", PLATFORM_RTL87XX, None, "22", "22", "22", "22"),
|
||||||
("host", PLATFORM_HOST, None, "23", "23", "23", "23"),
|
("arduino", PLATFORM_LN882X, None, "23", "23", "23", "23"),
|
||||||
|
("host", PLATFORM_HOST, None, "24", "24", "24", "24"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_split_default(framework, platform, variant, full, idf, arduino, simple):
|
def test_split_default(framework, platform, variant, full, idf, arduino, simple):
|
||||||
@ -244,7 +246,8 @@ def test_split_default(framework, platform, variant, full, idf, arduino, simple)
|
|||||||
"rp2040": "20",
|
"rp2040": "20",
|
||||||
"bk72xx": "21",
|
"bk72xx": "21",
|
||||||
"rtl87xx": "22",
|
"rtl87xx": "22",
|
||||||
"host": "23",
|
"ln882x": "23",
|
||||||
|
"host": "24",
|
||||||
}
|
}
|
||||||
|
|
||||||
idf_mappings = {
|
idf_mappings = {
|
||||||
|
@ -8,6 +8,7 @@ import pytest
|
|||||||
from esphome.components.bk72xx.boards import BK72XX_BOARD_PINS
|
from esphome.components.bk72xx.boards import BK72XX_BOARD_PINS
|
||||||
from esphome.components.esp32.boards import ESP32_BOARD_PINS
|
from esphome.components.esp32.boards import ESP32_BOARD_PINS
|
||||||
from esphome.components.esp8266.boards import ESP8266_BOARD_PINS
|
from esphome.components.esp8266.boards import ESP8266_BOARD_PINS
|
||||||
|
from esphome.components.ln882x.boards import LN882X_BOARD_PINS
|
||||||
from esphome.components.rtl87xx.boards import RTL87XX_BOARD_PINS
|
from esphome.components.rtl87xx.boards import RTL87XX_BOARD_PINS
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
import esphome.wizard as wz
|
import esphome.wizard as wz
|
||||||
@ -187,6 +188,27 @@ def test_wizard_write_defaults_platform_from_board_bk72xx(
|
|||||||
assert "bk72xx:" in generated_config
|
assert "bk72xx:" in generated_config
|
||||||
|
|
||||||
|
|
||||||
|
def test_wizard_write_defaults_platform_from_board_ln882x(
|
||||||
|
default_config, tmp_path, monkeypatch
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
If the platform is not explicitly set, use "LN882X" if the board is one of LN882X boards
|
||||||
|
"""
|
||||||
|
# Given
|
||||||
|
del default_config["platform"]
|
||||||
|
default_config["board"] = [*LN882X_BOARD_PINS][0]
|
||||||
|
|
||||||
|
monkeypatch.setattr(wz, "write_file", MagicMock())
|
||||||
|
monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path))
|
||||||
|
|
||||||
|
# When
|
||||||
|
wz.wizard_write(tmp_path, **default_config)
|
||||||
|
|
||||||
|
# Then
|
||||||
|
generated_config = wz.write_file.call_args.args[1]
|
||||||
|
assert "ln882x:" in generated_config
|
||||||
|
|
||||||
|
|
||||||
def test_wizard_write_defaults_platform_from_board_rtl87xx(
|
def test_wizard_write_defaults_platform_from_board_rtl87xx(
|
||||||
default_config, tmp_path, monkeypatch
|
default_config, tmp_path, monkeypatch
|
||||||
):
|
):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user