1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-11 04:15:52 +00:00

Compare commits

..

31 Commits

Author SHA1 Message Date
J. Nick Koston
99bb6ead7e Merge branch 'dev' into no_batch_events 2025-11-09 22:04:58 -06:00
J. Nick Koston
d171406dff Merge branch 'dev' into no_batch_events 2025-11-09 17:35:33 -06:00
J. Nick Koston
65fd784fa7 tidy 2025-11-08 10:39:09 -06:00
J. Nick Koston
b9f208b63a [api][event] Send events immediately to prevent loss during rapid triggers 2025-11-08 10:16:18 -06:00
J. Nick Koston
fac05dab35 Merge branch 'api_event_test' into controller_registry_event_has_prog_lifetime 2025-11-08 09:46:44 -06:00
J. Nick Koston
f6100a55bc Merge branch 'controller_registry' into api_event_test 2025-11-08 09:46:43 -06:00
J. Nick Koston
c2abf363b6 Ensure event paths are enabled in api compile tests 2025-11-08 09:45:44 -06:00
J. Nick Koston
62f43d3353 dry 2025-11-08 08:41:46 -06:00
J. Nick Koston
7e96f10a79 dry 2025-11-08 08:39:23 -06:00
J. Nick Koston
6b9cb4289a Merge branch 'controller_registry' into controller_registry_event_has_prog_lifetime 2025-11-07 18:16:57 -06:00
J. Nick Koston
b264c6caac cleanup defines 2025-11-07 18:16:22 -06:00
J. Nick Koston
a6c669ff51 cleanup 2025-11-07 18:03:38 -06:00
J. Nick Koston
c15290e386 wip 2025-11-07 17:53:26 -06:00
J. Nick Koston
e3fb074a60 preen 2025-11-07 17:14:50 -06:00
J. Nick Koston
6e7f66d393 missing registry 2025-11-07 16:40:36 -06:00
J. Nick Koston
ac85949f17 cleanups 2025-11-07 16:38:32 -06:00
J. Nick Koston
0962024d99 cleanups 2025-11-07 16:35:24 -06:00
J. Nick Koston
327543303c cleanups 2025-11-07 16:34:37 -06:00
J. Nick Koston
8229e3a471 cleanups 2025-11-07 16:33:01 -06:00
J. Nick Koston
1b6471f4b0 cleanups 2025-11-07 16:30:38 -06:00
J. Nick Koston
c87d07ba70 fixes 2025-11-07 16:15:07 -06:00
J. Nick Koston
fc8dc33023 fixes 2025-11-07 16:13:59 -06:00
J. Nick Koston
c0e4f415f1 Revert "no ifdefs needed on forward decs"
This reverts commit 871c5ddb4e.
2025-11-07 16:10:56 -06:00
J. Nick Koston
871c5ddb4e no ifdefs needed on forward decs 2025-11-07 16:07:54 -06:00
J. Nick Koston
6ef2763cab controller registry 2025-11-07 16:01:45 -06:00
J. Nick Koston
929279dc23 controller registry 2025-11-07 15:55:22 -06:00
J. Nick Koston
6fa0f1e290 controller registry 2025-11-07 15:51:13 -06:00
J. Nick Koston
51eb8ea1d0 controller registry 2025-11-07 15:48:02 -06:00
J. Nick Koston
cbdd663fbf Merge remote-tracking branch 'upstream/dev' into controller_registry 2025-11-07 15:46:57 -06:00
J. Nick Koston
f1009a7468 tweak 2025-11-07 15:44:17 -06:00
J. Nick Koston
295fe8da04 controller registry phase1/2 2025-11-07 15:32:46 -06:00
47 changed files with 351 additions and 2263 deletions

View File

@@ -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:**

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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;

View File

@@ -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();

View File

@@ -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

View File

@@ -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))

View File

@@ -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

View File

@@ -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

View File

@@ -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))

View File

@@ -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))

View File

@@ -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,
@@ -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,

View File

