mirror of
https://github.com/esphome/esphome.git
synced 2025-11-11 20:35:51 +00:00
Compare commits
31 Commits
dev
...
no_batch_e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99bb6ead7e | ||
|
|
d171406dff | ||
|
|
65fd784fa7 | ||
|
|
b9f208b63a | ||
|
|
fac05dab35 | ||
|
|
f6100a55bc | ||
|
|
c2abf363b6 | ||
|
|
62f43d3353 | ||
|
|
7e96f10a79 | ||
|
|
6b9cb4289a | ||
|
|
b264c6caac | ||
|
|
a6c669ff51 | ||
|
|
c15290e386 | ||
|
|
e3fb074a60 | ||
|
|
6e7f66d393 | ||
|
|
ac85949f17 | ||
|
|
0962024d99 | ||
|
|
327543303c | ||
|
|
8229e3a471 | ||
|
|
1b6471f4b0 | ||
|
|
c87d07ba70 | ||
|
|
fc8dc33023 | ||
|
|
c0e4f415f1 | ||
|
|
871c5ddb4e | ||
|
|
6ef2763cab | ||
|
|
929279dc23 | ||
|
|
6fa0f1e290 | ||
|
|
51eb8ea1d0 | ||
|
|
cbdd663fbf | ||
|
|
f1009a7468 | ||
|
|
295fe8da04 |
@@ -172,7 +172,8 @@ This document provides essential context for AI models interacting with this pro
|
||||
|
||||
* **C++ Class Pattern:**
|
||||
```cpp
|
||||
namespace esphome::my_component {
|
||||
namespace esphome {
|
||||
namespace my_component {
|
||||
|
||||
class MyComponent : public Component {
|
||||
public:
|
||||
@@ -188,7 +189,8 @@ This document provides essential context for AI models interacting with this pro
|
||||
int param_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::my_component
|
||||
} // namespace my_component
|
||||
} // namespace esphome
|
||||
```
|
||||
|
||||
* **Common Component Examples:**
|
||||
|
||||
@@ -21,7 +21,7 @@ permissions:
|
||||
jobs:
|
||||
request-codeowner-reviews:
|
||||
name: Run
|
||||
if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }}
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Request reviews from component codeowners
|
||||
|
||||
@@ -206,7 +206,6 @@ esphome/components/hdc2010/* @optimusprimespace @ssieb
|
||||
esphome/components/he60r/* @clydebarrow
|
||||
esphome/components/heatpumpir/* @rob-deutsch
|
||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
||||
esphome/components/hlk_fm22x/* @OnFreund
|
||||
esphome/components/hm3301/* @freekode
|
||||
esphome/components/hmac_md5/* @dwmw2
|
||||
esphome/components/homeassistant/* @esphome/core @OttoWinter
|
||||
@@ -396,7 +395,6 @@ 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
|
||||
|
||||
@@ -1294,11 +1294,11 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void APIConnection::send_event(event::Event *event, const std::string &event_type) {
|
||||
this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
|
||||
EventResponse::ESTIMATED_SIZE);
|
||||
void APIConnection::send_event(event::Event *event, const char *event_type) {
|
||||
this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
|
||||
EventResponse::ESTIMATED_SIZE);
|
||||
}
|
||||
uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
|
||||
uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
|
||||
uint32_t remaining_size, bool is_single) {
|
||||
EventResponse resp;
|
||||
resp.set_event_type(StringRef(event_type));
|
||||
@@ -1650,9 +1650,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c
|
||||
// O(n) but optimized for RAM and not performance.
|
||||
for (auto &item : items) {
|
||||
if (item.entity == entity && item.message_type == message_type) {
|
||||
// Clean up old creator before replacing
|
||||
item.creator.cleanup(message_type);
|
||||
// Move assign the new creator
|
||||
// Replace with new creator
|
||||
item.creator = std::move(creator);
|
||||
return;
|
||||
}
|
||||
@@ -1822,7 +1820,7 @@ void APIConnection::process_batch_() {
|
||||
|
||||
// Handle remaining items more efficiently
|
||||
if (items_processed < this->deferred_batch_.size()) {
|
||||
// Remove processed items from the beginning with proper cleanup
|
||||
// Remove processed items from the beginning
|
||||
this->deferred_batch_.remove_front(items_processed);
|
||||
// Reschedule for remaining items
|
||||
this->schedule_batch_();
|
||||
@@ -1835,10 +1833,10 @@ void APIConnection::process_batch_() {
|
||||
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single, uint8_t message_type) const {
|
||||
#ifdef USE_EVENT
|
||||
// Special case: EventResponse uses string pointer
|
||||
// Special case: EventResponse uses const char * pointer
|
||||
if (message_type == EventResponse::MESSAGE_TYPE) {
|
||||
auto *e = static_cast<event::Event *>(entity);
|
||||
return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single);
|
||||
return APIConnection::try_send_event_response(e, data_.const_char_ptr, conn, remaining_size, is_single);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ class APIConnection final : public APIServerConnection {
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void send_event(event::Event *event, const std::string &event_type);
|
||||
void send_event(event::Event *event, const char *event_type);
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
@@ -450,7 +450,7 @@ class APIConnection final : public APIServerConnection {
|
||||
bool is_single);
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
static uint16_t try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
|
||||
static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
|
||||
uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
#endif
|
||||
@@ -508,10 +508,8 @@ class APIConnection final : public APIServerConnection {
|
||||
// Constructor for function pointer
|
||||
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
|
||||
|
||||
// Constructor for string state capture
|
||||
explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); }
|
||||
|
||||
// No destructor - cleanup must be called explicitly with message_type
|
||||
// Constructor for const char * (Event types - no allocation needed)
|
||||
explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; }
|
||||
|
||||
// Delete copy operations - MessageCreator should only be moved
|
||||
MessageCreator(const MessageCreator &other) = delete;
|
||||
@@ -523,8 +521,6 @@ class APIConnection final : public APIServerConnection {
|
||||
// Move assignment
|
||||
MessageCreator &operator=(MessageCreator &&other) noexcept {
|
||||
if (this != &other) {
|
||||
// IMPORTANT: Caller must ensure cleanup() was called if this contains a string!
|
||||
// In our usage, this happens in add_item() deduplication and vector::erase()
|
||||
data_ = other.data_;
|
||||
other.data_.function_ptr = nullptr;
|
||||
}
|
||||
@@ -535,20 +531,10 @@ class APIConnection final : public APIServerConnection {
|
||||
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
|
||||
uint8_t message_type) const;
|
||||
|
||||
// Manual cleanup method - must be called before destruction for string types
|
||||
void cleanup(uint8_t message_type) {
|
||||
#ifdef USE_EVENT
|
||||
if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) {
|
||||
delete data_.string_ptr;
|
||||
data_.string_ptr = nullptr;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private:
|
||||
union Data {
|
||||
MessageCreatorPtr function_ptr;
|
||||
std::string *string_ptr;
|
||||
const char *const_char_ptr;
|
||||
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
|
||||
};
|
||||
|
||||
@@ -568,42 +554,24 @@ class APIConnection final : public APIServerConnection {
|
||||
std::vector<BatchItem> items;
|
||||
uint32_t batch_start_time{0};
|
||||
|
||||
private:
|
||||
// Helper to cleanup items from the beginning
|
||||
void cleanup_items_(size_t count) {
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
items[i].creator.cleanup(items[i].message_type);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
DeferredBatch() {
|
||||
// Pre-allocate capacity for typical batch sizes to avoid reallocation
|
||||
items.reserve(8);
|
||||
}
|
||||
|
||||
~DeferredBatch() {
|
||||
// Ensure cleanup of any remaining items
|
||||
clear();
|
||||
}
|
||||
|
||||
// Add item to the batch
|
||||
void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
|
||||
// Add item to the front of the batch (for high priority messages like ping)
|
||||
void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
|
||||
|
||||
// Clear all items with proper cleanup
|
||||
// Clear all items
|
||||
void clear() {
|
||||
cleanup_items_(items.size());
|
||||
items.clear();
|
||||
batch_start_time = 0;
|
||||
}
|
||||
|
||||
// Remove processed items from the front with proper cleanup
|
||||
void remove_front(size_t count) {
|
||||
cleanup_items_(count);
|
||||
items.erase(items.begin(), items.begin() + count);
|
||||
}
|
||||
// Remove processed items from the front
|
||||
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); }
|
||||
|
||||
bool empty() const { return items.empty(); }
|
||||
size_t size() const { return items.size(); }
|
||||
@@ -682,21 +650,30 @@ class APIConnection final : public APIServerConnection {
|
||||
}
|
||||
#endif
|
||||
|
||||
// Helper to check if a message type should bypass batching
|
||||
// Returns true if:
|
||||
// 1. It's an UpdateStateResponse (always send immediately to handle cases where
|
||||
// the main loop is blocked, e.g., during OTA updates)
|
||||
// 2. It's an EventResponse (events are edge-triggered - every occurrence matters)
|
||||
// 3. OR: User has opted into immediate sending (should_try_send_immediately = true
|
||||
// AND batch_delay = 0)
|
||||
inline bool should_send_immediately_(uint8_t message_type) const {
|
||||
return (
|
||||
#ifdef USE_UPDATE
|
||||
message_type == UpdateStateResponse::MESSAGE_TYPE ||
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
message_type == EventResponse::MESSAGE_TYPE ||
|
||||
#endif
|
||||
(this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0));
|
||||
}
|
||||
|
||||
// Helper method to send a message either immediately or via batching
|
||||
// Tries immediate send if should_send_immediately_() returns true and buffer has space
|
||||
// Falls back to batching if immediate send fails or isn't applicable
|
||||
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
|
||||
uint8_t estimated_size) {
|
||||
// Try to send immediately if:
|
||||
// 1. It's an UpdateStateResponse (always send immediately to handle cases where
|
||||
// the main loop is blocked, e.g., during OTA updates)
|
||||
// 2. OR: We should try to send immediately (should_try_send_immediately = true)
|
||||
// AND Batch delay is 0 (user has opted in to immediate sending)
|
||||
// 3. AND: Buffer has space available
|
||||
if ((
|
||||
#ifdef USE_UPDATE
|
||||
message_type == UpdateStateResponse::MESSAGE_TYPE ||
|
||||
#endif
|
||||
(this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) &&
|
||||
this->helper_->can_write_without_blocking()) {
|
||||
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
|
||||
// Now actually encode and send
|
||||
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
|
||||
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
|
||||
@@ -714,6 +691,27 @@ class APIConnection final : public APIServerConnection {
|
||||
return this->schedule_message_(entity, creator, message_type, estimated_size);
|
||||
}
|
||||
|
||||
// Overload for MessageCreator (used by events which need to capture event_type)
|
||||
bool send_message_smart_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
|
||||
// Try to send immediately if message type should bypass batching and buffer has space
|
||||
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
|
||||
// Now actually encode and send
|
||||
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type) &&
|
||||
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
// Log the message in verbose mode
|
||||
this->log_proto_message_(entity, creator, message_type);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
// If immediate send failed, fall through to batching
|
||||
}
|
||||
|
||||
// Fall back to scheduled batching
|
||||
return this->schedule_message_(entity, std::move(creator), message_type, estimated_size);
|
||||
}
|
||||
|
||||
// Helper function to schedule a deferred message with known message type
|
||||
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
|
||||
this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);
|
||||
|
||||
@@ -23,7 +23,7 @@ void DS1307Component::dump_config() {
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
RealTimeClock::dump_config();
|
||||
ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str());
|
||||
}
|
||||
|
||||
float DS1307Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
@@ -418,6 +418,8 @@ void EthernetComponent::dump_config() {
|
||||
|
||||
float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; }
|
||||
|
||||
bool EthernetComponent::can_proceed() { return this->is_connected(); }
|
||||
|
||||
network::IPAddresses EthernetComponent::get_ip_addresses() {
|
||||
network::IPAddresses addresses;
|
||||
esp_netif_ip_info_t ip;
|
||||
|
||||
@@ -58,6 +58,7 @@ class EthernetComponent : public Component {
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
bool can_proceed() override;
|
||||
void on_powerdown() override { powerdown(); }
|
||||
bool is_connected();
|
||||
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DIRECTION,
|
||||
CONF_ID,
|
||||
CONF_NAME,
|
||||
CONF_ON_ENROLLMENT_DONE,
|
||||
CONF_ON_ENROLLMENT_FAILED,
|
||||
CONF_TRIGGER_ID,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@OnFreund"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
AUTO_LOAD = ["binary_sensor", "sensor", "text_sensor"]
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_HLK_FM22X_ID = "hlk_fm22x_id"
|
||||
CONF_FACE_ID = "face_id"
|
||||
CONF_ON_FACE_SCAN_MATCHED = "on_face_scan_matched"
|
||||
CONF_ON_FACE_SCAN_UNMATCHED = "on_face_scan_unmatched"
|
||||
CONF_ON_FACE_SCAN_INVALID = "on_face_scan_invalid"
|
||||
CONF_ON_FACE_INFO = "on_face_info"
|
||||
|
||||
hlk_fm22x_ns = cg.esphome_ns.namespace("hlk_fm22x")
|
||||
HlkFm22xComponent = hlk_fm22x_ns.class_(
|
||||
"HlkFm22xComponent", cg.PollingComponent, uart.UARTDevice
|
||||
)
|
||||
|
||||
FaceScanMatchedTrigger = hlk_fm22x_ns.class_(
|
||||
"FaceScanMatchedTrigger", automation.Trigger.template(cg.int16, cg.std_string)
|
||||
)
|
||||
|
||||
FaceScanUnmatchedTrigger = hlk_fm22x_ns.class_(
|
||||
"FaceScanUnmatchedTrigger", automation.Trigger.template()
|
||||
)
|
||||
|
||||
FaceScanInvalidTrigger = hlk_fm22x_ns.class_(
|
||||
"FaceScanInvalidTrigger", automation.Trigger.template(cg.uint8)
|
||||
)
|
||||
|
||||
FaceInfoTrigger = hlk_fm22x_ns.class_(
|
||||
"FaceInfoTrigger",
|
||||
automation.Trigger.template(
|
||||
cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16, cg.int16
|
||||
),
|
||||
)
|
||||
|
||||
EnrollmentDoneTrigger = hlk_fm22x_ns.class_(
|
||||
"EnrollmentDoneTrigger", automation.Trigger.template(cg.int16, cg.uint8)
|
||||
)
|
||||
|
||||
EnrollmentFailedTrigger = hlk_fm22x_ns.class_(
|
||||
"EnrollmentFailedTrigger", automation.Trigger.template(cg.uint8)
|
||||
)
|
||||
|
||||
EnrollmentAction = hlk_fm22x_ns.class_("EnrollmentAction", automation.Action)
|
||||
DeleteAction = hlk_fm22x_ns.class_("DeleteAction", automation.Action)
|
||||
DeleteAllAction = hlk_fm22x_ns.class_("DeleteAllAction", automation.Action)
|
||||
ScanAction = hlk_fm22x_ns.class_("ScanAction", automation.Action)
|
||||
ResetAction = hlk_fm22x_ns.class_("ResetAction", automation.Action)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(HlkFm22xComponent),
|
||||
cv.Optional(CONF_ON_FACE_SCAN_MATCHED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
FaceScanMatchedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_FACE_SCAN_UNMATCHED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
FaceScanUnmatchedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_FACE_SCAN_INVALID): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
FaceScanInvalidTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_FACE_INFO): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FaceInfoTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
EnrollmentDoneTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
EnrollmentFailedTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("50ms"))
|
||||
.extend(uart.UART_DEVICE_SCHEMA),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
for conf in config.get(CONF_ON_FACE_SCAN_MATCHED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(cg.int16, "face_id"), (cg.std_string, "name")], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_FACE_SCAN_UNMATCHED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_FACE_SCAN_INVALID, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(cg.uint8, "error")], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_FACE_INFO, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger,
|
||||
[
|
||||
(cg.int16, "status"),
|
||||
(cg.int16, "left"),
|
||||
(cg.int16, "top"),
|
||||
(cg.int16, "right"),
|
||||
(cg.int16, "bottom"),
|
||||
(cg.int16, "yaw"),
|
||||
(cg.int16, "pitch"),
|
||||
(cg.int16, "roll"),
|
||||
],
|
||||
conf,
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_ENROLLMENT_DONE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger, [(cg.int16, "face_id"), (cg.uint8, "direction")], conf
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(cg.uint8, "error")], conf)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"hlk_fm22x.enroll",
|
||||
EnrollmentAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
|
||||
cv.Required(CONF_NAME): cv.templatable(cv.string),
|
||||
cv.Required(CONF_DIRECTION): cv.templatable(cv.uint8_t),
|
||||
},
|
||||
key=CONF_NAME,
|
||||
),
|
||||
)
|
||||
async def hlk_fm22x_enroll_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
template_ = await cg.templatable(config[CONF_NAME], args, cg.std_string)
|
||||
cg.add(var.set_name(template_))
|
||||
template_ = await cg.templatable(config[CONF_DIRECTION], args, cg.uint8)
|
||||
cg.add(var.set_direction(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"hlk_fm22x.delete",
|
||||
DeleteAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
|
||||
cv.Required(CONF_FACE_ID): cv.templatable(cv.uint16_t),
|
||||
},
|
||||
key=CONF_FACE_ID,
|
||||
),
|
||||
)
|
||||
async def hlk_fm22x_delete_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
template_ = await cg.templatable(config[CONF_FACE_ID], args, cg.int16)
|
||||
cg.add(var.set_face_id(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"hlk_fm22x.delete_all",
|
||||
DeleteAllAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def hlk_fm22x_delete_all_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(
|
||||
"hlk_fm22x.scan",
|
||||
ScanAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def hlk_fm22x_scan_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(
|
||||
"hlk_fm22x.reset",
|
||||
ResetAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(HlkFm22xComponent),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def hlk_fm22x_reset_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
|
||||
@@ -1,21 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ICON, ICON_KEY_PLUS
|
||||
|
||||
from . import CONF_HLK_FM22X_ID, HlkFm22xComponent
|
||||
|
||||
DEPENDENCIES = ["hlk_fm22x"]
|
||||
|
||||
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend(
|
||||
{
|
||||
cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent),
|
||||
cv.Optional(CONF_ICON, default=ICON_KEY_PLUS): cv.icon,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_HLK_FM22X_ID])
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
cg.add(hub.set_enrolling_binary_sensor(var))
|
||||
@@ -1,325 +0,0 @@
|
||||
#include "hlk_fm22x.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <array>
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::hlk_fm22x {
|
||||
|
||||
static const char *const TAG = "hlk_fm22x";
|
||||
|
||||
void HlkFm22xComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X...");
|
||||
this->set_enrolling_(false);
|
||||
while (this->available()) {
|
||||
this->read();
|
||||
}
|
||||
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::update() {
|
||||
if (this->active_command_ != HlkFm22xCommand::NONE) {
|
||||
if (this->wait_cycles_ > 600) {
|
||||
ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_);
|
||||
if (HlkFm22xCommand::RESET == this->active_command_) {
|
||||
this->mark_failed();
|
||||
} else {
|
||||
this->reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
this->recv_command_();
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) {
|
||||
if (name.length() > 31) {
|
||||
ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str());
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "Starting enrollment for %s", name.c_str());
|
||||
std::array<uint8_t, 35> data{};
|
||||
data[0] = 0; // admin
|
||||
std::copy(name.begin(), name.end(), data.begin() + 1);
|
||||
// Remaining bytes are already zero-initialized
|
||||
data[33] = (uint8_t) direction;
|
||||
data[34] = 10; // timeout
|
||||
this->send_command_(HlkFm22xCommand::ENROLL, data.data(), data.size());
|
||||
this->set_enrolling_(true);
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::scan_face() {
|
||||
ESP_LOGI(TAG, "Verify face");
|
||||
static const uint8_t DATA[] = {0, 0};
|
||||
this->send_command_(HlkFm22xCommand::VERIFY, DATA, sizeof(DATA));
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::delete_face(int16_t face_id) {
|
||||
ESP_LOGI(TAG, "Deleting face in slot %d", face_id);
|
||||
const uint8_t data[] = {(uint8_t) (face_id >> 8), (uint8_t) (face_id & 0xFF)};
|
||||
this->send_command_(HlkFm22xCommand::DELETE_FACE, data, sizeof(data));
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::delete_all_faces() {
|
||||
ESP_LOGI(TAG, "Deleting all stored faces");
|
||||
this->send_command_(HlkFm22xCommand::DELETE_ALL_FACES);
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::get_face_count_() {
|
||||
ESP_LOGD(TAG, "Getting face count");
|
||||
this->send_command_(HlkFm22xCommand::GET_ALL_FACE_IDS);
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::reset() {
|
||||
ESP_LOGI(TAG, "Resetting module");
|
||||
this->active_command_ = HlkFm22xCommand::NONE;
|
||||
this->wait_cycles_ = 0;
|
||||
this->set_enrolling_(false);
|
||||
this->send_command_(HlkFm22xCommand::RESET);
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *data, size_t size) {
|
||||
ESP_LOGV(TAG, "Send command: 0x%.2X", command);
|
||||
if (this->active_command_ != HlkFm22xCommand::NONE) {
|
||||
ESP_LOGW(TAG, "Command 0x%.2X already active", this->active_command_);
|
||||
return;
|
||||
}
|
||||
this->wait_cycles_ = 0;
|
||||
this->active_command_ = command;
|
||||
while (this->available())
|
||||
this->read();
|
||||
this->write((uint8_t) (START_CODE >> 8));
|
||||
this->write((uint8_t) (START_CODE & 0xFF));
|
||||
this->write((uint8_t) command);
|
||||
uint16_t data_size = size;
|
||||
this->write((uint8_t) (data_size >> 8));
|
||||
this->write((uint8_t) (data_size & 0xFF));
|
||||
|
||||
uint8_t checksum = 0;
|
||||
checksum ^= (uint8_t) command;
|
||||
checksum ^= (data_size >> 8);
|
||||
checksum ^= (data_size & 0xFF);
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
this->write(data[i]);
|
||||
checksum ^= data[i];
|
||||
}
|
||||
|
||||
this->write(checksum);
|
||||
this->active_command_ = command;
|
||||
this->wait_cycles_ = 0;
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::recv_command_() {
|
||||
uint8_t byte, checksum = 0;
|
||||
uint16_t length = 0;
|
||||
|
||||
if (this->available() < 7) {
|
||||
++this->wait_cycles_;
|
||||
return;
|
||||
}
|
||||
this->wait_cycles_ = 0;
|
||||
|
||||
if ((this->read() != (uint8_t) (START_CODE >> 8)) || (this->read() != (uint8_t) (START_CODE & 0xFF))) {
|
||||
ESP_LOGE(TAG, "Invalid start code");
|
||||
return;
|
||||
}
|
||||
|
||||
byte = this->read();
|
||||
checksum ^= byte;
|
||||
HlkFm22xResponseType response_type = (HlkFm22xResponseType) byte;
|
||||
|
||||
byte = this->read();
|
||||
checksum ^= byte;
|
||||
length = byte << 8;
|
||||
byte = this->read();
|
||||
checksum ^= byte;
|
||||
length |= byte;
|
||||
|
||||
std::vector<uint8_t> data;
|
||||
data.reserve(length);
|
||||
for (uint16_t idx = 0; idx < length; ++idx) {
|
||||
byte = this->read();
|
||||
checksum ^= byte;
|
||||
data.push_back(byte);
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty(data).c_str());
|
||||
|
||||
byte = this->read();
|
||||
if (byte != checksum) {
|
||||
ESP_LOGE(TAG, "Invalid checksum for data. Calculated: 0x%.2X, Received: 0x%.2X", checksum, byte);
|
||||
return;
|
||||
}
|
||||
switch (response_type) {
|
||||
case HlkFm22xResponseType::NOTE:
|
||||
this->handle_note_(data);
|
||||
break;
|
||||
case HlkFm22xResponseType::REPLY:
|
||||
this->handle_reply_(data);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::handle_note_(const std::vector<uint8_t> &data) {
|
||||
switch (data[0]) {
|
||||
case HlkFm22xNoteType::FACE_STATE:
|
||||
if (data.size() < 17) {
|
||||
ESP_LOGE(TAG, "Invalid face note data size: %u", data.size());
|
||||
break;
|
||||
}
|
||||
{
|
||||
int16_t info[8];
|
||||
uint8_t offset = 1;
|
||||
for (int16_t &i : info) {
|
||||
i = ((int16_t) data[offset + 1] << 8) | data[offset];
|
||||
offset += 2;
|
||||
}
|
||||
ESP_LOGV(TAG, "Face state: status: %d, left: %d, top: %d, right: %d, bottom: %d, yaw: %d, pitch: %d, roll: %d",
|
||||
info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]);
|
||||
this->face_info_callback_.call(info[0], info[1], info[2], info[3], info[4], info[5], info[6], info[7]);
|
||||
}
|
||||
break;
|
||||
case HlkFm22xNoteType::READY:
|
||||
ESP_LOGE(TAG, "Command 0x%.2X timed out", this->active_command_);
|
||||
switch (this->active_command_) {
|
||||
case HlkFm22xCommand::ENROLL:
|
||||
this->set_enrolling_(false);
|
||||
this->enrollment_failed_callback_.call(HlkFm22xResult::FAILED4_TIMEOUT);
|
||||
break;
|
||||
case HlkFm22xCommand::VERIFY:
|
||||
this->face_scan_invalid_callback_.call(HlkFm22xResult::FAILED4_TIMEOUT);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
this->active_command_ = HlkFm22xCommand::NONE;
|
||||
this->wait_cycles_ = 0;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unhandled note: 0x%.2X", data[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
|
||||
auto expected = this->active_command_;
|
||||
this->active_command_ = HlkFm22xCommand::NONE;
|
||||
if (data[0] != (uint8_t) expected) {
|
||||
ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data[1] != HlkFm22xResult::SUCCESS) {
|
||||
ESP_LOGE(TAG, "Command <0x%.2X> failed. Error: 0x%.2X", data[0], data[1]);
|
||||
switch (expected) {
|
||||
case HlkFm22xCommand::ENROLL:
|
||||
this->set_enrolling_(false);
|
||||
this->enrollment_failed_callback_.call(data[1]);
|
||||
break;
|
||||
case HlkFm22xCommand::VERIFY:
|
||||
if (data[1] == HlkFm22xResult::REJECTED) {
|
||||
this->face_scan_unmatched_callback_.call();
|
||||
} else {
|
||||
this->face_scan_invalid_callback_.call(data[1]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (expected) {
|
||||
case HlkFm22xCommand::VERIFY: {
|
||||
int16_t face_id = ((int16_t) data[2] << 8) | data[3];
|
||||
std::string name(data.begin() + 4, data.begin() + 36);
|
||||
ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str());
|
||||
if (this->last_face_id_sensor_ != nullptr) {
|
||||
this->last_face_id_sensor_->publish_state(face_id);
|
||||
}
|
||||
if (this->last_face_name_text_sensor_ != nullptr) {
|
||||
this->last_face_name_text_sensor_->publish_state(name);
|
||||
}
|
||||
this->face_scan_matched_callback_.call(face_id, name);
|
||||
break;
|
||||
}
|
||||
case HlkFm22xCommand::ENROLL: {
|
||||
int16_t face_id = ((int16_t) data[2] << 8) | data[3];
|
||||
HlkFm22xFaceDirection direction = (HlkFm22xFaceDirection) data[4];
|
||||
ESP_LOGI(TAG, "Face enrolled. ID: %d, Direction: 0x%.2X", face_id, direction);
|
||||
this->enrollment_done_callback_.call(face_id, (uint8_t) direction);
|
||||
this->set_enrolling_(false);
|
||||
this->defer([this]() { this->get_face_count_(); });
|
||||
break;
|
||||
}
|
||||
case HlkFm22xCommand::GET_STATUS:
|
||||
if (this->status_sensor_ != nullptr) {
|
||||
this->status_sensor_->publish_state(data[2]);
|
||||
}
|
||||
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); });
|
||||
break;
|
||||
case HlkFm22xCommand::GET_VERSION:
|
||||
if (this->version_text_sensor_ != nullptr) {
|
||||
std::string version(data.begin() + 2, data.end());
|
||||
this->version_text_sensor_->publish_state(version);
|
||||
}
|
||||
this->defer([this]() { this->get_face_count_(); });
|
||||
break;
|
||||
case HlkFm22xCommand::GET_ALL_FACE_IDS:
|
||||
if (this->face_count_sensor_ != nullptr) {
|
||||
this->face_count_sensor_->publish_state(data[2]);
|
||||
}
|
||||
break;
|
||||
case HlkFm22xCommand::DELETE_FACE:
|
||||
ESP_LOGI(TAG, "Deleted face");
|
||||
break;
|
||||
case HlkFm22xCommand::DELETE_ALL_FACES:
|
||||
ESP_LOGI(TAG, "Deleted all faces");
|
||||
break;
|
||||
case HlkFm22xCommand::RESET:
|
||||
ESP_LOGI(TAG, "Module reset");
|
||||
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unhandled command: 0x%.2X", this->active_command_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::set_enrolling_(bool enrolling) {
|
||||
if (this->enrolling_binary_sensor_ != nullptr) {
|
||||
this->enrolling_binary_sensor_->publish_state(enrolling);
|
||||
}
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "HLK_FM22X:");
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
if (this->version_text_sensor_) {
|
||||
LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %s", this->version_text_sensor_->get_state().c_str());
|
||||
}
|
||||
if (this->enrolling_binary_sensor_) {
|
||||
LOG_BINARY_SENSOR(" ", "Enrolling", this->enrolling_binary_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %s", this->enrolling_binary_sensor_->state ? "ON" : "OFF");
|
||||
}
|
||||
if (this->face_count_sensor_) {
|
||||
LOG_SENSOR(" ", "Face Count", this->face_count_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %u", (uint16_t) this->face_count_sensor_->get_state());
|
||||
}
|
||||
if (this->status_sensor_) {
|
||||
LOG_SENSOR(" ", "Status", this->status_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %u", (uint8_t) this->status_sensor_->get_state());
|
||||
}
|
||||
if (this->last_face_id_sensor_) {
|
||||
LOG_SENSOR(" ", "Last Face ID", this->last_face_id_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %u", (int16_t) this->last_face_id_sensor_->get_state());
|
||||
}
|
||||
if (this->last_face_name_text_sensor_) {
|
||||
LOG_TEXT_SENSOR(" ", "Last Face Name", this->last_face_name_text_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %s", this->last_face_name_text_sensor_->get_state().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::hlk_fm22x
|
||||
@@ -1,224 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::hlk_fm22x {
|
||||
|
||||
static const uint16_t START_CODE = 0xEFAA;
|
||||
enum HlkFm22xCommand {
|
||||
NONE = 0x00,
|
||||
RESET = 0x10,
|
||||
GET_STATUS = 0x11,
|
||||
VERIFY = 0x12,
|
||||
ENROLL = 0x13,
|
||||
DELETE_FACE = 0x20,
|
||||
DELETE_ALL_FACES = 0x21,
|
||||
GET_ALL_FACE_IDS = 0x24,
|
||||
GET_VERSION = 0x30,
|
||||
GET_SERIAL_NUMBER = 0x93,
|
||||
};
|
||||
|
||||
enum HlkFm22xResponseType {
|
||||
REPLY = 0x00,
|
||||
NOTE = 0x01,
|
||||
IMAGE = 0x02,
|
||||
};
|
||||
|
||||
enum HlkFm22xNoteType {
|
||||
READY = 0x00,
|
||||
FACE_STATE = 0x01,
|
||||
};
|
||||
|
||||
enum HlkFm22xResult {
|
||||
SUCCESS = 0x00,
|
||||
REJECTED = 0x01,
|
||||
ABORTED = 0x02,
|
||||
FAILED4_CAMERA = 0x04,
|
||||
FAILED4_UNKNOWNREASON = 0x05,
|
||||
FAILED4_INVALIDPARAM = 0x06,
|
||||
FAILED4_NOMEMORY = 0x07,
|
||||
FAILED4_UNKNOWNUSER = 0x08,
|
||||
FAILED4_MAXUSER = 0x09,
|
||||
FAILED4_FACEENROLLED = 0x0A,
|
||||
FAILED4_LIVENESSCHECK = 0x0C,
|
||||
FAILED4_TIMEOUT = 0x0D,
|
||||
FAILED4_AUTHORIZATION = 0x0E,
|
||||
FAILED4_READ_FILE = 0x13,
|
||||
FAILED4_WRITE_FILE = 0x14,
|
||||
FAILED4_NO_ENCRYPT = 0x15,
|
||||
FAILED4_NO_RGBIMAGE = 0x17,
|
||||
FAILED4_JPGPHOTO_LARGE = 0x18,
|
||||
FAILED4_JPGPHOTO_SMALL = 0x19,
|
||||
};
|
||||
|
||||
enum HlkFm22xFaceDirection {
|
||||
FACE_DIRECTION_UNDEFINED = 0x00,
|
||||
FACE_DIRECTION_MIDDLE = 0x01,
|
||||
FACE_DIRECTION_RIGHT = 0x02,
|
||||
FACE_DIRECTION_LEFT = 0x04,
|
||||
FACE_DIRECTION_DOWN = 0x08,
|
||||
FACE_DIRECTION_UP = 0x10,
|
||||
};
|
||||
|
||||
class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_face_count_sensor(sensor::Sensor *face_count_sensor) { this->face_count_sensor_ = face_count_sensor; }
|
||||
void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; }
|
||||
void set_last_face_id_sensor(sensor::Sensor *last_face_id_sensor) {
|
||||
this->last_face_id_sensor_ = last_face_id_sensor;
|
||||
}
|
||||
void set_last_face_name_text_sensor(text_sensor::TextSensor *last_face_name_text_sensor) {
|
||||
this->last_face_name_text_sensor_ = last_face_name_text_sensor;
|
||||
}
|
||||
void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) {
|
||||
this->enrolling_binary_sensor_ = enrolling_binary_sensor;
|
||||
}
|
||||
void set_version_text_sensor(text_sensor::TextSensor *version_text_sensor) {
|
||||
this->version_text_sensor_ = version_text_sensor;
|
||||
}
|
||||
void add_on_face_scan_matched_callback(std::function<void(int16_t, std::string)> callback) {
|
||||
this->face_scan_matched_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_face_scan_unmatched_callback(std::function<void()> callback) {
|
||||
this->face_scan_unmatched_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_face_scan_invalid_callback(std::function<void(uint8_t)> callback) {
|
||||
this->face_scan_invalid_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_face_info_callback(
|
||||
std::function<void(int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t)> callback) {
|
||||
this->face_info_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_enrollment_done_callback(std::function<void(int16_t, uint8_t)> callback) {
|
||||
this->enrollment_done_callback_.add(std::move(callback));
|
||||
}
|
||||
void add_on_enrollment_failed_callback(std::function<void(uint8_t)> callback) {
|
||||
this->enrollment_failed_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void enroll_face(const std::string &name, HlkFm22xFaceDirection direction);
|
||||
void scan_face();
|
||||
void delete_face(int16_t face_id);
|
||||
void delete_all_faces();
|
||||
void reset();
|
||||
|
||||
protected:
|
||||
void get_face_count_();
|
||||
void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0);
|
||||
void recv_command_();
|
||||
void handle_note_(const std::vector<uint8_t> &data);
|
||||
void handle_reply_(const std::vector<uint8_t> &data);
|
||||
void set_enrolling_(bool enrolling);
|
||||
|
||||
HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE;
|
||||
uint16_t wait_cycles_ = 0;
|
||||
sensor::Sensor *face_count_sensor_{nullptr};
|
||||
sensor::Sensor *status_sensor_{nullptr};
|
||||
sensor::Sensor *last_face_id_sensor_{nullptr};
|
||||
binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr};
|
||||
text_sensor::TextSensor *last_face_name_text_sensor_{nullptr};
|
||||
text_sensor::TextSensor *version_text_sensor_{nullptr};
|
||||
CallbackManager<void(uint8_t)> face_scan_invalid_callback_;
|
||||
CallbackManager<void(int16_t, std::string)> face_scan_matched_callback_;
|
||||
CallbackManager<void()> face_scan_unmatched_callback_;
|
||||
CallbackManager<void(int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t)> face_info_callback_;
|
||||
CallbackManager<void(int16_t, uint8_t)> enrollment_done_callback_;
|
||||
CallbackManager<void(uint8_t)> enrollment_failed_callback_;
|
||||
};
|
||||
|
||||
class FaceScanMatchedTrigger : public Trigger<int16_t, std::string> {
|
||||
public:
|
||||
explicit FaceScanMatchedTrigger(HlkFm22xComponent *parent) {
|
||||
parent->add_on_face_scan_matched_callback(
|
||||
[this](int16_t face_id, const std::string &name) { this->trigger(face_id, name); });
|
||||
}
|
||||
};
|
||||
|
||||
class FaceScanUnmatchedTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit FaceScanUnmatchedTrigger(HlkFm22xComponent *parent) {
|
||||
parent->add_on_face_scan_unmatched_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
class FaceScanInvalidTrigger : public Trigger<uint8_t> {
|
||||
public:
|
||||
explicit FaceScanInvalidTrigger(HlkFm22xComponent *parent) {
|
||||
parent->add_on_face_scan_invalid_callback([this](uint8_t error) { this->trigger(error); });
|
||||
}
|
||||
};
|
||||
|
||||
class FaceInfoTrigger : public Trigger<int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t> {
|
||||
public:
|
||||
explicit FaceInfoTrigger(HlkFm22xComponent *parent) {
|
||||
parent->add_on_face_info_callback(
|
||||
[this](int16_t status, int16_t left, int16_t top, int16_t right, int16_t bottom, int16_t yaw, int16_t pitch,
|
||||
int16_t roll) { this->trigger(status, left, top, right, bottom, yaw, pitch, roll); });
|
||||
}
|
||||
};
|
||||
|
||||
class EnrollmentDoneTrigger : public Trigger<int16_t, uint8_t> {
|
||||
public:
|
||||
explicit EnrollmentDoneTrigger(HlkFm22xComponent *parent) {
|
||||
parent->add_on_enrollment_done_callback(
|
||||
[this](int16_t face_id, uint8_t direction) { this->trigger(face_id, direction); });
|
||||
}
|
||||
};
|
||||
|
||||
class EnrollmentFailedTrigger : public Trigger<uint8_t> {
|
||||
public:
|
||||
explicit EnrollmentFailedTrigger(HlkFm22xComponent *parent) {
|
||||
parent->add_on_enrollment_failed_callback([this](uint8_t error) { this->trigger(error); });
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class EnrollmentAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(std::string, name)
|
||||
TEMPLATABLE_VALUE(uint8_t, direction)
|
||||
|
||||
void play(Ts... x) override {
|
||||
auto name = this->name_.value(x...);
|
||||
auto direction = (HlkFm22xFaceDirection) this->direction_.value(x...);
|
||||
this->parent_->enroll_face(name, direction);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class DeleteAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(int16_t, face_id)
|
||||
|
||||
void play(Ts... x) override {
|
||||
auto face_id = this->face_id_.value(x...);
|
||||
this->parent_->delete_face(face_id);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class DeleteAllAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->delete_all_faces(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class ScanAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->scan_face(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class ResetAction : public Action<Ts...>, public Parented<HlkFm22xComponent> {
|
||||
public:
|
||||
void play(Ts... x) override { this->parent_->reset(); }
|
||||
};
|
||||
|
||||
} // namespace esphome::hlk_fm22x
|
||||
@@ -1,47 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_STATUS, ENTITY_CATEGORY_DIAGNOSTIC, ICON_ACCOUNT
|
||||
|
||||
from . import CONF_HLK_FM22X_ID, HlkFm22xComponent
|
||||
|
||||
DEPENDENCIES = ["hlk_fm22x"]
|
||||
|
||||
CONF_FACE_COUNT = "face_count"
|
||||
CONF_LAST_FACE_ID = "last_face_id"
|
||||
ICON_FACE = "mdi:face-recognition"
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent),
|
||||
cv.Optional(CONF_FACE_COUNT): sensor.sensor_schema(
|
||||
icon=ICON_FACE,
|
||||
accuracy_decimals=0,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_STATUS): sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_LAST_FACE_ID): sensor.sensor_schema(
|
||||
icon=ICON_ACCOUNT,
|
||||
accuracy_decimals=0,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_HLK_FM22X_ID])
|
||||
|
||||
for key in [
|
||||
CONF_FACE_COUNT,
|
||||
CONF_STATUS,
|
||||
CONF_LAST_FACE_ID,
|
||||
]:
|
||||
if key not in config:
|
||||
continue
|
||||
conf = config[key]
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(getattr(hub, f"set_{key}_sensor")(sens))
|
||||
@@ -1,42 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_VERSION,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
ICON_ACCOUNT,
|
||||
ICON_RESTART,
|
||||
)
|
||||
|
||||
from . import CONF_HLK_FM22X_ID, HlkFm22xComponent
|
||||
|
||||
DEPENDENCIES = ["hlk_fm22x"]
|
||||
|
||||
CONF_LAST_FACE_NAME = "last_face_name"
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_HLK_FM22X_ID): cv.use_id(HlkFm22xComponent),
|
||||
cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema(
|
||||
icon=ICON_RESTART,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_LAST_FACE_NAME): text_sensor.text_sensor_schema(
|
||||
icon=ICON_ACCOUNT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
hub = await cg.get_variable(config[CONF_HLK_FM22X_ID])
|
||||
for key in [
|
||||
CONF_VERSION,
|
||||
CONF_LAST_FACE_NAME,
|
||||
]:
|
||||
if key not in config:
|
||||
continue
|
||||
conf = config[key]
|
||||
sens = await text_sensor.new_text_sensor(conf)
|
||||
cg.add(getattr(hub, f"set_{key}_text_sensor")(sens))
|
||||
@@ -7,8 +7,10 @@ namespace homeassistant {
|
||||
static const char *const TAG = "homeassistant.time";
|
||||
|
||||
void HomeassistantTime::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Home Assistant Time");
|
||||
RealTimeClock::dump_config();
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Home Assistant Time:\n"
|
||||
" Timezone: '%s'",
|
||||
this->timezone_.c_str());
|
||||
}
|
||||
|
||||
float HomeassistantTime::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import importlib
|
||||
import logging
|
||||
import pkgutil
|
||||
|
||||
from esphome.automation import build_automation, validate_automation
|
||||
from esphome.automation import build_automation, register_action, validate_automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING
|
||||
from esphome.components.display import Display
|
||||
@@ -27,8 +25,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, widgets
|
||||
from .automation import disp_update, focused_widgets, refreshed_widgets
|
||||
from . import defines as df, helpers, lv_validation as lvalid
|
||||
from .automation import disp_update, focused_widgets, refreshed_widgets, update_to_code
|
||||
from .defines import add_define
|
||||
from .encoders import (
|
||||
ENCODERS_CONFIG,
|
||||
@@ -47,6 +45,7 @@ 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
|
||||
@@ -55,6 +54,7 @@ from .trigger import add_on_boot_triggers, generate_triggers
|
||||
from .types import (
|
||||
FontEngine,
|
||||
IdleTrigger,
|
||||
ObjUpdateAction,
|
||||
PlainTrigger,
|
||||
lv_font_t,
|
||||
lv_group_t,
|
||||
@@ -69,23 +69,33 @@ from .widgets import (
|
||||
set_obj_properties,
|
||||
styles_used,
|
||||
)
|
||||
|
||||
# Import only what we actually use directly in this file
|
||||
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
|
||||
from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code
|
||||
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__)
|
||||
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
|
||||
|
||||
DOMAIN = "lvgl"
|
||||
DEPENDENCIES = ["display"]
|
||||
@@ -93,6 +103,41 @@ 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,
|
||||
@@ -331,7 +376,7 @@ async def to_code(configs):
|
||||
# This must be done after all widgets are created
|
||||
for comp in helpers.lvgl_components_required:
|
||||
cg.add_define(f"USE_LVGL_{comp.upper()}")
|
||||
if {"transform_angle", "transform_zoom"} & styles_used:
|
||||
if "transform_angle" in styles_used:
|
||||
add_define("LV_COLOR_SCREEN_TRANSP", "1")
|
||||
for use in helpers.lv_uses:
|
||||
add_define(f"LV_USE_{use.upper()}")
|
||||
@@ -357,15 +402,6 @@ 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(
|
||||
@@ -418,7 +454,12 @@ LVGL_SCHEMA = cv.All(
|
||||
cv.Optional(
|
||||
df.CONF_TRANSPARENCY_KEY, default=0x000400
|
||||
): lvalid.lv_color,
|
||||
cv.Optional(df.CONF_THEME): _theme_schema,
|
||||
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_GRADIENTS): GRADIENT_SCHEMA,
|
||||
cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema,
|
||||
cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG,
|
||||
|
||||
@@ -411,10 +411,6 @@ 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,10 +1,8 @@
|
||||
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
|
||||
|
||||
@@ -126,16 +124,13 @@ 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, 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
|
||||
:param modify_schema: A schema to update the widget
|
||||
"""
|
||||
self.name = name
|
||||
self.lv_name = lv_name or name
|
||||
@@ -151,22 +146,6 @@ 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,14 +213,17 @@ class LvScrActType(WidgetType):
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("lv_scr_act()", lv_obj_t, (), is_mock=True)
|
||||
super().__init__("lv_scr_act()", lv_obj_t, ())
|
||||
|
||||
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(), LvScrActType(), {})
|
||||
return Widget.create(None, lv_comp.get_scr_act(), lv_scr_act_spec, {})
|
||||
|
||||
|
||||
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
|
||||
from ..automation import action_to_code, update_to_code
|
||||
from ..defines import (
|
||||
CONF_CURSOR,
|
||||
CONF_DECIMAL_PLACES,
|
||||
@@ -171,3 +171,17 @@ 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)
|
||||
|
||||
@@ -25,7 +25,6 @@ from esphome.const import (
|
||||
CONF_FRAMEWORK,
|
||||
CONF_ID,
|
||||
CONF_RESET_PIN,
|
||||
CONF_VOLTAGE,
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
@@ -103,11 +102,6 @@ nrf52_ns = cg.esphome_ns.namespace("nrf52")
|
||||
DeviceFirmwareUpdate = nrf52_ns.class_("DeviceFirmwareUpdate", cg.Component)
|
||||
|
||||
CONF_DFU = "dfu"
|
||||
CONF_REG0 = "reg0"
|
||||
CONF_UICR_ERASE = "uicr_erase"
|
||||
|
||||
VOLTAGE_LEVELS = [1.8, 2.1, 2.4, 2.7, 3.0, 3.3]
|
||||
DEFAULT_VOLTAGE_LEVEL = "default"
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
_detect_bootloader,
|
||||
@@ -122,18 +116,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_REG0): cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_VOLTAGE): cv.Any(
|
||||
cv.All(
|
||||
cv.voltage,
|
||||
cv.one_of(*VOLTAGE_LEVELS, float=True),
|
||||
),
|
||||
cv.one_of(*[DEFAULT_VOLTAGE_LEVEL], lower=True),
|
||||
),
|
||||
cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -201,14 +183,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
if dfu_config := config.get(CONF_DFU):
|
||||
CORE.add_job(_dfu_to_code, dfu_config)
|
||||
|
||||
if reg0_config := config.get(CONF_REG0):
|
||||
value = 7 # DEFAULT_VOLTAGE_LEVEL
|
||||
if reg0_config[CONF_VOLTAGE] in VOLTAGE_LEVELS:
|
||||
value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE])
|
||||
cg.add_define("USE_NRF52_REG0_VOUT", value)
|
||||
if reg0_config[CONF_UICR_ERASE]:
|
||||
cg.add_define("USE_NRF52_UICR_ERASE")
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.DIAGNOSTICS)
|
||||
async def _dfu_to_code(dfu_config):
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_NRF52_REG0_VOUT
|
||||
#include <zephyr/init.h>
|
||||
#include <hal/nrf_power.h>
|
||||
#include <zephyr/sys/printk.h>
|
||||
|
||||
extern "C" {
|
||||
void nvmc_config(uint32_t mode);
|
||||
void nvmc_wait();
|
||||
nrfx_err_t nrfx_nvmc_uicr_erase();
|
||||
}
|
||||
|
||||
namespace esphome::nrf52 {
|
||||
|
||||
enum class StatusFlags : uint8_t {
|
||||
OK = 0x00,
|
||||
NEED_RESET = 0x01,
|
||||
NEED_ERASE = 0x02,
|
||||
};
|
||||
|
||||
constexpr StatusFlags &operator|=(StatusFlags &a, StatusFlags b) {
|
||||
a = static_cast<StatusFlags>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
|
||||
return a;
|
||||
}
|
||||
|
||||
constexpr bool operator&(StatusFlags a, StatusFlags b) {
|
||||
return (static_cast<uint8_t>(a) & static_cast<uint8_t>(b)) != 0;
|
||||
}
|
||||
|
||||
static bool regout0_ok() {
|
||||
return (NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) == (USE_NRF52_REG0_VOUT << UICR_REGOUT0_VOUT_Pos);
|
||||
}
|
||||
|
||||
static StatusFlags set_regout0() {
|
||||
/* If the board is powered from USB (high voltage mode),
|
||||
* GPIO output voltage is set to 1.8 volts by default.
|
||||
*/
|
||||
if (!regout0_ok()) {
|
||||
nvmc_config(NVMC_CONFIG_WEN_Wen);
|
||||
NRF_UICR->REGOUT0 =
|
||||
(NRF_UICR->REGOUT0 & ~((uint32_t) UICR_REGOUT0_VOUT_Msk)) | (USE_NRF52_REG0_VOUT << UICR_REGOUT0_VOUT_Pos);
|
||||
nvmc_wait();
|
||||
nvmc_config(NVMC_CONFIG_WEN_Ren);
|
||||
return regout0_ok() ? StatusFlags::NEED_RESET : StatusFlags::NEED_ERASE;
|
||||
}
|
||||
return StatusFlags::OK;
|
||||
}
|
||||
|
||||
#ifndef USE_BOOTLOADER_MCUBOOT
|
||||
// https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/6a9a6a3e6d0f86918e9286188426a279976645bd/lib/sdk11/components/libraries/bootloader_dfu/dfu_types.h#L61
|
||||
constexpr uint32_t BOOTLOADER_REGION_START = 0x000F4000;
|
||||
constexpr uint32_t BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS = 0x000FE000;
|
||||
|
||||
static bool bootloader_ok() {
|
||||
return NRF_UICR->NRFFW[0] == BOOTLOADER_REGION_START && NRF_UICR->NRFFW[1] == BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS;
|
||||
}
|
||||
|
||||
static StatusFlags fix_bootloader() {
|
||||
if (!bootloader_ok()) {
|
||||
nvmc_config(NVMC_CONFIG_WEN_Wen);
|
||||
NRF_UICR->NRFFW[0] = BOOTLOADER_REGION_START;
|
||||
NRF_UICR->NRFFW[1] = BOOTLOADER_MBR_PARAMS_PAGE_ADDRESS;
|
||||
nvmc_wait();
|
||||
nvmc_config(NVMC_CONFIG_WEN_Ren);
|
||||
return bootloader_ok() ? StatusFlags::NEED_RESET : StatusFlags::NEED_ERASE;
|
||||
}
|
||||
return StatusFlags::OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
static StatusFlags set_uicr() {
|
||||
StatusFlags status = StatusFlags::OK;
|
||||
status |= set_regout0();
|
||||
#ifndef USE_BOOTLOADER_MCUBOOT
|
||||
status |= fix_bootloader();
|
||||
#endif
|
||||
return status;
|
||||
}
|
||||
|
||||
static int board_esphome_init() {
|
||||
StatusFlags status = set_uicr();
|
||||
|
||||
#ifdef USE_NRF52_UICR_ERASE
|
||||
if (status & StatusFlags::NEED_ERASE) {
|
||||
nrfx_err_t ret = nrfx_nvmc_uicr_erase();
|
||||
if (ret != NRFX_SUCCESS) {
|
||||
#ifdef CONFIG_PRINTK
|
||||
printk("nrfx_nvmc_uicr_erase failed %d\n", ret);
|
||||
#endif
|
||||
} else {
|
||||
status |= set_uicr();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (status & StatusFlags::NEED_RESET) {
|
||||
/* a reset is required for changes to take effect */
|
||||
NVIC_SystemReset();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace esphome::nrf52
|
||||
|
||||
static int board_esphome_init() { return esphome::nrf52::board_esphome_init(); }
|
||||
|
||||
SYS_INIT(board_esphome_init, PRE_KERNEL_1, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);
|
||||
|
||||
#endif
|
||||
@@ -23,7 +23,7 @@ void PCF85063Component::dump_config() {
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
RealTimeClock::dump_config();
|
||||
ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str());
|
||||
}
|
||||
|
||||
float PCF85063Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
@@ -23,7 +23,7 @@ void PCF8563Component::dump_config() {
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
RealTimeClock::dump_config();
|
||||
ESP_LOGCONFIG(TAG, " Timezone: '%s'", this->timezone_.c_str());
|
||||
}
|
||||
|
||||
float PCF8563Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
#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);
|
||||
RealTimeClock::dump_config();
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1,35 +0,0 @@
|
||||
#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
|
||||
@@ -1,56 +0,0 @@
|
||||
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)
|
||||
@@ -61,7 +61,6 @@ void SNTPComponent::dump_config() {
|
||||
for (auto &server : this->servers_) {
|
||||
ESP_LOGCONFIG(TAG, " Server %zu: '%s'", i++, server);
|
||||
}
|
||||
RealTimeClock::dump_config();
|
||||
}
|
||||
void SNTPComponent::update() {
|
||||
#if !defined(USE_ESP32)
|
||||
|
||||
@@ -23,13 +23,6 @@ namespace time {
|
||||
static const char *const TAG = "time";
|
||||
|
||||
RealTimeClock::RealTimeClock() = default;
|
||||
|
||||
void RealTimeClock::dump_config() {
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
ESP_LOGCONFIG(TAG, "Timezone: '%s'", this->timezone_.c_str());
|
||||
#endif
|
||||
}
|
||||
|
||||
void RealTimeClock::synchronize_epoch_(uint32_t epoch) {
|
||||
ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch);
|
||||
// Update UTC epoch time.
|
||||
|
||||
@@ -52,8 +52,6 @@ class RealTimeClock : public PollingComponent {
|
||||
this->time_sync_callback_.add(std::move(callback));
|
||||
};
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
/// Report a unix epoch as current time.
|
||||
void synchronize_epoch_(uint32_t epoch);
|
||||
|
||||
@@ -42,258 +42,6 @@ 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) {
|
||||
@@ -361,28 +109,18 @@ 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;
|
||||
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->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)
|
||||
this->selected_sta_index_ = 0;
|
||||
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_();
|
||||
params = this->build_wifi_ap_from_selected_();
|
||||
}
|
||||
this->start_connecting(params, false);
|
||||
#else
|
||||
// Without fast_connect: go straight to scanning (or hidden mode if all networks are hidden)
|
||||
this->start_initial_connection_();
|
||||
this->start_scanning();
|
||||
#endif
|
||||
#ifdef USE_WIFI_AP
|
||||
} else if (this->has_ap()) {
|
||||
@@ -412,7 +150,8 @@ void WiFiComponent::restart_adapter() {
|
||||
ESP_LOGW(TAG, "Restarting adapter");
|
||||
this->wifi_mode_(false, {});
|
||||
delay(100); // NOLINT
|
||||
// Don't set retry_phase_ or num_retried_ here - state machine handles transitions
|
||||
this->num_retried_ = 0;
|
||||
this->retry_hidden_ = false;
|
||||
}
|
||||
|
||||
void WiFiComponent::loop() {
|
||||
@@ -433,19 +172,21 @@ void WiFiComponent::loop() {
|
||||
case WIFI_COMPONENT_STATE_COOLDOWN: {
|
||||
this->status_set_warning(LOG_STR("waiting to reconnect"));
|
||||
if (millis() - this->action_started_ > 5000) {
|
||||
// After cooldown, connect based on current retry phase
|
||||
#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)
|
||||
this->reset_selected_ap_to_first_if_invalid_();
|
||||
|
||||
// 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_();
|
||||
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 {
|
||||
this->start_scanning();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -603,44 +344,30 @@ void WiFiComponent::set_sta(const WiFiAP &ap) {
|
||||
this->selected_sta_index_ = 0;
|
||||
}
|
||||
|
||||
WiFiAP WiFiComponent::build_params_for_current_phase_() {
|
||||
WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const {
|
||||
// PRECONDITION: selected_sta_index_ must be valid (ensured by all callers)
|
||||
const WiFiAP *config = this->get_selected_sta_();
|
||||
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();
|
||||
}
|
||||
|
||||
assert(config != nullptr);
|
||||
WiFiAP params = *config;
|
||||
|
||||
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;
|
||||
// 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>{});
|
||||
}
|
||||
|
||||
return params;
|
||||
@@ -665,21 +392,7 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa
|
||||
}
|
||||
|
||||
void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
|
||||
// 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_)));
|
||||
|
||||
ESP_LOGI(TAG, "Connecting to '%s'", ap.get_ssid().c_str());
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
ESP_LOGV(TAG, "Connection Params:");
|
||||
ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str());
|
||||
@@ -852,39 +565,8 @@ void WiFiComponent::start_scanning() {
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING;
|
||||
}
|
||||
|
||||
/// 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
|
||||
// Helper function for WiFi scan result comparison
|
||||
// Returns true if 'a' should be placed before 'b' in the sorted order
|
||||
[[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())
|
||||
@@ -892,13 +574,21 @@ void WiFiComponent::start_scanning() {
|
||||
if (!a.get_matches() && b.get_matches())
|
||||
return false;
|
||||
|
||||
// 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();
|
||||
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();
|
||||
}
|
||||
|
||||
// Use RSSI as tiebreaker (for equal-priority matching networks or all non-matching networks)
|
||||
// Both don't match - sort by signal strength
|
||||
return a.get_rssi() > b.get_rssi();
|
||||
}
|
||||
|
||||
@@ -933,8 +623,10 @@ __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: %2u, RSSI: %3d dB, Priority: %4.1f", res.get_channel(), res.get_rssi(),
|
||||
res.get_priority());
|
||||
ESP_LOGD(TAG,
|
||||
" Channel: %u\n"
|
||||
" RSSI: %d dB",
|
||||
res.get_channel(), res.get_rssi());
|
||||
} 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())));
|
||||
@@ -983,36 +675,34 @@ 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_params_for_current_phase_() can safely use both to build connection parameters.
|
||||
// synchronized so build_wifi_ap_from_selected_() 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;
|
||||
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;
|
||||
}
|
||||
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");
|
||||
// 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
|
||||
this->retry_connect();
|
||||
return;
|
||||
}
|
||||
|
||||
yield();
|
||||
|
||||
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)
|
||||
WiFiAP params = this->build_wifi_ap_from_selected_();
|
||||
this->start_connecting(params, false);
|
||||
}
|
||||
|
||||
@@ -1034,14 +724,11 @@ 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_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());
|
||||
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());
|
||||
}
|
||||
// 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->retry_hidden_ = false;
|
||||
|
||||
this->print_connect_params_();
|
||||
|
||||
@@ -1109,340 +796,70 @@ void WiFiComponent::check_connecting_finished() {
|
||||
this->retry_connect();
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
// 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_();
|
||||
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);
|
||||
}
|
||||
|
||||
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 (!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_++;
|
||||
}
|
||||
|
||||
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();
|
||||
// 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_STA_CONNECTING_2;
|
||||
WiFiAP params = this->build_wifi_ap_from_selected_();
|
||||
this->start_connecting(params, true);
|
||||
return;
|
||||
}
|
||||
|
||||
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
|
||||
this->action_started_ = millis();
|
||||
}
|
||||
|
||||
bool WiFiComponent::can_proceed() {
|
||||
if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) {
|
||||
return true;
|
||||
}
|
||||
return this->is_connected();
|
||||
}
|
||||
void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
bool WiFiComponent::is_connected() {
|
||||
return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED &&
|
||||
|
||||
@@ -94,24 +94,6 @@ 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;
|
||||
@@ -269,6 +251,8 @@ class WiFiComponent : public Component {
|
||||
|
||||
void retry_connect();
|
||||
|
||||
bool can_proceed() override;
|
||||
|
||||
void set_reboot_timeout(uint32_t reboot_timeout);
|
||||
|
||||
bool is_connected();
|
||||
@@ -357,37 +341,8 @@ class WiFiComponent : public Component {
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
void print_connect_params_();
|
||||
WiFiAP build_params_for_current_phase_();
|
||||
WiFiAP build_wifi_ap_from_selected_() const;
|
||||
|
||||
/// 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_];
|
||||
@@ -401,15 +356,14 @@ class WiFiComponent : public Component {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
#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();
|
||||
}
|
||||
#endif
|
||||
|
||||
void wifi_loop_();
|
||||
bool wifi_mode_(optional<bool> sta, optional<bool> ap);
|
||||
@@ -489,18 +443,20 @@ class WiFiComponent : public Component {
|
||||
// Group all 8-bit values together
|
||||
WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF};
|
||||
WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE};
|
||||
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};
|
||||
|
||||
@@ -287,8 +287,6 @@
|
||||
|
||||
#ifdef USE_NRF52
|
||||
#define USE_NRF52_DFU
|
||||
#define USE_NRF52_REG0_VOUT 5
|
||||
#define USE_NRF52_UICR_ERASE
|
||||
#define USE_SOFTDEVICE_ID 7
|
||||
#define USE_SOFTDEVICE_VERSION 1
|
||||
#endif
|
||||
|
||||
@@ -1174,18 +1174,12 @@ template<class T> using ExternalRAMAllocator = RAMAllocator<T>;
|
||||
* Functions to constrain the range of arithmetic values.
|
||||
*/
|
||||
|
||||
template<typename T, typename U>
|
||||
concept comparable_with = requires(T a, U b) {
|
||||
{ a > b } -> std::convertible_to<bool>;
|
||||
{ a < b } -> std::convertible_to<bool>;
|
||||
};
|
||||
|
||||
template<std::totally_ordered T, comparable_with<T> U> T clamp_at_least(T value, U min) {
|
||||
template<std::totally_ordered T> T clamp_at_least(T value, T min) {
|
||||
if (value < min)
|
||||
return min;
|
||||
return value;
|
||||
}
|
||||
template<std::totally_ordered T, comparable_with<T> U> T clamp_at_most(T value, U max) {
|
||||
template<std::totally_ordered T> T clamp_at_most(T value, T max) {
|
||||
if (value > max)
|
||||
return max;
|
||||
return value;
|
||||
|
||||
@@ -6,7 +6,3 @@
|
||||
#ifdef USE_ARDUINO
|
||||
#include <Arduino.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
pylint==4.0.2
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.14.4 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.0 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
# Unit tests
|
||||
pytest==9.0.0
|
||||
pytest==8.4.2
|
||||
pytest-cov==7.0.0
|
||||
pytest-mock==3.15.1
|
||||
pytest-asyncio==1.3.0
|
||||
pytest-asyncio==1.2.0
|
||||
pytest-xdist==3.8.0
|
||||
asyncmock==0.4.2
|
||||
hypothesis==6.92.1
|
||||
|
||||
@@ -86,7 +86,6 @@ 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,6 +16,5 @@ display:
|
||||
|
||||
touchscreen:
|
||||
- platform: chsc6x
|
||||
i2c_id: i2c_bus
|
||||
display: ili9xxx_display
|
||||
interrupt_pin: 22
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,41 +0,0 @@
|
||||
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,4 +0,0 @@
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,4 +0,0 @@
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,4 +0,0 @@
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -700,10 +700,6 @@ 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
|
||||
|
||||
@@ -15,6 +15,3 @@ nrf52:
|
||||
inverted: true
|
||||
mode:
|
||||
output: true
|
||||
reg0:
|
||||
voltage: 2.1V
|
||||
uicr_erase: true
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
nrf52:
|
||||
reg0:
|
||||
voltage: 3.3V
|
||||
uicr_erase: true
|
||||
|
||||
@@ -5,5 +5,3 @@ nrf52:
|
||||
inverted: true
|
||||
mode:
|
||||
output: true
|
||||
reg0:
|
||||
voltage: default
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,4 +0,0 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,8 +0,0 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
- rx8130.read_time
|
||||
- rx8130.write_time
|
||||
|
||||
time:
|
||||
- platform: rx8130
|
||||
i2c_id: i2c_bus
|
||||
@@ -1,4 +0,0 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,4 +0,0 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,4 +0,0 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,4 +0,0 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -11,42 +11,26 @@ esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- speaker.mute_on:
|
||||
id: speaker_id
|
||||
- speaker.mute_off:
|
||||
id: speaker_id
|
||||
- if:
|
||||
condition:
|
||||
speaker.is_stopped:
|
||||
id: speaker_id
|
||||
condition: speaker.is_stopped
|
||||
then:
|
||||
- speaker.play:
|
||||
id: speaker_id
|
||||
data: [0, 1, 2, 3]
|
||||
- speaker.volume_set:
|
||||
id: speaker_id
|
||||
volume: 0.9
|
||||
- speaker.play: [0, 1, 2, 3]
|
||||
- speaker.volume_set: 0.9
|
||||
- if:
|
||||
condition:
|
||||
speaker.is_playing:
|
||||
id: speaker_id
|
||||
condition: speaker.is_playing
|
||||
then:
|
||||
- speaker.finish:
|
||||
id: speaker_id
|
||||
- speaker.stop:
|
||||
id: speaker_id
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Speaker Button"
|
||||
on_press:
|
||||
then:
|
||||
- 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};
|
||||
- speaker.play: [0x10, 0x20, 0x30, 0x40]
|
||||
- speaker.play: !lambda |-
|
||||
return {0x01, 0x02, (uint8_t)id(my_number).state};
|
||||
|
||||
i2s_audio:
|
||||
i2s_lrclk_pin: ${i2s_bclk_pin}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
substitutions:
|
||||
reset_pin: P0.10
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/nrf52.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,3 +1,11 @@
|
||||
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 @@
|
||||
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
|
||||
substitutions:
|
||||
tx_pin: GPIO5
|
||||
rx_pin: GPIO4
|
||||
|
||||
<<: !include common_ras2819t.yaml
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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
|
||||
substitutions:
|
||||
tx_pin: GPIO5
|
||||
rx_pin: GPIO4
|
||||
|
||||
<<: !include common_ras2819t.yaml
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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
|
||||
substitutions:
|
||||
tx_pin: GPIO5
|
||||
rx_pin: GPIO4
|
||||
|
||||
<<: !include common_ras2819t.yaml
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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
|
||||
substitutions:
|
||||
tx_pin: GPIO5
|
||||
rx_pin: GPIO4
|
||||
|
||||
<<: !include common_ras2819t.yaml
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# 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%
|
||||
@@ -1,12 +0,0 @@
|
||||
# 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%
|
||||
@@ -670,51 +670,3 @@ class TestEsphomeCore:
|
||||
os.environ.pop("ESPHOME_IS_HA_ADDON", None)
|
||||
os.environ.pop("ESPHOME_DATA_DIR", None)
|
||||
assert target.data_dir == Path(expected_default)
|
||||
|
||||
def test_web_port__none(self, target):
|
||||
"""Test web_port returns None when web_server is not configured."""
|
||||
target.config = {}
|
||||
assert target.web_port is None
|
||||
|
||||
def test_web_port__explicit_web_server_default_port(self, target):
|
||||
"""Test web_port returns 80 when web_server is explicitly configured without port."""
|
||||
target.config = {const.CONF_WEB_SERVER: {}}
|
||||
assert target.web_port == 80
|
||||
|
||||
def test_web_port__explicit_web_server_custom_port(self, target):
|
||||
"""Test web_port returns custom port when web_server is configured with port."""
|
||||
target.config = {const.CONF_WEB_SERVER: {const.CONF_PORT: 8080}}
|
||||
assert target.web_port == 8080
|
||||
|
||||
def test_web_port__ota_web_server_platform_only(self, target):
|
||||
"""
|
||||
Test web_port returns None when ota.web_server platform is explicitly configured.
|
||||
|
||||
This is a critical test for Dashboard Issue #766:
|
||||
https://github.com/esphome/dashboard/issues/766
|
||||
|
||||
When ota: platform: web_server is explicitly configured (or auto-loaded by captive_portal):
|
||||
- "web_server" appears in loaded_integrations (platform name added to integrations)
|
||||
- "ota/web_server" appears in loaded_platforms
|
||||
- But CONF_WEB_SERVER is NOT in config (only the platform is loaded, not the component)
|
||||
- web_port MUST return None (no web UI available)
|
||||
- Dashboard should NOT show VISIT button
|
||||
|
||||
This test ensures web_port only checks CONF_WEB_SERVER in config, not loaded_integrations.
|
||||
"""
|
||||
# Simulate config with ota.web_server platform but no web_server component
|
||||
# This happens when:
|
||||
# 1. User explicitly configures: ota: - platform: web_server
|
||||
# 2. OR captive_portal auto-loads ota.web_server
|
||||
target.config = {
|
||||
const.CONF_OTA: [
|
||||
{
|
||||
"platform": "web_server",
|
||||
# OTA web_server platform config would be here
|
||||
}
|
||||
],
|
||||
# Note: CONF_WEB_SERVER is NOT in config - only the OTA platform
|
||||
}
|
||||
# Even though "web_server" is in loaded_integrations due to the platform,
|
||||
# web_port must return None because the full web_server component is not configured
|
||||
assert target.web_port is None
|
||||
|
||||
Reference in New Issue
Block a user