mirror of
https://github.com/esphome/esphome.git
synced 2025-11-14 05:45:48 +00:00
Merge branch 'wifi_min_ver' into integration
This commit is contained in:
@@ -396,6 +396,7 @@ esphome/components/rpi_dpi_rgb/* @clydebarrow
|
||||
esphome/components/rtl87xx/* @kuba2k2
|
||||
esphome/components/rtttl/* @glmnet
|
||||
esphome/components/runtime_stats/* @bdraco
|
||||
esphome/components/rx8130/* @beormund
|
||||
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
||||
esphome/components/scd4x/* @martgras @sjtrny
|
||||
esphome/components/script/* @esphome/core
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import importlib
|
||||
import logging
|
||||
import pkgutil
|
||||
|
||||
from esphome.automation import build_automation, register_action, validate_automation
|
||||
from esphome.automation import build_automation, validate_automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING
|
||||
from esphome.components.display import Display
|
||||
@@ -25,8 +27,8 @@ from esphome.cpp_generator import MockObj
|
||||
from esphome.final_validate import full_config
|
||||
from esphome.helpers import write_file_if_changed
|
||||
|
||||
from . import defines as df, helpers, lv_validation as lvalid
|
||||
from .automation import disp_update, focused_widgets, refreshed_widgets, update_to_code
|
||||
from . import defines as df, helpers, lv_validation as lvalid, widgets
|
||||
from .automation import disp_update, focused_widgets, refreshed_widgets
|
||||
from .defines import add_define
|
||||
from .encoders import (
|
||||
ENCODERS_CONFIG,
|
||||
@@ -45,7 +47,6 @@ from .schemas import (
|
||||
WIDGET_TYPES,
|
||||
any_widget_schema,
|
||||
container_schema,
|
||||
create_modify_schema,
|
||||
obj_schema,
|
||||
)
|
||||
from .styles import add_top_layer, styles_to_code, theme_to_code
|
||||
@@ -54,7 +55,6 @@ from .trigger import add_on_boot_triggers, generate_triggers
|
||||
from .types import (
|
||||
FontEngine,
|
||||
IdleTrigger,
|
||||
ObjUpdateAction,
|
||||
PlainTrigger,
|
||||
lv_font_t,
|
||||
lv_group_t,
|
||||
@@ -69,33 +69,23 @@ from .widgets import (
|
||||
set_obj_properties,
|
||||
styles_used,
|
||||
)
|
||||
from .widgets.animimg import animimg_spec
|
||||
from .widgets.arc import arc_spec
|
||||
from .widgets.button import button_spec
|
||||
from .widgets.buttonmatrix import buttonmatrix_spec
|
||||
from .widgets.canvas import canvas_spec
|
||||
from .widgets.checkbox import checkbox_spec
|
||||
from .widgets.container import container_spec
|
||||
from .widgets.dropdown import dropdown_spec
|
||||
from .widgets.img import img_spec
|
||||
from .widgets.keyboard import keyboard_spec
|
||||
from .widgets.label import label_spec
|
||||
from .widgets.led import led_spec
|
||||
from .widgets.line import line_spec
|
||||
from .widgets.lv_bar import bar_spec
|
||||
from .widgets.meter import meter_spec
|
||||
|
||||
# Import only what we actually use directly in this file
|
||||
from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code
|
||||
from .widgets.obj import obj_spec
|
||||
from .widgets.page import add_pages, generate_page_triggers, page_spec
|
||||
from .widgets.qrcode import qr_code_spec
|
||||
from .widgets.roller import roller_spec
|
||||
from .widgets.slider import slider_spec
|
||||
from .widgets.spinbox import spinbox_spec
|
||||
from .widgets.spinner import spinner_spec
|
||||
from .widgets.switch import switch_spec
|
||||
from .widgets.tabview import tabview_spec
|
||||
from .widgets.textarea import textarea_spec
|
||||
from .widgets.tileview import tileview_spec
|
||||
from .widgets.obj import obj_spec # Used in LVGL_SCHEMA
|
||||
from .widgets.page import ( # page_spec used in LVGL_SCHEMA
|
||||
add_pages,
|
||||
generate_page_triggers,
|
||||
page_spec,
|
||||
)
|
||||
|
||||
# Widget registration happens via WidgetType.__init__ in individual widget files
|
||||
# The imports below trigger creation of the widget types
|
||||
# Action registration (lvgl.{widget}.update) happens automatically
|
||||
# in the WidgetType.__init__ method
|
||||
|
||||
for module_info in pkgutil.iter_modules(widgets.__path__):
|
||||
importlib.import_module(f".widgets.{module_info.name}", package=__package__)
|
||||
|
||||
DOMAIN = "lvgl"
|
||||
DEPENDENCIES = ["display"]
|
||||
@@ -103,41 +93,6 @@ AUTO_LOAD = ["key_provider"]
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
for w_type in (
|
||||
label_spec,
|
||||
obj_spec,
|
||||
button_spec,
|
||||
bar_spec,
|
||||
slider_spec,
|
||||
arc_spec,
|
||||
line_spec,
|
||||
spinner_spec,
|
||||
led_spec,
|
||||
animimg_spec,
|
||||
checkbox_spec,
|
||||
img_spec,
|
||||
switch_spec,
|
||||
tabview_spec,
|
||||
buttonmatrix_spec,
|
||||
meter_spec,
|
||||
dropdown_spec,
|
||||
roller_spec,
|
||||
textarea_spec,
|
||||
spinbox_spec,
|
||||
keyboard_spec,
|
||||
tileview_spec,
|
||||
qr_code_spec,
|
||||
canvas_spec,
|
||||
container_spec,
|
||||
):
|
||||
WIDGET_TYPES[w_type.name] = w_type
|
||||
|
||||
for w_type in WIDGET_TYPES.values():
|
||||
register_action(
|
||||
f"lvgl.{w_type.name}.update",
|
||||
ObjUpdateAction,
|
||||
create_modify_schema(w_type),
|
||||
)(update_to_code)
|
||||
|
||||
SIMPLE_TRIGGERS = (
|
||||
df.CONF_ON_PAUSE,
|
||||
@@ -402,6 +357,15 @@ def add_hello_world(config):
|
||||
return config
|
||||
|
||||
|
||||
def _theme_schema(value):
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA)
|
||||
for name, w in WIDGET_TYPES.items()
|
||||
}
|
||||
)(value)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validation
|
||||
|
||||
LVGL_SCHEMA = cv.All(
|
||||
@@ -454,12 +418,7 @@ LVGL_SCHEMA = cv.All(
|
||||
cv.Optional(
|
||||
df.CONF_TRANSPARENCY_KEY, default=0x000400
|
||||
): lvalid.lv_color,
|
||||
cv.Optional(df.CONF_THEME): cv.Schema(
|
||||
{
|
||||
cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA)
|
||||
for name, w in WIDGET_TYPES.items()
|
||||
}
|
||||
),
|
||||
cv.Optional(df.CONF_THEME): _theme_schema,
|
||||
cv.Optional(df.CONF_GRADIENTS): GRADIENT_SCHEMA,
|
||||
cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema,
|
||||
cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG,
|
||||
|
||||
@@ -411,6 +411,10 @@ def any_widget_schema(extras=None):
|
||||
Dynamically generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of
|
||||
widget under the widgets: key.
|
||||
|
||||
This uses lazy evaluation - the schema is built when called during validation,
|
||||
not at import time. This allows external components to register widgets
|
||||
before schema validation begins.
|
||||
|
||||
:param extras: Additional schema to be applied to each generated one
|
||||
:return: A validator for the Widgets key
|
||||
"""
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import sys
|
||||
|
||||
from esphome import automation, codegen as cg
|
||||
from esphome.automation import register_action
|
||||
from esphome.config_validation import Schema
|
||||
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.cpp_types import esphome_ns
|
||||
|
||||
@@ -124,13 +126,16 @@ class WidgetType:
|
||||
schema=None,
|
||||
modify_schema=None,
|
||||
lv_name=None,
|
||||
is_mock: bool = False,
|
||||
):
|
||||
"""
|
||||
:param name: The widget name, e.g. "bar"
|
||||
:param w_type: The C type of the widget
|
||||
:param parts: What parts this widget supports
|
||||
:param schema: The config schema for defining a widget
|
||||
:param modify_schema: A schema to update the widget
|
||||
:param modify_schema: A schema to update the widget, defaults to the same as the schema
|
||||
:param lv_name: The name of the LVGL widget in the LVGL library, if different from the name
|
||||
:param is_mock: Whether this widget is a mock widget, i.e. not a real LVGL widget
|
||||
"""
|
||||
self.name = name
|
||||
self.lv_name = lv_name or name
|
||||
@@ -146,6 +151,22 @@ class WidgetType:
|
||||
self.modify_schema = modify_schema
|
||||
self.mock_obj = MockObj(f"lv_{self.lv_name}", "_")
|
||||
|
||||
# Local import to avoid circular import
|
||||
from .automation import update_to_code
|
||||
from .schemas import WIDGET_TYPES, create_modify_schema
|
||||
|
||||
if not is_mock:
|
||||
if self.name in WIDGET_TYPES:
|
||||
raise EsphomeError(f"Duplicate definition of widget type '{self.name}'")
|
||||
WIDGET_TYPES[self.name] = self
|
||||
|
||||
# Register the update action automatically
|
||||
register_action(
|
||||
f"lvgl.{self.name}.update",
|
||||
ObjUpdateAction,
|
||||
create_modify_schema(self),
|
||||
)(update_to_code)
|
||||
|
||||
@property
|
||||
def animated(self):
|
||||
return False
|
||||
|
||||
@@ -213,17 +213,14 @@ class LvScrActType(WidgetType):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("lv_scr_act()", lv_obj_t, ())
|
||||
super().__init__("lv_scr_act()", lv_obj_t, (), is_mock=True)
|
||||
|
||||
async def to_code(self, w, config: dict):
|
||||
return []
|
||||
|
||||
|
||||
lv_scr_act_spec = LvScrActType()
|
||||
|
||||
|
||||
def get_scr_act(lv_comp: MockObj) -> Widget:
|
||||
return Widget.create(None, lv_comp.get_scr_act(), lv_scr_act_spec, {})
|
||||
return Widget.create(None, lv_comp.get_scr_act(), LvScrActType(), {})
|
||||
|
||||
|
||||
def get_widget_generator(wid):
|
||||
|
||||
@@ -2,7 +2,7 @@ from esphome import automation
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE
|
||||
|
||||
from ..automation import action_to_code, update_to_code
|
||||
from ..automation import action_to_code
|
||||
from ..defines import (
|
||||
CONF_CURSOR,
|
||||
CONF_DECIMAL_PLACES,
|
||||
@@ -171,17 +171,3 @@ async def spinbox_decrement(config, action_id, template_arg, args):
|
||||
lv.spinbox_decrement(w.obj)
|
||||
|
||||
return await action_to_code(widgets, do_increment, action_id, template_arg, args)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.spinbox.update",
|
||||
ObjUpdateAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(lv_spinbox_t),
|
||||
cv.Required(CONF_VALUE): lv_float,
|
||||
}
|
||||
),
|
||||
)
|
||||
async def spinbox_update_to_code(config, action_id, template_arg, args):
|
||||
return await update_to_code(config, action_id, template_arg, args)
|
||||
|
||||
0
esphome/components/rx8130/__init__.py
Normal file
0
esphome/components/rx8130/__init__.py
Normal file
127
esphome/components/rx8130/rx8130.cpp
Normal file
127
esphome/components/rx8130/rx8130.cpp
Normal file
@@ -0,0 +1,127 @@
|
||||
#include "rx8130.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
// https://download.epsondevice.com/td/pdf/app/RX8130CE_en.pdf
|
||||
|
||||
namespace esphome {
|
||||
namespace rx8130 {
|
||||
|
||||
static const uint8_t RX8130_REG_SEC = 0x10;
|
||||
static const uint8_t RX8130_REG_MIN = 0x11;
|
||||
static const uint8_t RX8130_REG_HOUR = 0x12;
|
||||
static const uint8_t RX8130_REG_WDAY = 0x13;
|
||||
static const uint8_t RX8130_REG_MDAY = 0x14;
|
||||
static const uint8_t RX8130_REG_MONTH = 0x15;
|
||||
static const uint8_t RX8130_REG_YEAR = 0x16;
|
||||
static const uint8_t RX8130_REG_EXTEN = 0x1C;
|
||||
static const uint8_t RX8130_REG_FLAG = 0x1D;
|
||||
static const uint8_t RX8130_REG_CTRL0 = 0x1E;
|
||||
static const uint8_t RX8130_REG_CTRL1 = 0x1F;
|
||||
static const uint8_t RX8130_REG_DIG_OFFSET = 0x30;
|
||||
static const uint8_t RX8130_BIT_CTRL_STOP = 0x40;
|
||||
static const uint8_t RX8130_BAT_FLAGS = 0x30;
|
||||
static const uint8_t RX8130_CLEAR_FLAGS = 0x00;
|
||||
|
||||
static const char *const TAG = "rx8130";
|
||||
|
||||
constexpr uint8_t bcd2dec(uint8_t val) { return (val >> 4) * 10 + (val & 0x0f); }
|
||||
constexpr uint8_t dec2bcd(uint8_t val) { return ((val / 10) << 4) + (val % 10); }
|
||||
|
||||
void RX8130Component::setup() {
|
||||
// Set digital offset to disabled with no offset
|
||||
if (this->write_register(RX8130_REG_DIG_OFFSET, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// Disable wakeup timers
|
||||
if (this->write_register(RX8130_REG_EXTEN, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// Clear VLF flag in case there has been data loss
|
||||
if (this->write_register(RX8130_REG_FLAG, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// Clear test flag and disable interrupts
|
||||
if (this->write_register(RX8130_REG_CTRL0, &RX8130_CLEAR_FLAGS, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// Enable battery charging and switching
|
||||
if (this->write_register(RX8130_REG_CTRL1, &RX8130_BAT_FLAGS, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// Clear STOP bit
|
||||
this->stop_(false);
|
||||
}
|
||||
|
||||
void RX8130Component::update() { this->read_time(); }
|
||||
|
||||
void RX8130Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "RX8130:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
}
|
||||
|
||||
void RX8130Component::read_time() {
|
||||
uint8_t date[7];
|
||||
if (this->read_register(RX8130_REG_SEC, date, 7) != i2c::ERROR_OK) {
|
||||
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
|
||||
return;
|
||||
}
|
||||
ESPTime rtc_time{
|
||||
.second = bcd2dec(date[0] & 0x7f),
|
||||
.minute = bcd2dec(date[1] & 0x7f),
|
||||
.hour = bcd2dec(date[2] & 0x3f),
|
||||
.day_of_week = bcd2dec(date[3] & 0x7f),
|
||||
.day_of_month = bcd2dec(date[4] & 0x3f),
|
||||
.day_of_year = 1, // ignored by recalc_timestamp_utc(false)
|
||||
.month = bcd2dec(date[5] & 0x1f),
|
||||
.year = static_cast<uint16_t>(bcd2dec(date[6]) + 2000),
|
||||
.is_dst = false, // not used
|
||||
.timestamp = 0 // overwritten by recalc_timestamp_utc(false)
|
||||
};
|
||||
rtc_time.recalc_timestamp_utc(false);
|
||||
if (!rtc_time.is_valid()) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Read UTC time: %04d-%02d-%02d %02d:%02d:%02d", rtc_time.year, rtc_time.month, rtc_time.day_of_month,
|
||||
rtc_time.hour, rtc_time.minute, rtc_time.second);
|
||||
time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp);
|
||||
}
|
||||
|
||||
void RX8130Component::write_time() {
|
||||
auto now = time::RealTimeClock::utcnow();
|
||||
if (!now.is_valid()) {
|
||||
ESP_LOGE(TAG, "Invalid system time, not syncing to RTC.");
|
||||
return;
|
||||
}
|
||||
uint8_t buff[7];
|
||||
buff[0] = dec2bcd(now.second);
|
||||
buff[1] = dec2bcd(now.minute);
|
||||
buff[2] = dec2bcd(now.hour);
|
||||
buff[3] = dec2bcd(now.day_of_week);
|
||||
buff[4] = dec2bcd(now.day_of_month);
|
||||
buff[5] = dec2bcd(now.month);
|
||||
buff[6] = dec2bcd(now.year % 100);
|
||||
this->stop_(true);
|
||||
if (this->write_register(RX8130_REG_SEC, buff, 7) != i2c::ERROR_OK) {
|
||||
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Wrote UTC time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour,
|
||||
now.minute, now.second);
|
||||
}
|
||||
this->stop_(false);
|
||||
}
|
||||
|
||||
void RX8130Component::stop_(bool stop) {
|
||||
const uint8_t data = stop ? RX8130_BIT_CTRL_STOP : RX8130_CLEAR_FLAGS;
|
||||
if (this->write_register(RX8130_REG_CTRL0, &data, 1) != i2c::ERROR_OK) {
|
||||
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace rx8130
|
||||
} // namespace esphome
|
||||
35
esphome/components/rx8130/rx8130.h
Normal file
35
esphome/components/rx8130/rx8130.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace rx8130 {
|
||||
|
||||
class RX8130Component : public time::RealTimeClock, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
void read_time();
|
||||
void write_time();
|
||||
/// Ensure RTC is initialized at the correct time in the setup sequence
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
protected:
|
||||
void stop_(bool stop);
|
||||
};
|
||||
|
||||
template<typename... Ts> class WriteAction : public Action<Ts...>, public Parented<RX8130Component> {
|
||||
public:
|
||||
void play(const Ts... x) override { this->parent_->write_time(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class ReadAction : public Action<Ts...>, public Parented<RX8130Component> {
|
||||
public:
|
||||
void play(const Ts... x) override { this->parent_->read_time(); }
|
||||
};
|
||||
|
||||
} // namespace rx8130
|
||||
} // namespace esphome
|
||||
56
esphome/components/rx8130/time.py
Normal file
56
esphome/components/rx8130/time.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, time
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
CODEOWNERS = ["@beormund"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
rx8130_ns = cg.esphome_ns.namespace("rx8130")
|
||||
RX8130Component = rx8130_ns.class_("RX8130Component", time.RealTimeClock, i2c.I2CDevice)
|
||||
WriteAction = rx8130_ns.class_("WriteAction", automation.Action)
|
||||
ReadAction = rx8130_ns.class_("ReadAction", automation.Action)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(RX8130Component),
|
||||
}
|
||||
).extend(i2c.i2c_device_schema(0x32))
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"rx8130.write_time",
|
||||
WriteAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(RX8130Component),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def rx8130_write_time_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"rx8130.read_time",
|
||||
ReadAction,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(RX8130Component),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def rx8130_read_time_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
await time.register_time(var, config)
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
from esphome import automation
|
||||
from esphome.automation import Condition
|
||||
import esphome.codegen as cg
|
||||
@@ -42,6 +44,7 @@ from esphome.const import (
|
||||
CONF_TTLS_PHASE_2,
|
||||
CONF_USE_ADDRESS,
|
||||
CONF_USERNAME,
|
||||
Platform,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority
|
||||
@@ -49,10 +52,13 @@ import esphome.final_validate as fv
|
||||
|
||||
from . import wpa2_eap
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
AUTO_LOAD = ["network"]
|
||||
|
||||
NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4]
|
||||
CONF_SAVE = "save"
|
||||
CONF_MIN_AUTH_MODE = "min_auth_mode"
|
||||
|
||||
# Maximum number of WiFi networks that can be configured
|
||||
# Limited to 127 because selected_sta_index_ is int8_t in C++
|
||||
@@ -70,6 +76,14 @@ WIFI_POWER_SAVE_MODES = {
|
||||
"LIGHT": WiFiPowerSaveMode.WIFI_POWER_SAVE_LIGHT,
|
||||
"HIGH": WiFiPowerSaveMode.WIFI_POWER_SAVE_HIGH,
|
||||
}
|
||||
|
||||
WifiMinAuthMode = wifi_ns.enum("WifiMinAuthMode")
|
||||
WIFI_MIN_AUTH_MODES = {
|
||||
"WPA": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA,
|
||||
"WPA2": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA2,
|
||||
"WPA3": WifiMinAuthMode.WIFI_MIN_AUTH_MODE_WPA3,
|
||||
}
|
||||
VALIDATE_WIFI_MIN_AUTH_MODE = cv.enum(WIFI_MIN_AUTH_MODES, upper=True)
|
||||
WiFiConnectedCondition = wifi_ns.class_("WiFiConnectedCondition", Condition)
|
||||
WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition)
|
||||
WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action)
|
||||
@@ -187,6 +201,27 @@ def validate_variant(_):
|
||||
raise cv.Invalid(f"WiFi requires component esp32_hosted on {variant}")
|
||||
|
||||
|
||||
def _apply_min_auth_mode_default(config):
|
||||
"""Apply platform-specific default for min_auth_mode and warn ESP8266 users."""
|
||||
# Only apply defaults for platforms that support min_auth_mode
|
||||
if CONF_MIN_AUTH_MODE not in config and (CORE.is_esp8266 or CORE.is_esp32):
|
||||
if CORE.is_esp8266:
|
||||
_LOGGER.warning(
|
||||
"The minimum WiFi authentication mode (wifi -> min_auth_mode) is not set. "
|
||||
"This controls the weakest encryption your device will accept when connecting to WiFi. "
|
||||
"Currently defaults to WPA (less secure), but will change to WPA2 (more secure) in 2026.6.0. "
|
||||
"WPA uses TKIP encryption which has known security vulnerabilities and should be avoided. "
|
||||
"WPA2 uses AES encryption which is significantly more secure. "
|
||||
"To silence this warning, explicitly set min_auth_mode under 'wifi:'. "
|
||||
"If your router supports WPA2 or WPA3, set 'min_auth_mode: WPA2'. "
|
||||
"If your router only supports WPA, set 'min_auth_mode: WPA'."
|
||||
)
|
||||
config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA")
|
||||
elif CORE.is_esp32:
|
||||
config[CONF_MIN_AUTH_MODE] = VALIDATE_WIFI_MIN_AUTH_MODE("WPA2")
|
||||
return config
|
||||
|
||||
|
||||
def final_validate(config):
|
||||
has_sta = bool(config.get(CONF_NETWORKS, True))
|
||||
has_ap = CONF_AP in config
|
||||
@@ -287,6 +322,10 @@ CONFIG_SCHEMA = cv.All(
|
||||
): cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
|
||||
cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean,
|
||||
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
||||
cv.Optional(CONF_MIN_AUTH_MODE): cv.All(
|
||||
VALIDATE_WIFI_MIN_AUTH_MODE,
|
||||
cv.only_on([Platform.ESP32, Platform.ESP8266]),
|
||||
),
|
||||
cv.SplitDefault(CONF_OUTPUT_POWER, esp8266=20.0): cv.All(
|
||||
cv.decibel, cv.float_range(min=8.5, max=20.5)
|
||||
),
|
||||
@@ -311,6 +350,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
}
|
||||
),
|
||||
_apply_min_auth_mode_default,
|
||||
_validate,
|
||||
)
|
||||
|
||||
@@ -420,6 +460,8 @@ async def to_code(config):
|
||||
|
||||
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
||||
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
|
||||
if CONF_MIN_AUTH_MODE in config:
|
||||
cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE]))
|
||||
if config[CONF_FAST_CONNECT]:
|
||||
cg.add_define("USE_WIFI_FAST_CONNECT")
|
||||
cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN]))
|
||||
|
||||
@@ -42,6 +42,258 @@ namespace wifi {
|
||||
|
||||
static const char *const TAG = "wifi";
|
||||
|
||||
/// WiFi Retry Logic - Priority-Based BSSID Selection
|
||||
///
|
||||
/// The WiFi component uses a state machine with priority degradation to handle connection failures
|
||||
/// and automatically cycle through different BSSIDs in mesh networks or multiple configured networks.
|
||||
///
|
||||
/// Connection Flow:
|
||||
/// ┌──────────────────────────────────────────────────────────────────────┐
|
||||
/// │ Fast Connect Path (Optional) │
|
||||
/// ├──────────────────────────────────────────────────────────────────────┤
|
||||
/// │ Entered if: configuration has 'fast_connect: true' │
|
||||
/// │ Optimization to skip scanning when possible: │
|
||||
/// │ │
|
||||
/// │ 1. INITIAL_CONNECT → Try one of: │
|
||||
/// │ a) Saved BSSID+channel (from previous boot) │
|
||||
/// │ b) First configured non-hidden network (any BSSID) │
|
||||
/// │ ↓ │
|
||||
/// │ [FAILED] → Check if more configured networks available │
|
||||
/// │ ↓ │
|
||||
/// │ 2. FAST_CONNECT_CYCLING_APS → Try remaining configured networks │
|
||||
/// │ (1 attempt each, any BSSID) │
|
||||
/// │ ↓ │
|
||||
/// │ [All Failed] → Fall through to explicit hidden or scanning │
|
||||
/// │ │
|
||||
/// │ Note: Fast connect data saved from previous successful connection │
|
||||
/// └──────────────────────────────────────────────────────────────────────┘
|
||||
/// ↓
|
||||
/// ┌──────────────────────────────────────────────────────────────────────┐
|
||||
/// │ Explicit Hidden Networks Path (Optional) │
|
||||
/// ├──────────────────────────────────────────────────────────────────────┤
|
||||
/// │ Entered if: first configured network has 'hidden: true' │
|
||||
/// │ │
|
||||
/// │ 1. EXPLICIT_HIDDEN → Try consecutive hidden networks (1 attempt) │
|
||||
/// │ Stop when visible network reached │
|
||||
/// │ ↓ │
|
||||
/// │ Example: Hidden1, Hidden2, Visible1, Hidden3, Visible2 │
|
||||
/// │ Try: Hidden1, Hidden2 (stop at Visible1) │
|
||||
/// │ ↓ │
|
||||
/// │ [All Failed] → Fall back to scan-based connection │
|
||||
/// │ │
|
||||
/// │ Note: Fast connect saves BSSID after first successful connection, │
|
||||
/// │ so subsequent boots use fast path instead of hidden mode │
|
||||
/// └──────────────────────────────────────────────────────────────────────┘
|
||||
/// ↓
|
||||
/// ┌──────────────────────────────────────────────────────────────────────┐
|
||||
/// │ Scan-Based Connection Path │
|
||||
/// ├──────────────────────────────────────────────────────────────────────┤
|
||||
/// │ │
|
||||
/// │ 1. SCAN → Sort by priority (highest first), then RSSI │
|
||||
/// │ ┌─────────────────────────────────────────────────┐ │
|
||||
/// │ │ scan_result_[0] = Best BSSID (highest priority) │ │
|
||||
/// │ │ scan_result_[1] = Second best │ │
|
||||
/// │ │ scan_result_[2] = Third best │ │
|
||||
/// │ └─────────────────────────────────────────────────┘ │
|
||||
/// │ ↓ │
|
||||
/// │ 2. SCAN_CONNECTING → Try scan_result_[0] (2 attempts) │
|
||||
/// │ (Visible1, Visible2 from example above) │
|
||||
/// │ ↓ │
|
||||
/// │ 3. FAILED → Decrease priority: 0.0 → -1.0 → -2.0 │
|
||||
/// │ (stored in persistent sta_priorities_) │
|
||||
/// │ ↓ │
|
||||
/// │ 4. Check for hidden networks: │
|
||||
/// │ - If found → RETRY_HIDDEN (try SSIDs not in scan, 1 attempt) │
|
||||
/// │ Skip hidden networks before first visible one │
|
||||
/// │ (Skip Hidden1/Hidden2, try Hidden3 from example) │
|
||||
/// │ - If none → Skip RETRY_HIDDEN, go to step 5 │
|
||||
/// │ ↓ │
|
||||
/// │ 5. FAILED → RESTARTING_ADAPTER (skipped if AP/improv active) │
|
||||
/// │ ↓ │
|
||||
/// │ 6. Loop back to start: │
|
||||
/// │ - If first network is hidden → EXPLICIT_HIDDEN (retry cycle) │
|
||||
/// │ - Otherwise → SCAN_CONNECTING (rescan) │
|
||||
/// │ ↓ │
|
||||
/// │ 7. RESCAN → Apply stored priorities, sort again │
|
||||
/// │ ┌─────────────────────────────────────────────────┐ │
|
||||
/// │ │ scan_result_[0] = BSSID B (priority 0.0) ← NEW │ │
|
||||
/// │ │ scan_result_[1] = BSSID C (priority 0.0) │ │
|
||||
/// │ │ scan_result_[2] = BSSID A (priority -2.0) ← OLD │ │
|
||||
/// │ └─────────────────────────────────────────────────┘ │
|
||||
/// │ ↓ │
|
||||
/// │ 8. SCAN_CONNECTING → Try scan_result_[0] (next best) │
|
||||
/// │ │
|
||||
/// │ Key: Priority system cycles through BSSIDs ACROSS scan cycles │
|
||||
/// │ Full retry cycle: EXPLICIT_HIDDEN → SCAN → RETRY_HIDDEN │
|
||||
/// │ Always try best available BSSID (scan_result_[0]) │
|
||||
/// └──────────────────────────────────────────────────────────────────────┘
|
||||
///
|
||||
/// Retry Phases:
|
||||
/// - INITIAL_CONNECT: Try saved BSSID+channel (fast_connect), or fall back to normal flow
|
||||
/// - FAST_CONNECT_CYCLING_APS: Cycle through remaining configured networks (1 attempt each, fast_connect only)
|
||||
/// - EXPLICIT_HIDDEN: Try consecutive networks marked hidden:true before scanning (1 attempt per SSID)
|
||||
/// - SCAN_CONNECTING: Connect using scan results (2 attempts per BSSID)
|
||||
/// - RETRY_HIDDEN: Try networks not found in scan (1 attempt per SSID, skipped if none found)
|
||||
/// - RESTARTING_ADAPTER: Restart WiFi adapter to clear stuck state
|
||||
///
|
||||
/// Hidden Network Handling:
|
||||
/// - Networks marked 'hidden: true' before first non-hidden → Tried in EXPLICIT_HIDDEN phase
|
||||
/// - Networks marked 'hidden: true' after first non-hidden → Tried in RETRY_HIDDEN phase
|
||||
/// - After successful connection, fast_connect saves BSSID → subsequent boots use fast path
|
||||
/// - Networks not in scan results → Tried in RETRY_HIDDEN phase
|
||||
/// - Networks visible in scan + not marked hidden → Skipped in RETRY_HIDDEN phase
|
||||
/// - Networks marked 'hidden: true' always use hidden mode, even if broadcasting SSID
|
||||
|
||||
static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
|
||||
switch (phase) {
|
||||
case WiFiRetryPhase::INITIAL_CONNECT:
|
||||
return LOG_STR("INITIAL_CONNECT");
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS:
|
||||
return LOG_STR("FAST_CONNECT_CYCLING");
|
||||
#endif
|
||||
case WiFiRetryPhase::EXPLICIT_HIDDEN:
|
||||
return LOG_STR("EXPLICIT_HIDDEN");
|
||||
case WiFiRetryPhase::SCAN_CONNECTING:
|
||||
return LOG_STR("SCAN_CONNECTING");
|
||||
case WiFiRetryPhase::RETRY_HIDDEN:
|
||||
return LOG_STR("RETRY_HIDDEN");
|
||||
case WiFiRetryPhase::RESTARTING_ADAPTER:
|
||||
return LOG_STR("RESTARTING");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
bool WiFiComponent::went_through_explicit_hidden_phase_() const {
|
||||
// If first configured network is marked hidden, we went through EXPLICIT_HIDDEN phase
|
||||
// This means those networks were already tried and should be skipped in RETRY_HIDDEN
|
||||
return !this->sta_.empty() && this->sta_[0].get_hidden();
|
||||
}
|
||||
|
||||
int8_t WiFiComponent::find_first_non_hidden_index_() const {
|
||||
// Find the first network that is NOT marked hidden:true
|
||||
// This is where EXPLICIT_HIDDEN phase would have stopped
|
||||
for (size_t i = 0; i < this->sta_.size(); i++) {
|
||||
if (!this->sta_[i].get_hidden()) {
|
||||
return static_cast<int8_t>(i);
|
||||
}
|
||||
}
|
||||
return -1; // All networks are hidden
|
||||
}
|
||||
|
||||
// 2 attempts per BSSID in SCAN_CONNECTING phase
|
||||
// Rationale: This is the ONLY phase where we decrease BSSID priority, so we must be very sure.
|
||||
// Auth failures are common immediately after scan due to WiFi stack state transitions.
|
||||
// Trying twice filters out false positives and prevents unnecessarily marking a good BSSID as bad.
|
||||
// After 2 genuine failures, priority degradation ensures we skip this BSSID on subsequent scans.
|
||||
static constexpr uint8_t WIFI_RETRY_COUNT_PER_BSSID = 2;
|
||||
|
||||
// 1 attempt per SSID in RETRY_HIDDEN phase
|
||||
// Rationale: Try hidden mode once, then rescan to get next best BSSID via priority system
|
||||
static constexpr uint8_t WIFI_RETRY_COUNT_PER_SSID = 1;
|
||||
|
||||
// 1 attempt per AP in fast_connect mode (INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS)
|
||||
// Rationale: Fast connect prioritizes speed - try each AP once to find a working one quickly
|
||||
static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1;
|
||||
|
||||
static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) {
|
||||
switch (phase) {
|
||||
case WiFiRetryPhase::INITIAL_CONNECT:
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS:
|
||||
#endif
|
||||
// INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS both use 1 attempt per AP (fast_connect mode)
|
||||
return WIFI_RETRY_COUNT_PER_AP;
|
||||
case WiFiRetryPhase::EXPLICIT_HIDDEN:
|
||||
// Explicitly hidden network: 1 attempt (user marked as hidden, try once then scan)
|
||||
return WIFI_RETRY_COUNT_PER_SSID;
|
||||
case WiFiRetryPhase::SCAN_CONNECTING:
|
||||
// Scan-based phase: 2 attempts per BSSID (handles transient auth failures after scan)
|
||||
return WIFI_RETRY_COUNT_PER_BSSID;
|
||||
case WiFiRetryPhase::RETRY_HIDDEN:
|
||||
// Hidden network mode: 1 attempt per SSID
|
||||
return WIFI_RETRY_COUNT_PER_SSID;
|
||||
default:
|
||||
return WIFI_RETRY_COUNT_PER_BSSID;
|
||||
}
|
||||
}
|
||||
|
||||
static void apply_scan_result_to_params(WiFiAP ¶ms, const WiFiScanResult &scan) {
|
||||
params.set_hidden(false);
|
||||
params.set_ssid(scan.get_ssid());
|
||||
params.set_bssid(scan.get_bssid());
|
||||
params.set_channel(scan.get_channel());
|
||||
}
|
||||
|
||||
bool WiFiComponent::needs_scan_results_() const {
|
||||
// Only SCAN_CONNECTING phase needs scan results
|
||||
if (this->retry_phase_ != WiFiRetryPhase::SCAN_CONNECTING) {
|
||||
return false;
|
||||
}
|
||||
// Need scan if we have no results or no matching networks
|
||||
return this->scan_result_.empty() || !this->scan_result_[0].get_matches();
|
||||
}
|
||||
|
||||
bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const {
|
||||
// Check if this SSID is configured as hidden
|
||||
// If explicitly marked hidden, we should always try hidden mode regardless of scan results
|
||||
for (const auto &conf : this->sta_) {
|
||||
if (conf.get_ssid() == ssid && conf.get_hidden()) {
|
||||
return false; // Treat as not seen - force hidden mode attempt
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, check if we saw it in scan results
|
||||
for (const auto &scan : this->scan_result_) {
|
||||
if (scan.get_ssid() == ssid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index, bool include_explicit_hidden) {
|
||||
// Find next SSID that wasn't in scan results (might be hidden)
|
||||
// Start searching from start_index + 1
|
||||
for (size_t i = start_index + 1; i < this->sta_.size(); i++) {
|
||||
const auto &sta = this->sta_[i];
|
||||
|
||||
// Skip networks that were already tried in EXPLICIT_HIDDEN phase
|
||||
// Those are: networks marked hidden:true that appear before the first non-hidden network
|
||||
if (!include_explicit_hidden && sta.get_hidden()) {
|
||||
int8_t first_non_hidden_idx = this->find_first_non_hidden_index_();
|
||||
if (first_non_hidden_idx >= 0 && static_cast<int8_t>(i) < first_non_hidden_idx) {
|
||||
ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.get_ssid().c_str());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->ssid_was_seen_in_scan_(sta.get_ssid())) {
|
||||
ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast<int>(i));
|
||||
return static_cast<int8_t>(i);
|
||||
}
|
||||
ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (visible in scan)", sta.get_ssid().c_str());
|
||||
}
|
||||
// No hidden SSIDs found
|
||||
return -1;
|
||||
}
|
||||
|
||||
void WiFiComponent::start_initial_connection_() {
|
||||
// If first network (highest priority) is explicitly marked hidden, try it first before scanning
|
||||
// This respects user's priority order when they explicitly configure hidden networks
|
||||
if (!this->sta_.empty() && this->sta_[0].get_hidden()) {
|
||||
ESP_LOGI(TAG, "Starting with explicit hidden network (highest priority)");
|
||||
this->selected_sta_index_ = 0;
|
||||
this->retry_phase_ = WiFiRetryPhase::EXPLICIT_HIDDEN;
|
||||
WiFiAP params = this->build_params_for_current_phase_();
|
||||
this->start_connecting(params, false);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "Starting scan");
|
||||
this->start_scanning();
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
static const char *eap_phase2_to_str(esp_eap_ttls_phase2_types type) {
|
||||
switch (type) {
|
||||
@@ -109,18 +361,28 @@ void WiFiComponent::start() {
|
||||
ESP_LOGV(TAG, "Setting Power Save Option failed");
|
||||
}
|
||||
|
||||
this->transition_to_phase_(WiFiRetryPhase::INITIAL_CONNECT);
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
WiFiAP params;
|
||||
this->trying_loaded_ap_ = this->load_fast_connect_settings_(params);
|
||||
if (!this->trying_loaded_ap_) {
|
||||
// FAST CONNECT FALLBACK: No saved settings available
|
||||
// Use first config (will use SSID from config)
|
||||
bool loaded_fast_connect = this->load_fast_connect_settings_(params);
|
||||
// Fast connect optimization: only use when we have saved BSSID+channel data
|
||||
// Without saved data, try first configured network or use normal flow
|
||||
if (loaded_fast_connect) {
|
||||
ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.get_ssid().c_str());
|
||||
this->start_connecting(params, false);
|
||||
} else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) {
|
||||
// No saved data, but have configured networks - try first non-hidden network
|
||||
ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].get_ssid().c_str());
|
||||
this->selected_sta_index_ = 0;
|
||||
params = this->build_wifi_ap_from_selected_();
|
||||
params = this->build_params_for_current_phase_();
|
||||
this->start_connecting(params, false);
|
||||
} else {
|
||||
// No saved data and (no networks OR first is hidden) - use normal flow
|
||||
this->start_initial_connection_();
|
||||
}
|
||||
this->start_connecting(params, false);
|
||||
#else
|
||||
this->start_scanning();
|
||||
// Without fast_connect: go straight to scanning (or hidden mode if all networks are hidden)
|
||||
this->start_initial_connection_();
|
||||
#endif
|
||||
#ifdef USE_WIFI_AP
|
||||
} else if (this->has_ap()) {
|
||||
@@ -150,8 +412,7 @@ void WiFiComponent::restart_adapter() {
|
||||
ESP_LOGW(TAG, "Restarting adapter");
|
||||
this->wifi_mode_(false, {});
|
||||
delay(100); // NOLINT
|
||||
this->num_retried_ = 0;
|
||||
this->retry_hidden_ = false;
|
||||
// Don't set retry_phase_ or num_retried_ here - state machine handles transitions
|
||||
}
|
||||
|
||||
void WiFiComponent::loop() {
|
||||
@@ -172,21 +433,19 @@ void WiFiComponent::loop() {
|
||||
case WIFI_COMPONENT_STATE_COOLDOWN: {
|
||||
this->status_set_warning(LOG_STR("waiting to reconnect"));
|
||||
if (millis() - this->action_started_ > 5000) {
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
// Safety check: Ensure selected_sta_index_ is valid before retrying
|
||||
// (should already be set by retry_connect(), but check for robustness)
|
||||
// After cooldown, connect based on current retry phase
|
||||
this->reset_selected_ap_to_first_if_invalid_();
|
||||
WiFiAP params = this->build_wifi_ap_from_selected_();
|
||||
this->start_connecting(params, false);
|
||||
#else
|
||||
if (this->retry_hidden_) {
|
||||
this->reset_selected_ap_to_first_if_invalid_();
|
||||
WiFiAP params = this->build_wifi_ap_from_selected_();
|
||||
this->start_connecting(params, false);
|
||||
} else {
|
||||
|
||||
// Check if we need to trigger a scan first
|
||||
if (this->needs_scan_results_() && !this->all_networks_hidden_()) {
|
||||
// Need scan results or no matching networks found - scan/rescan
|
||||
ESP_LOGD(TAG, "Scanning required for phase %s", LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
|
||||
this->start_scanning();
|
||||
} else {
|
||||
// Have everything we need to connect (or all networks are hidden, skip scanning)
|
||||
WiFiAP params = this->build_params_for_current_phase_();
|
||||
this->start_connecting(params, false);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -344,30 +603,44 @@ void WiFiComponent::set_sta(const WiFiAP &ap) {
|
||||
this->selected_sta_index_ = 0;
|
||||
}
|
||||
|
||||
WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const {
|
||||
// PRECONDITION: selected_sta_index_ must be valid (ensured by all callers)
|
||||
WiFiAP WiFiComponent::build_params_for_current_phase_() {
|
||||
const WiFiAP *config = this->get_selected_sta_();
|
||||
assert(config != nullptr);
|
||||
if (config == nullptr) {
|
||||
ESP_LOGE(TAG, "No valid network config (selected_sta_index_=%d, sta_.size()=%zu)",
|
||||
static_cast<int>(this->selected_sta_index_), this->sta_.size());
|
||||
// Return empty params - caller should handle this gracefully
|
||||
return WiFiAP();
|
||||
}
|
||||
|
||||
WiFiAP params = *config;
|
||||
|
||||
// SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync after wifi_scan_done():
|
||||
// - wifi_scan_done() sorts all scan results by priority/RSSI (best first)
|
||||
// - It then finds which sta_[i] config matches scan_result_[0]
|
||||
// - Sets selected_sta_index_ = i to record that matching config
|
||||
// This sync holds until scan_result_ is cleared (e.g., after connection or in reset_for_next_ap_attempt_())
|
||||
if (!this->scan_result_.empty()) {
|
||||
// Override with scan data - network is visible
|
||||
const WiFiScanResult &scan = this->scan_result_[0];
|
||||
params.set_hidden(false);
|
||||
params.set_ssid(scan.get_ssid());
|
||||
params.set_bssid(scan.get_bssid());
|
||||
params.set_channel(scan.get_channel());
|
||||
} else if (params.get_hidden()) {
|
||||
// Hidden network - clear BSSID and channel even if set in config
|
||||
// There might be multiple hidden networks with same SSID but we can't know which is correct
|
||||
// Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN.
|
||||
params.set_bssid(optional<bssid_t>{});
|
||||
params.set_channel(optional<uint8_t>{});
|
||||
switch (this->retry_phase_) {
|
||||
case WiFiRetryPhase::INITIAL_CONNECT:
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS:
|
||||
#endif
|
||||
// Fast connect phases: use config-only (no scan results)
|
||||
// BSSID/channel from config if user specified them, otherwise empty
|
||||
break;
|
||||
|
||||
case WiFiRetryPhase::EXPLICIT_HIDDEN:
|
||||
case WiFiRetryPhase::RETRY_HIDDEN:
|
||||
// Hidden network mode: clear BSSID/channel to trigger probe request
|
||||
// (both explicit hidden and retry hidden use same behavior)
|
||||
params.set_bssid(optional<bssid_t>{});
|
||||
params.set_channel(optional<uint8_t>{});
|
||||
break;
|
||||
|
||||
case WiFiRetryPhase::SCAN_CONNECTING:
|
||||
// Scan-based phase: always use best scan result (index 0 - highest priority after sorting)
|
||||
if (!this->scan_result_.empty()) {
|
||||
apply_scan_result_to_params(params, this->scan_result_[0]);
|
||||
}
|
||||
break;
|
||||
|
||||
case WiFiRetryPhase::RESTARTING_ADAPTER:
|
||||
// Should not be building params during restart
|
||||
break;
|
||||
}
|
||||
|
||||
return params;
|
||||
@@ -392,7 +665,21 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa
|
||||
}
|
||||
|
||||
void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
|
||||
ESP_LOGI(TAG, "Connecting to '%s'", ap.get_ssid().c_str());
|
||||
// Log connection attempt at INFO level with priority
|
||||
std::string bssid_formatted;
|
||||
float priority = 0.0f;
|
||||
|
||||
if (ap.get_bssid().has_value()) {
|
||||
bssid_formatted = format_mac_address_pretty(ap.get_bssid().value().data());
|
||||
priority = this->get_sta_priority(ap.get_bssid().value());
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %.1f, attempt %u/%u in phase %s)...",
|
||||
ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_formatted.c_str() : LOG_STR_LITERAL("any"),
|
||||
priority, this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_),
|
||||
LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
ESP_LOGV(TAG, "Connection Params:");
|
||||
ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str());
|
||||
@@ -565,8 +852,39 @@ void WiFiComponent::start_scanning() {
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING;
|
||||
}
|
||||
|
||||
// Helper function for WiFi scan result comparison
|
||||
// Returns true if 'a' should be placed before 'b' in the sorted order
|
||||
/// Comparator for WiFi scan result sorting - determines which network should be tried first
|
||||
/// Returns true if 'a' should be placed before 'b' in the sorted order (a is "better" than b)
|
||||
///
|
||||
/// Sorting logic (in priority order):
|
||||
/// 1. Matching networks always ranked before non-matching networks
|
||||
/// 2. For matching networks: Priority first (CRITICAL - tracks failure history)
|
||||
/// 3. RSSI as tiebreaker for equal priority or non-matching networks
|
||||
///
|
||||
/// WHY PRIORITY MUST BE CHECKED FIRST:
|
||||
/// The priority field tracks connection failure history via priority degradation:
|
||||
/// - Initial priority: 0.0 (from config or default)
|
||||
/// - Each connection failure: priority -= 1.0 (becomes -1.0, -2.0, -3.0, etc.)
|
||||
/// - Failed BSSIDs sorted lower → naturally try different BSSID on next scan
|
||||
///
|
||||
/// This enables automatic BSSID cycling for various real-world failure scenarios:
|
||||
/// - Crashed/hung AP (visible but not responding)
|
||||
/// - Misconfigured mesh node (accepts auth but no DHCP/routing)
|
||||
/// - Capacity limits (AP refuses new clients)
|
||||
/// - Rogue AP (same SSID, wrong password or malicious)
|
||||
/// - Intermittent hardware issues (flaky radio, overheating)
|
||||
///
|
||||
/// Example mesh network: 3 APs with same SSID "home", all at priority 0.0 initially
|
||||
/// - Try strongest BSSID A (sorted by RSSI) → fails → priority A becomes -1.0
|
||||
/// - Next scan: BSSID B and C (priority 0.0) sorted BEFORE A (priority -1.0)
|
||||
/// - Try next strongest BSSID B → succeeds or fails and gets deprioritized
|
||||
/// - System naturally cycles through all BSSIDs via priority degradation
|
||||
/// - Eventually finds working AP or tries all options before restarting adapter
|
||||
///
|
||||
/// If we checked RSSI first (Bug in PR #9963):
|
||||
/// - Same failed BSSID would keep being selected if it has strongest signal
|
||||
/// - Device stuck connecting to crashed AP with -30dBm while working AP at -50dBm ignored
|
||||
/// - Priority degradation would be useless
|
||||
/// - Mesh networks would never recover from single AP failure
|
||||
[[nodiscard]] inline static bool wifi_scan_result_is_better(const WiFiScanResult &a, const WiFiScanResult &b) {
|
||||
// Matching networks always come before non-matching
|
||||
if (a.get_matches() && !b.get_matches())
|
||||
@@ -574,21 +892,13 @@ void WiFiComponent::start_scanning() {
|
||||
if (!a.get_matches() && b.get_matches())
|
||||
return false;
|
||||
|
||||
if (a.get_matches() && b.get_matches()) {
|
||||
// For APs with the same SSID, always prefer stronger signal
|
||||
// This helps with mesh networks and multiple APs
|
||||
if (a.get_ssid() == b.get_ssid()) {
|
||||
return a.get_rssi() > b.get_rssi();
|
||||
}
|
||||
|
||||
// For different SSIDs, check priority first
|
||||
if (a.get_priority() != b.get_priority())
|
||||
return a.get_priority() > b.get_priority();
|
||||
// If priorities are equal, prefer stronger signal
|
||||
return a.get_rssi() > b.get_rssi();
|
||||
// Both matching: check priority first (tracks connection failures via priority degradation)
|
||||
// Priority is decreased when a BSSID fails to connect, so lower priority = previously failed
|
||||
if (a.get_matches() && b.get_matches() && a.get_priority() != b.get_priority()) {
|
||||
return a.get_priority() > b.get_priority();
|
||||
}
|
||||
|
||||
// Both don't match - sort by signal strength
|
||||
// Use RSSI as tiebreaker (for equal-priority matching networks or all non-matching networks)
|
||||
return a.get_rssi() > b.get_rssi();
|
||||
}
|
||||
|
||||
@@ -623,10 +933,8 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res)
|
||||
ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
|
||||
res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
|
||||
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
|
||||
ESP_LOGD(TAG,
|
||||
" Channel: %u\n"
|
||||
" RSSI: %d dB",
|
||||
res.get_channel(), res.get_rssi());
|
||||
ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4.1f", res.get_channel(), res.get_rssi(),
|
||||
res.get_priority());
|
||||
} else {
|
||||
ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s,
|
||||
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
|
||||
@@ -675,34 +983,36 @@ void WiFiComponent::check_scanning_finished() {
|
||||
// SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_
|
||||
// After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config
|
||||
// matches that network and record it in selected_sta_index_. This keeps the two indices
|
||||
// synchronized so build_wifi_ap_from_selected_() can safely use both to build connection parameters.
|
||||
// synchronized so build_params_for_current_phase_() can safely use both to build connection parameters.
|
||||
const WiFiScanResult &scan_res = this->scan_result_[0];
|
||||
if (!scan_res.get_matches()) {
|
||||
ESP_LOGW(TAG, "No matching network found");
|
||||
this->retry_connect();
|
||||
return;
|
||||
}
|
||||
|
||||
bool found_match = false;
|
||||
for (size_t i = 0; i < this->sta_.size(); i++) {
|
||||
if (scan_res.matches(this->sta_[i])) {
|
||||
// Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation
|
||||
// No overflow check needed - YAML validation prevents >127 networks
|
||||
this->selected_sta_index_ = static_cast<int8_t>(i); // Links scan_result_[0] with sta_[i]
|
||||
found_match = true;
|
||||
break;
|
||||
if (scan_res.get_matches()) {
|
||||
for (size_t i = 0; i < this->sta_.size(); i++) {
|
||||
if (scan_res.matches(this->sta_[i])) {
|
||||
// Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation
|
||||
// No overflow check needed - YAML validation prevents >127 networks
|
||||
this->selected_sta_index_ = static_cast<int8_t>(i); // Links scan_result_[0] with sta_[i]
|
||||
found_match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found_match) {
|
||||
ESP_LOGW(TAG, "No matching network found");
|
||||
this->retry_connect();
|
||||
return;
|
||||
// No scan results matched our configured networks - transition directly to hidden mode
|
||||
// Don't call retry_connect() since we never attempted a connection (no BSSID to penalize)
|
||||
this->transition_to_phase_(WiFiRetryPhase::RETRY_HIDDEN);
|
||||
// Now start connection attempt in hidden mode
|
||||
} else if (this->transition_to_phase_(WiFiRetryPhase::SCAN_CONNECTING)) {
|
||||
return; // scan started, wait for next loop iteration
|
||||
}
|
||||
|
||||
yield();
|
||||
|
||||
WiFiAP params = this->build_wifi_ap_from_selected_();
|
||||
WiFiAP params = this->build_params_for_current_phase_();
|
||||
// Ensure we're in SCAN_CONNECTING phase when connecting with scan results
|
||||
// (needed when scan was started directly without transition_to_phase_, e.g., initial scan)
|
||||
this->start_connecting(params, false);
|
||||
}
|
||||
|
||||
@@ -724,11 +1034,14 @@ void WiFiComponent::check_connecting_finished() {
|
||||
ESP_LOGI(TAG, "Connected");
|
||||
// Warn if we had to retry with hidden network mode for a network that's not marked hidden
|
||||
// Only warn if we actually connected without scan data (SSID only), not if scan succeeded on retry
|
||||
if (const WiFiAP *config = this->get_selected_sta_();
|
||||
this->retry_hidden_ && config && !config->get_hidden() && this->scan_result_.empty()) {
|
||||
ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config->get_ssid().c_str());
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); this->retry_phase_ == WiFiRetryPhase::RETRY_HIDDEN &&
|
||||
config && !config->get_hidden() &&
|
||||
this->scan_result_.empty()) {
|
||||
ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->get_ssid().c_str());
|
||||
}
|
||||
this->retry_hidden_ = false;
|
||||
// Reset to initial phase on successful connection (don't log transition, just reset state)
|
||||
this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT;
|
||||
this->num_retried_ = 0;
|
||||
|
||||
this->print_connect_params_();
|
||||
|
||||
@@ -796,58 +1109,334 @@ void WiFiComponent::check_connecting_finished() {
|
||||
this->retry_connect();
|
||||
}
|
||||
|
||||
void WiFiComponent::retry_connect() {
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid()) {
|
||||
auto bssid = *config->get_bssid();
|
||||
float priority = this->get_sta_priority(bssid);
|
||||
this->set_sta_priority(bssid, priority - 1.0f);
|
||||
/// Determine the next retry phase based on current state and failure conditions
|
||||
/// This function examines the current retry phase, number of retries, and failure reasons
|
||||
/// to decide what phase to move to next. It does not modify any state - it only returns
|
||||
/// the recommended next phase.
|
||||
///
|
||||
/// @return The next WiFiRetryPhase to transition to (may be same as current phase if should retry)
|
||||
WiFiRetryPhase WiFiComponent::determine_next_phase_() {
|
||||
switch (this->retry_phase_) {
|
||||
case WiFiRetryPhase::INITIAL_CONNECT:
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS:
|
||||
// INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS: no retries, try next AP or fall back to scan
|
||||
if (this->selected_sta_index_ < static_cast<int8_t>(this->sta_.size()) - 1) {
|
||||
return WiFiRetryPhase::FAST_CONNECT_CYCLING_APS; // Move to next AP
|
||||
}
|
||||
#endif
|
||||
// No more APs to try, fall back to scan
|
||||
return WiFiRetryPhase::SCAN_CONNECTING;
|
||||
|
||||
case WiFiRetryPhase::EXPLICIT_HIDDEN: {
|
||||
// Try all explicitly hidden networks before scanning
|
||||
if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) {
|
||||
return WiFiRetryPhase::EXPLICIT_HIDDEN; // Keep retrying same SSID
|
||||
}
|
||||
|
||||
// Exhausted retries on current SSID - check for more explicitly hidden networks
|
||||
// Stop when we reach a visible network (proceed to scanning)
|
||||
size_t next_index = this->selected_sta_index_ + 1;
|
||||
if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) {
|
||||
// Found another explicitly hidden network
|
||||
return WiFiRetryPhase::EXPLICIT_HIDDEN;
|
||||
}
|
||||
|
||||
// No more consecutive explicitly hidden networks - proceed to scanning
|
||||
return WiFiRetryPhase::SCAN_CONNECTING;
|
||||
}
|
||||
|
||||
case WiFiRetryPhase::SCAN_CONNECTING:
|
||||
// If scan found no matching networks, skip to hidden network mode
|
||||
if (!this->scan_result_.empty() && !this->scan_result_[0].get_matches()) {
|
||||
return WiFiRetryPhase::RETRY_HIDDEN;
|
||||
}
|
||||
|
||||
if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_BSSID) {
|
||||
return WiFiRetryPhase::SCAN_CONNECTING; // Keep retrying same BSSID
|
||||
}
|
||||
|
||||
// Exhausted retries on current BSSID (scan_result_[0])
|
||||
// Its priority has been decreased, so on next scan it will be sorted lower
|
||||
// and we'll try the next best BSSID.
|
||||
// Check if there are any potentially hidden networks to try
|
||||
if (this->find_next_hidden_sta_(-1, !this->went_through_explicit_hidden_phase_()) >= 0) {
|
||||
return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try
|
||||
}
|
||||
// No hidden networks - skip directly to restart/rescan
|
||||
if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) {
|
||||
return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN
|
||||
: WiFiRetryPhase::SCAN_CONNECTING;
|
||||
}
|
||||
return WiFiRetryPhase::RESTARTING_ADAPTER;
|
||||
|
||||
case WiFiRetryPhase::RETRY_HIDDEN:
|
||||
// If no hidden SSIDs to try (selected_sta_index_ == -1), skip directly to rescan
|
||||
if (this->selected_sta_index_ >= 0) {
|
||||
if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) {
|
||||
return WiFiRetryPhase::RETRY_HIDDEN; // Keep retrying same SSID
|
||||
}
|
||||
|
||||
// Exhausted retries on current SSID - check if there are more potentially hidden SSIDs to try
|
||||
if (this->selected_sta_index_ < static_cast<int8_t>(this->sta_.size()) - 1) {
|
||||
// More SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect()
|
||||
return WiFiRetryPhase::RETRY_HIDDEN;
|
||||
}
|
||||
}
|
||||
// Exhausted all potentially hidden SSIDs - rescan to try next BSSID
|
||||
// If captive portal/improv is active, skip adapter restart and go back to start
|
||||
// Otherwise restart adapter to clear any stuck state
|
||||
if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) {
|
||||
// Go back to explicit hidden if we went through it initially, otherwise scan
|
||||
return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN
|
||||
: WiFiRetryPhase::SCAN_CONNECTING;
|
||||
}
|
||||
|
||||
// Restart adapter
|
||||
return WiFiRetryPhase::RESTARTING_ADAPTER;
|
||||
|
||||
case WiFiRetryPhase::RESTARTING_ADAPTER:
|
||||
// After restart, go back to explicit hidden if we went through it initially, otherwise scan
|
||||
return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN
|
||||
: WiFiRetryPhase::SCAN_CONNECTING;
|
||||
}
|
||||
|
||||
delay(10);
|
||||
if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() &&
|
||||
(this->num_retried_ > 3 || this->error_from_callback_)) {
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
// No empty check needed - YAML validation requires at least one network for fast_connect
|
||||
if (this->trying_loaded_ap_) {
|
||||
this->trying_loaded_ap_ = false;
|
||||
this->selected_sta_index_ = 0; // Retry from the first configured AP
|
||||
this->reset_for_next_ap_attempt_();
|
||||
} else if (this->selected_sta_index_ >= static_cast<int8_t>(this->sta_.size()) - 1) {
|
||||
// Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation
|
||||
// Exhausted all configured APs, restart adapter and cycle back to first
|
||||
// Restart clears any stuck WiFi driver state
|
||||
// Each AP is tried with config data only (SSID + optional BSSID/channel if user configured them)
|
||||
// Typically SSID only, which triggers ESP-IDF internal scanning
|
||||
ESP_LOGW(TAG, "No more APs to try");
|
||||
this->selected_sta_index_ = 0;
|
||||
this->reset_for_next_ap_attempt_();
|
||||
this->restart_adapter();
|
||||
} else {
|
||||
// Try next AP
|
||||
this->selected_sta_index_++;
|
||||
this->reset_for_next_ap_attempt_();
|
||||
}
|
||||
#else
|
||||
if (this->num_retried_ > 5) {
|
||||
// If retry failed for more than 5 times, let's restart STA
|
||||
this->restart_adapter();
|
||||
} else {
|
||||
// Try hidden networks after 3 failed retries
|
||||
ESP_LOGD(TAG, "Retrying with hidden networks");
|
||||
this->retry_hidden_ = true;
|
||||
this->num_retried_++;
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
this->num_retried_++;
|
||||
// Should never reach here
|
||||
return WiFiRetryPhase::SCAN_CONNECTING;
|
||||
}
|
||||
|
||||
/// Transition from current retry phase to a new phase with logging and phase-specific setup
|
||||
/// This function handles the actual state change, including:
|
||||
/// - Logging the phase transition
|
||||
/// - Resetting the retry counter
|
||||
/// - Performing phase-specific initialization (e.g., advancing AP index, starting scans)
|
||||
///
|
||||
/// @param new_phase The phase we're transitioning TO
|
||||
/// @return true if an async scan was started (caller should wait for completion)
|
||||
/// false if no scan started (caller can proceed with connection attempt)
|
||||
bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
|
||||
WiFiRetryPhase old_phase = this->retry_phase_;
|
||||
|
||||
// No-op if staying in same phase
|
||||
if (old_phase == new_phase) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Retry phase: %s → %s", LOG_STR_ARG(retry_phase_to_log_string(old_phase)),
|
||||
LOG_STR_ARG(retry_phase_to_log_string(new_phase)));
|
||||
|
||||
this->retry_phase_ = new_phase;
|
||||
this->num_retried_ = 0; // Reset retry counter on phase change
|
||||
|
||||
// Phase-specific setup
|
||||
switch (new_phase) {
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS:
|
||||
// Move to next configured AP - clear old scan data so new AP is tried with config only
|
||||
this->selected_sta_index_++;
|
||||
this->scan_result_.clear();
|
||||
break;
|
||||
#endif
|
||||
|
||||
case WiFiRetryPhase::EXPLICIT_HIDDEN:
|
||||
// Starting explicit hidden phase - reset to first network
|
||||
this->selected_sta_index_ = 0;
|
||||
break;
|
||||
|
||||
case WiFiRetryPhase::SCAN_CONNECTING:
|
||||
// Transitioning to scan-based connection
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
if (old_phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS) {
|
||||
ESP_LOGI(TAG, "Fast connect exhausted, falling back to scan");
|
||||
}
|
||||
#endif
|
||||
// Trigger scan if we don't have scan results OR if transitioning from phases that need fresh scan
|
||||
if (this->scan_result_.empty() || old_phase == WiFiRetryPhase::EXPLICIT_HIDDEN ||
|
||||
old_phase == WiFiRetryPhase::RETRY_HIDDEN || old_phase == WiFiRetryPhase::RESTARTING_ADAPTER) {
|
||||
this->selected_sta_index_ = -1; // Will be set after scan completes
|
||||
this->start_scanning();
|
||||
return true; // Started scan, wait for completion
|
||||
}
|
||||
// Already have scan results - selected_sta_index_ should already be synchronized
|
||||
// (set in check_scanning_finished() when scan completed)
|
||||
// No need to reset it here
|
||||
break;
|
||||
|
||||
case WiFiRetryPhase::RETRY_HIDDEN:
|
||||
// Starting hidden mode - find first SSID that wasn't in scan results
|
||||
if (old_phase == WiFiRetryPhase::SCAN_CONNECTING) {
|
||||
// Keep scan results so we can skip SSIDs that were visible in the scan
|
||||
// Don't clear scan_result_ - we need it to know which SSIDs are NOT hidden
|
||||
|
||||
// If first network is marked hidden, we went through EXPLICIT_HIDDEN phase
|
||||
// In that case, skip networks marked hidden:true (already tried)
|
||||
// Otherwise, include them (they haven't been tried yet)
|
||||
this->selected_sta_index_ = this->find_next_hidden_sta_(-1, !this->went_through_explicit_hidden_phase_());
|
||||
|
||||
if (this->selected_sta_index_ == -1) {
|
||||
ESP_LOGD(TAG, "All SSIDs visible or already tried, skipping hidden mode");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case WiFiRetryPhase::RESTARTING_ADAPTER:
|
||||
this->restart_adapter();
|
||||
// Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting
|
||||
return true;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return false; // Did not start scan, can proceed with connection
|
||||
}
|
||||
|
||||
/// Log failed connection attempt and decrease BSSID priority to avoid repeated failures
|
||||
/// This function identifies which BSSID was attempted (from scan results or config),
|
||||
/// decreases its priority by 1.0 to discourage future attempts, and logs the change.
|
||||
///
|
||||
/// The priority degradation system ensures that failed BSSIDs are automatically sorted
|
||||
/// lower in subsequent scans, naturally cycling through different APs without explicit
|
||||
/// BSSID tracking within a scan cycle.
|
||||
///
|
||||
/// Priority sources:
|
||||
/// - SCAN_CONNECTING phase: Uses BSSID from scan_result_[0] (best match after sorting)
|
||||
/// - Other phases: Uses BSSID from config if explicitly specified by user or fast_connect
|
||||
///
|
||||
/// If no BSSID is available (SSID-only connection), priority adjustment is skipped.
|
||||
void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
|
||||
// Determine which BSSID we tried to connect to
|
||||
optional<bssid_t> failed_bssid;
|
||||
|
||||
if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
|
||||
// Scan-based phase: always use best result (index 0)
|
||||
failed_bssid = this->scan_result_[0].get_bssid();
|
||||
} else if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid()) {
|
||||
// Config has specific BSSID (fast_connect or user-specified)
|
||||
failed_bssid = *config->get_bssid();
|
||||
}
|
||||
|
||||
if (!failed_bssid.has_value()) {
|
||||
return; // No BSSID to penalize
|
||||
}
|
||||
|
||||
// Decrease priority to avoid repeatedly trying the same failed BSSID
|
||||
float old_priority = this->get_sta_priority(failed_bssid.value());
|
||||
float new_priority = old_priority - 1.0f;
|
||||
this->set_sta_priority(failed_bssid.value(), new_priority);
|
||||
|
||||
// Get SSID for logging
|
||||
std::string ssid;
|
||||
if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
|
||||
ssid = this->scan_result_[0].get_ssid();
|
||||
} else if (const WiFiAP *config = this->get_selected_sta_()) {
|
||||
ssid = config->get_ssid();
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %.1f → %.1f", ssid.c_str(),
|
||||
format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority);
|
||||
}
|
||||
|
||||
/// Handle target advancement or retry counter increment when staying in the same phase
|
||||
/// This function is called when a connection attempt fails and determine_next_phase_() indicates
|
||||
/// we should stay in the current phase. It decides whether to:
|
||||
/// - Advance to the next target (AP in fast_connect, SSID in hidden mode)
|
||||
/// - Or increment the retry counter to try the same target again
|
||||
///
|
||||
/// Phase-specific behavior:
|
||||
/// - FAST_CONNECT_CYCLING_APS: Always advance to next AP (no retries per AP)
|
||||
/// - RETRY_HIDDEN: Advance to next SSID after exhausting retries on current SSID
|
||||
/// - Other phases: Increment retry counter (will retry same target)
|
||||
void WiFiComponent::advance_to_next_target_or_increment_retry_() {
|
||||
WiFiRetryPhase current_phase = this->retry_phase_;
|
||||
|
||||
// Check if we need to advance to next AP/SSID within the same phase
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
if (current_phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS) {
|
||||
// Fast connect: always advance to next AP (no retries per AP)
|
||||
this->selected_sta_index_++;
|
||||
this->num_retried_ = 0;
|
||||
ESP_LOGD(TAG, "Next AP in %s", LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (current_phase == WiFiRetryPhase::EXPLICIT_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) {
|
||||
// Explicit hidden: exhausted retries on current SSID, find next explicitly hidden network
|
||||
// Stop when we reach a visible network (proceed to scanning)
|
||||
size_t next_index = this->selected_sta_index_ + 1;
|
||||
if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) {
|
||||
this->selected_sta_index_ = static_cast<int8_t>(next_index);
|
||||
this->num_retried_ = 0;
|
||||
ESP_LOGD(TAG, "Next explicit hidden network at index %d", static_cast<int>(next_index));
|
||||
return;
|
||||
}
|
||||
// No more consecutive explicit hidden networks found - fall through to trigger phase change
|
||||
}
|
||||
|
||||
if (current_phase == WiFiRetryPhase::RETRY_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) {
|
||||
// Hidden mode: exhausted retries on current SSID, find next potentially hidden SSID
|
||||
// If first network is marked hidden, we went through EXPLICIT_HIDDEN phase
|
||||
// In that case, skip networks marked hidden:true (already tried)
|
||||
// Otherwise, include them (they haven't been tried yet)
|
||||
int8_t next_index =
|
||||
this->find_next_hidden_sta_(this->selected_sta_index_, !this->went_through_explicit_hidden_phase_());
|
||||
if (next_index != -1) {
|
||||
// Found another potentially hidden SSID
|
||||
this->selected_sta_index_ = next_index;
|
||||
this->num_retried_ = 0;
|
||||
return;
|
||||
}
|
||||
// No more potentially hidden SSIDs - set selected_sta_index_ to -1 to trigger phase change
|
||||
// This ensures determine_next_phase_() will skip the RETRY_HIDDEN logic and transition out
|
||||
this->selected_sta_index_ = -1;
|
||||
// Return early - phase change will happen on next wifi_loop() iteration
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't increment retry counter if we're in a scan phase with no valid targets
|
||||
if (this->needs_scan_results_()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Increment retry counter to try the same target again
|
||||
this->num_retried_++;
|
||||
ESP_LOGD(TAG, "Retry attempt %u/%u in phase %s", this->num_retried_ + 1,
|
||||
get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
|
||||
}
|
||||
|
||||
void WiFiComponent::retry_connect() {
|
||||
this->log_and_adjust_priority_for_failed_connect_();
|
||||
|
||||
delay(10);
|
||||
|
||||
// Determine next retry phase based on current state
|
||||
WiFiRetryPhase current_phase = this->retry_phase_;
|
||||
WiFiRetryPhase next_phase = this->determine_next_phase_();
|
||||
|
||||
// Handle phase transitions (transition_to_phase_ handles same-phase no-op internally)
|
||||
if (this->transition_to_phase_(next_phase)) {
|
||||
return; // Wait for scan to complete
|
||||
}
|
||||
|
||||
if (next_phase == current_phase) {
|
||||
this->advance_to_next_target_or_increment_retry_();
|
||||
}
|
||||
|
||||
this->error_from_callback_ = false;
|
||||
|
||||
if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) {
|
||||
yield();
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2;
|
||||
WiFiAP params = this->build_wifi_ap_from_selected_();
|
||||
this->start_connecting(params, true);
|
||||
return;
|
||||
// Check if we have a valid target before building params
|
||||
// After exhausting all networks in a phase, selected_sta_index_ may be -1
|
||||
// In that case, skip connection and let next wifi_loop() handle phase transition
|
||||
if (this->selected_sta_index_ >= 0) {
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2;
|
||||
WiFiAP params = this->build_params_for_current_phase_();
|
||||
this->start_connecting(params, true);
|
||||
return;
|
||||
}
|
||||
// No valid target - fall through to set state to allow phase transition
|
||||
}
|
||||
|
||||
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
|
||||
|
||||
@@ -94,6 +94,24 @@ enum class WiFiSTAConnectStatus : int {
|
||||
ERROR_CONNECT_FAILED,
|
||||
};
|
||||
|
||||
/// Tracks the current retry strategy/phase for WiFi connection attempts
|
||||
enum class WiFiRetryPhase : uint8_t {
|
||||
/// Initial connection attempt (varies based on fast_connect setting)
|
||||
INITIAL_CONNECT,
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
/// Fast connect mode: cycling through configured APs (config-only, no scan)
|
||||
FAST_CONNECT_CYCLING_APS,
|
||||
#endif
|
||||
/// Explicitly hidden networks (user marked as hidden, try before scanning)
|
||||
EXPLICIT_HIDDEN,
|
||||
/// Scan-based: connecting to best AP from scan results
|
||||
SCAN_CONNECTING,
|
||||
/// Retry networks not found in scan (might be hidden)
|
||||
RETRY_HIDDEN,
|
||||
/// Restarting WiFi adapter to clear stuck state
|
||||
RESTARTING_ADAPTER,
|
||||
};
|
||||
|
||||
/// Struct for setting static IPs in WiFiComponent.
|
||||
struct ManualIP {
|
||||
network::IPAddress static_ip;
|
||||
@@ -207,6 +225,12 @@ enum WiFiPowerSaveMode : uint8_t {
|
||||
WIFI_POWER_SAVE_HIGH,
|
||||
};
|
||||
|
||||
enum WifiMinAuthMode : uint8_t {
|
||||
WIFI_MIN_AUTH_MODE_WPA = 0,
|
||||
WIFI_MIN_AUTH_MODE_WPA2,
|
||||
WIFI_MIN_AUTH_MODE_WPA3,
|
||||
};
|
||||
|
||||
#ifdef USE_ESP32
|
||||
struct IDFWiFiEvent;
|
||||
#endif
|
||||
@@ -258,6 +282,7 @@ class WiFiComponent : public Component {
|
||||
bool is_connected();
|
||||
|
||||
void set_power_save_mode(WiFiPowerSaveMode power_save);
|
||||
void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; }
|
||||
void set_output_power(float output_power) { output_power_ = output_power; }
|
||||
|
||||
void set_passive_scan(bool passive);
|
||||
@@ -341,8 +366,37 @@ class WiFiComponent : public Component {
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
void print_connect_params_();
|
||||
WiFiAP build_wifi_ap_from_selected_() const;
|
||||
WiFiAP build_params_for_current_phase_();
|
||||
|
||||
/// Determine next retry phase based on current state and failure conditions
|
||||
WiFiRetryPhase determine_next_phase_();
|
||||
/// Transition to a new retry phase with logging
|
||||
/// Returns true if a scan was started (caller should wait), false otherwise
|
||||
bool transition_to_phase_(WiFiRetryPhase new_phase);
|
||||
/// Check if we need valid scan results for the current phase but don't have any
|
||||
/// Returns true if the phase requires scan results but they're missing or don't match
|
||||
bool needs_scan_results_() const;
|
||||
/// Check if we went through EXPLICIT_HIDDEN phase (first network is marked hidden)
|
||||
/// Used in RETRY_HIDDEN to determine whether to skip explicitly hidden networks
|
||||
bool went_through_explicit_hidden_phase_() const;
|
||||
/// Find the index of the first non-hidden network
|
||||
/// Returns where EXPLICIT_HIDDEN phase would have stopped, or -1 if all networks are hidden
|
||||
int8_t find_first_non_hidden_index_() const;
|
||||
/// Check if an SSID was seen in the most recent scan results
|
||||
/// Used to skip hidden mode for SSIDs we know are visible
|
||||
bool ssid_was_seen_in_scan_(const std::string &ssid) const;
|
||||
/// Find next SSID that wasn't in scan results (might be hidden)
|
||||
/// Returns index of next potentially hidden SSID, or -1 if none found
|
||||
/// @param start_index Start searching from index after this (-1 to start from beginning)
|
||||
/// @param include_explicit_hidden If true, include SSIDs marked hidden:true. If false, only find truly hidden SSIDs.
|
||||
int8_t find_next_hidden_sta_(int8_t start_index, bool include_explicit_hidden = true);
|
||||
/// Log failed connection and decrease BSSID priority to avoid repeated attempts
|
||||
void log_and_adjust_priority_for_failed_connect_();
|
||||
/// Advance to next target (AP/SSID) within current phase, or increment retry counter
|
||||
/// Called when staying in the same phase after a failed connection attempt
|
||||
void advance_to_next_target_or_increment_retry_();
|
||||
/// Start initial connection - either scan or connect directly to hidden networks
|
||||
void start_initial_connection_();
|
||||
const WiFiAP *get_selected_sta_() const {
|
||||
if (this->selected_sta_index_ >= 0 && static_cast<size_t>(this->selected_sta_index_) < this->sta_.size()) {
|
||||
return &this->sta_[this->selected_sta_index_];
|
||||
@@ -356,14 +410,15 @@ class WiFiComponent : public Component {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
// Reset state for next fast connect AP attempt
|
||||
// Clears old scan data so the new AP is tried with config only (SSID without specific BSSID/channel)
|
||||
void reset_for_next_ap_attempt_() {
|
||||
this->num_retried_ = 0;
|
||||
this->scan_result_.clear();
|
||||
bool all_networks_hidden_() const {
|
||||
if (this->sta_.empty())
|
||||
return false;
|
||||
for (const auto &ap : this->sta_) {
|
||||
if (!ap.get_hidden())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void wifi_loop_();
|
||||
bool wifi_mode_(optional<bool> sta, optional<bool> ap);
|
||||
@@ -443,20 +498,19 @@ class WiFiComponent : public Component {
|
||||
// Group all 8-bit values together
|
||||
WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF};
|
||||
WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE};
|
||||
WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2};
|
||||
WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT};
|
||||
uint8_t num_retried_{0};
|
||||
// Index into sta_ array for the currently selected AP configuration (-1 = none selected)
|
||||
// Used to access password, manual_ip, priority, EAP settings, and hidden flag
|
||||
// int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS)
|
||||
int8_t selected_sta_index_{-1};
|
||||
|
||||
#if USE_NETWORK_IPV6
|
||||
uint8_t num_ipv6_addresses_{0};
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
// Group all boolean values together
|
||||
#ifdef USE_WIFI_FAST_CONNECT
|
||||
bool trying_loaded_ap_{false};
|
||||
#endif
|
||||
bool retry_hidden_{false};
|
||||
bool has_ap_{false};
|
||||
bool handled_connected_state_{false};
|
||||
bool error_from_callback_{false};
|
||||
|
||||
@@ -258,8 +258,17 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
if (ap.get_password().empty()) {
|
||||
conf.threshold.authmode = AUTH_OPEN;
|
||||
} else {
|
||||
// Only allow auth modes with at least WPA
|
||||
conf.threshold.authmode = AUTH_WPA_PSK;
|
||||
// Set threshold based on configured minimum auth mode
|
||||
// Note: ESP8266 doesn't support WPA3
|
||||
switch (this->min_auth_mode_) {
|
||||
case WIFI_MIN_AUTH_MODE_WPA:
|
||||
conf.threshold.authmode = AUTH_WPA_PSK;
|
||||
break;
|
||||
case WIFI_MIN_AUTH_MODE_WPA2:
|
||||
case WIFI_MIN_AUTH_MODE_WPA3: // Fall back to WPA2 for ESP8266
|
||||
conf.threshold.authmode = AUTH_WPA2_PSK;
|
||||
break;
|
||||
}
|
||||
}
|
||||
conf.threshold.rssi = -127;
|
||||
#endif
|
||||
|
||||
@@ -308,7 +308,18 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
if (ap.get_password().empty()) {
|
||||
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
|
||||
} else {
|
||||
conf.sta.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK;
|
||||
// Set threshold based on configured minimum auth mode
|
||||
switch (this->min_auth_mode_) {
|
||||
case WIFI_MIN_AUTH_MODE_WPA:
|
||||
conf.sta.threshold.authmode = WIFI_AUTH_WPA_PSK;
|
||||
break;
|
||||
case WIFI_MIN_AUTH_MODE_WPA2:
|
||||
conf.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
|
||||
break;
|
||||
case WIFI_MIN_AUTH_MODE_WPA3:
|
||||
conf.sta.threshold.authmode = WIFI_AUTH_WPA3_PSK;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI_WPA2_EAP
|
||||
@@ -347,8 +358,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
// The minimum rssi to accept in the fast scan mode
|
||||
conf.sta.threshold.rssi = -127;
|
||||
|
||||
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
|
||||
|
||||
wifi_config_t current_conf;
|
||||
esp_err_t err;
|
||||
err = esp_wifi_get_config(WIFI_IF_STA, ¤t_conf);
|
||||
|
||||
@@ -86,6 +86,7 @@ ISOLATED_COMPONENTS = {
|
||||
"modbus_controller": "Defines multiple modbus buses for testing client/server functionality - conflicts with package modbus bus",
|
||||
"neopixelbus": "RMT type conflict with ESP32 Arduino/ESP-IDF headers (enum vs struct rmt_channel_t)",
|
||||
"packages": "cannot merge packages",
|
||||
"tinyusb": "Conflicts with usb_host component - cannot be used together",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,5 +16,6 @@ display:
|
||||
|
||||
touchscreen:
|
||||
- platform: chsc6x
|
||||
i2c_id: i2c_bus
|
||||
display: ili9xxx_display
|
||||
interrupt_pin: 22
|
||||
|
||||
41
tests/components/hlk_fm22x/common.yaml
Normal file
41
tests/components/hlk_fm22x/common.yaml
Normal file
@@ -0,0 +1,41 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- hlk_fm22x.enroll:
|
||||
name: "Test"
|
||||
direction: 1
|
||||
- hlk_fm22x.delete_all:
|
||||
|
||||
hlk_fm22x:
|
||||
on_face_scan_matched:
|
||||
- logger.log: test_hlk_22x_face_scan_matched
|
||||
on_face_scan_unmatched:
|
||||
- logger.log: test_hlk_22x_face_scan_unmatched
|
||||
on_face_scan_invalid:
|
||||
- logger.log: test_hlk_22x_face_scan_invalid
|
||||
on_face_info:
|
||||
- logger.log: test_hlk_22x_face_info
|
||||
on_enrollment_done:
|
||||
- logger.log: test_hlk_22x_enrollment_done
|
||||
on_enrollment_failed:
|
||||
- logger.log: test_hlk_22x_enrollment_failed
|
||||
|
||||
sensor:
|
||||
- platform: hlk_fm22x
|
||||
face_count:
|
||||
name: "Face Count"
|
||||
last_face_id:
|
||||
name: "Last Face ID"
|
||||
status:
|
||||
name: "Face Status"
|
||||
|
||||
binary_sensor:
|
||||
- platform: hlk_fm22x
|
||||
name: "Face Enrolling"
|
||||
|
||||
text_sensor:
|
||||
- platform: hlk_fm22x
|
||||
version:
|
||||
name: "HLK Version"
|
||||
last_face_name:
|
||||
name: "Last Face Name"
|
||||
@@ -1,47 +1,4 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- hlk_fm22x.enroll:
|
||||
name: "Test"
|
||||
direction: 1
|
||||
- hlk_fm22x.delete_all:
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
|
||||
|
||||
uart:
|
||||
- id: uart_hlk_fm22x
|
||||
tx_pin: 17
|
||||
rx_pin: 16
|
||||
baud_rate: 115200
|
||||
|
||||
hlk_fm22x:
|
||||
on_face_scan_matched:
|
||||
- logger.log: test_hlk_22x_face_scan_matched
|
||||
on_face_scan_unmatched:
|
||||
- logger.log: test_hlk_22x_face_scan_unmatched
|
||||
on_face_scan_invalid:
|
||||
- logger.log: test_hlk_22x_face_scan_invalid
|
||||
on_face_info:
|
||||
- logger.log: test_hlk_22x_face_info
|
||||
on_enrollment_done:
|
||||
- logger.log: test_hlk_22x_enrollment_done
|
||||
on_enrollment_failed:
|
||||
- logger.log: test_hlk_22x_enrollment_failed
|
||||
|
||||
sensor:
|
||||
- platform: hlk_fm22x
|
||||
face_count:
|
||||
name: "Face Count"
|
||||
last_face_id:
|
||||
name: "Last Face ID"
|
||||
status:
|
||||
name: "Face Status"
|
||||
|
||||
binary_sensor:
|
||||
- platform: hlk_fm22x
|
||||
name: "Face Enrolling"
|
||||
|
||||
text_sensor:
|
||||
- platform: hlk_fm22x
|
||||
version:
|
||||
name: "HLK Version"
|
||||
last_face_name:
|
||||
name: "Last Face Name"
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,47 +1,4 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- hlk_fm22x.enroll:
|
||||
name: "Test"
|
||||
direction: 1
|
||||
- hlk_fm22x.delete_all:
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
|
||||
|
||||
uart:
|
||||
- id: uart_hlk_fm22x
|
||||
tx_pin: 4
|
||||
rx_pin: 5
|
||||
baud_rate: 115200
|
||||
|
||||
hlk_fm22x:
|
||||
on_face_scan_matched:
|
||||
- logger.log: test_hlk_22x_face_scan_matched
|
||||
on_face_scan_unmatched:
|
||||
- logger.log: test_hlk_22x_face_scan_unmatched
|
||||
on_face_scan_invalid:
|
||||
- logger.log: test_hlk_22x_face_scan_invalid
|
||||
on_face_info:
|
||||
- logger.log: test_hlk_22x_face_info
|
||||
on_enrollment_done:
|
||||
- logger.log: test_hlk_22x_enrollment_done
|
||||
on_enrollment_failed:
|
||||
- logger.log: test_hlk_22x_enrollment_failed
|
||||
|
||||
sensor:
|
||||
- platform: hlk_fm22x
|
||||
face_count:
|
||||
name: "Face Count"
|
||||
last_face_id:
|
||||
name: "Last Face ID"
|
||||
status:
|
||||
name: "Face Status"
|
||||
|
||||
binary_sensor:
|
||||
- platform: hlk_fm22x
|
||||
name: "Face Enrolling"
|
||||
|
||||
text_sensor:
|
||||
- platform: hlk_fm22x
|
||||
version:
|
||||
name: "HLK Version"
|
||||
last_face_name:
|
||||
name: "Last Face Name"
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,47 +1,4 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- hlk_fm22x.enroll:
|
||||
name: "Test"
|
||||
direction: 1
|
||||
- hlk_fm22x.delete_all:
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
|
||||
|
||||
uart:
|
||||
- id: uart_hlk_fm22x
|
||||
tx_pin: 4
|
||||
rx_pin: 5
|
||||
baud_rate: 115200
|
||||
|
||||
hlk_fm22x:
|
||||
on_face_scan_matched:
|
||||
- logger.log: test_hlk_22x_face_scan_matched
|
||||
on_face_scan_unmatched:
|
||||
- logger.log: test_hlk_22x_face_scan_unmatched
|
||||
on_face_scan_invalid:
|
||||
- logger.log: test_hlk_22x_face_scan_invalid
|
||||
on_face_info:
|
||||
- logger.log: test_hlk_22x_face_info
|
||||
on_enrollment_done:
|
||||
- logger.log: test_hlk_22x_enrollment_done
|
||||
on_enrollment_failed:
|
||||
- logger.log: test_hlk_22x_enrollment_failed
|
||||
|
||||
sensor:
|
||||
- platform: hlk_fm22x
|
||||
face_count:
|
||||
name: "Face Count"
|
||||
last_face_id:
|
||||
name: "Last Face ID"
|
||||
status:
|
||||
name: "Face Status"
|
||||
|
||||
binary_sensor:
|
||||
- platform: hlk_fm22x
|
||||
name: "Face Enrolling"
|
||||
|
||||
text_sensor:
|
||||
- platform: hlk_fm22x
|
||||
version:
|
||||
name: "HLK Version"
|
||||
last_face_name:
|
||||
name: "Last Face Name"
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -700,6 +700,10 @@ lvgl:
|
||||
width: 100%
|
||||
height: 10%
|
||||
align: top_mid
|
||||
on_value:
|
||||
- lvgl.spinbox.update:
|
||||
id: spinbox_id
|
||||
value: !lambda return x;
|
||||
- button:
|
||||
styles: spin_button
|
||||
id: spin_up
|
||||
|
||||
8
tests/components/rx8130/common.yaml
Normal file
8
tests/components/rx8130/common.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
- rx8130.read_time
|
||||
- rx8130.write_time
|
||||
|
||||
time:
|
||||
- platform: rx8130
|
||||
i2c_id: i2c_bus
|
||||
4
tests/components/rx8130/test.esp32-idf.yaml
Normal file
4
tests/components/rx8130/test.esp32-idf.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
4
tests/components/rx8130/test.esp8266-ard.yaml
Normal file
4
tests/components/rx8130/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
4
tests/components/rx8130/test.rp2040-ard.yaml
Normal file
4
tests/components/rx8130/test.rp2040-ard.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -11,26 +11,42 @@ esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- speaker.mute_on:
|
||||
id: speaker_id
|
||||
- speaker.mute_off:
|
||||
id: speaker_id
|
||||
- if:
|
||||
condition: speaker.is_stopped
|
||||
condition:
|
||||
speaker.is_stopped:
|
||||
id: speaker_id
|
||||
then:
|
||||
- speaker.play: [0, 1, 2, 3]
|
||||
- speaker.volume_set: 0.9
|
||||
- speaker.play:
|
||||
id: speaker_id
|
||||
data: [0, 1, 2, 3]
|
||||
- speaker.volume_set:
|
||||
id: speaker_id
|
||||
volume: 0.9
|
||||
- if:
|
||||
condition: speaker.is_playing
|
||||
condition:
|
||||
speaker.is_playing:
|
||||
id: speaker_id
|
||||
then:
|
||||
- speaker.finish:
|
||||
id: speaker_id
|
||||
- speaker.stop:
|
||||
id: speaker_id
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Speaker Button"
|
||||
on_press:
|
||||
then:
|
||||
- speaker.play: [0x10, 0x20, 0x30, 0x40]
|
||||
- speaker.play: !lambda |-
|
||||
return {0x01, 0x02, (uint8_t)id(my_number).state};
|
||||
- speaker.play:
|
||||
id: speaker_id
|
||||
data: [0x10, 0x20, 0x30, 0x40]
|
||||
- speaker.play:
|
||||
id: speaker_id
|
||||
data: !lambda |-
|
||||
return {0x01, 0x02, (uint8_t)id(my_number).state};
|
||||
|
||||
i2s_audio:
|
||||
i2s_lrclk_pin: ${i2s_bclk_pin}
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
remote_transmitter:
|
||||
pin: ${tx_pin}
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
remote_receiver:
|
||||
id: rcvr
|
||||
pin: ${rx_pin}
|
||||
|
||||
climate:
|
||||
- platform: toshiba
|
||||
name: "RAS-2819T Climate"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO5
|
||||
rx_pin: GPIO4
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-ard.yaml
|
||||
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-ard.yaml
|
||||
|
||||
<<: !include common_ras2819t.yaml
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO5
|
||||
rx_pin: GPIO4
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-ard.yaml
|
||||
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-c3-ard.yaml
|
||||
|
||||
<<: !include common_ras2819t.yaml
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO5
|
||||
rx_pin: GPIO4
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-idf.yaml
|
||||
|
||||
<<: !include common_ras2819t.yaml
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO5
|
||||
rx_pin: GPIO4
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp8266-ard.yaml
|
||||
|
||||
<<: !include common_ras2819t.yaml
|
||||
|
||||
@@ -2,6 +2,7 @@ psram:
|
||||
|
||||
wifi:
|
||||
use_psram: true
|
||||
min_auth_mode: WPA
|
||||
|
||||
packages:
|
||||
- !include common.yaml
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
<<: !include common.yaml
|
||||
wifi:
|
||||
min_auth_mode: WPA2
|
||||
|
||||
packages:
|
||||
- !include common.yaml
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Common remote_receiver configuration for ESP32 Arduino tests
|
||||
# Provides a shared remote receiver that all components can use
|
||||
# Components will auto-use this receiver if they don't specify receiver_id
|
||||
|
||||
substitutions:
|
||||
remote_receiver_pin: GPIO32
|
||||
|
||||
remote_receiver:
|
||||
- id: rcvr
|
||||
pin: ${remote_receiver_pin}
|
||||
dump: all
|
||||
tolerance: 25%
|
||||
@@ -0,0 +1,12 @@
|
||||
# Common remote_receiver configuration for ESP32-C3 Arduino tests
|
||||
# Provides a shared remote receiver that all components can use
|
||||
# Components will auto-use this receiver if they don't specify receiver_id
|
||||
|
||||
substitutions:
|
||||
remote_receiver_pin: GPIO10
|
||||
|
||||
remote_receiver:
|
||||
- id: rcvr
|
||||
pin: ${remote_receiver_pin}
|
||||
dump: all
|
||||
tolerance: 25%
|
||||
Reference in New Issue
Block a user