@@ -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
"""

View File

@@ -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

View File

@@ -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):

View File

@@ -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)

View File

@@ -1,127 +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);
}
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

View File

@@ -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

View File

@@ -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)

View File

@@ -174,7 +174,7 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend(
{
cv.Optional(CONF_BSSID): cv.mac_address,
cv.Optional(CONF_HIDDEN): cv.boolean,
cv.Optional(CONF_PRIORITY, default=0): cv.int_range(min=-128, max=127),
cv.Optional(CONF_PRIORITY, default=0.0): cv.float_,
cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA,
}
)

File diff suppressed because it is too large Load Diff

View File

@@ -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;
@@ -157,7 +139,7 @@ class WiFiAP {
void set_eap(optional<EAPAuth> eap_auth);
#endif // USE_WIFI_WPA2_EAP
void set_channel(optional<uint8_t> channel);
void set_priority(int8_t priority) { priority_ = priority; }
void set_priority(float priority) { priority_ = priority; }
void set_manual_ip(optional<ManualIP> manual_ip);
void set_hidden(bool hidden);
const std::string &get_ssid() const;
@@ -167,7 +149,7 @@ class WiFiAP {
const optional<EAPAuth> &get_eap() const;
#endif // USE_WIFI_WPA2_EAP
const optional<uint8_t> &get_channel() const;
int8_t get_priority() const { return priority_; }
float get_priority() const { return priority_; }
const optional<ManualIP> &get_manual_ip() const;
bool get_hidden() const;
@@ -179,8 +161,8 @@ class WiFiAP {
optional<EAPAuth> eap_;
#endif // USE_WIFI_WPA2_EAP
optional<ManualIP> manual_ip_;
float priority_{0};
optional<uint8_t> channel_;
int8_t priority_{0};
bool hidden_{false};
};
@@ -198,17 +180,17 @@ class WiFiScanResult {
int8_t get_rssi() const;
bool get_with_auth() const;
bool get_is_hidden() const;
int8_t get_priority() const { return priority_; }
void set_priority(int8_t priority) { priority_ = priority; }
float get_priority() const { return priority_; }
void set_priority(float priority) { priority_ = priority; }
bool operator==(const WiFiScanResult &rhs) const;
protected:
bssid_t bssid_;
std::string ssid_;
float priority_{0.0f};
uint8_t channel_;
int8_t rssi_;
std::string ssid_;
int8_t priority_{0};
bool matches_{false};
bool with_auth_;
bool is_hidden_;
@@ -216,7 +198,7 @@ class WiFiScanResult {
struct WiFiSTAPriority {
bssid_t bssid;
int8_t priority;
float priority;
};
enum WiFiPowerSaveMode : uint8_t {
@@ -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();
@@ -317,14 +301,14 @@ class WiFiComponent : public Component {
}
return false;
}
int8_t get_sta_priority(const bssid_t bssid) {
float get_sta_priority(const bssid_t bssid) {
for (auto &it : this->sta_priorities_) {
if (it.bssid == bssid)
return it.priority;
}
return 0;
return 0.0f;
}
void set_sta_priority(const bssid_t bssid, int8_t priority) {
void set_sta_priority(const bssid_t bssid, float priority) {
for (auto &it : this->sta_priorities_) {
if (it.bssid == bssid) {
it.priority = priority;
@@ -357,39 +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_();
/// Clear BSSID priority tracking if all priorities are identical (saves memory)
void clear_priorities_if_all_same_();
/// 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_];
@@ -403,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);
@@ -491,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};

View File

@@ -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

View File

@@ -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",
}

View File

@@ -16,6 +16,5 @@ display:
touchscreen:
- platform: chsc6x
i2c_id: i2c_bus
display: ili9xxx_display
interrupt_pin: 22

View File

@@ -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"

View File

@@ -1,4 +0,0 @@
packages:
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
<<: !include common.yaml

View File

@@ -1,4 +0,0 @@
packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
<<: !include common.yaml

View File

@@ -1,4 +0,0 @@
packages:
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
<<: !include common.yaml

View File

@@ -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

View File

@@ -1,8 +0,0 @@
esphome:
on_boot:
- rx8130.read_time
- rx8130.write_time
time:
- platform: rx8130
i2c_id: i2c_bus

View File

@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml

View File

@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
<<: !include common.yaml

View File

@@ -1,4 +0,0 @@
packages:
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
<<: !include common.yaml

View File

@@ -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}

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -15,10 +15,5 @@ wifi:
networks:
- ssid: MySSID
password: password1
priority: 10
- ssid: MySSID2
password: password2
priority: 5
- ssid: MySSID3
password: password3
priority: 0

View File

@@ -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%

View File

@@ -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%

View File

@@ -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