mirror of
https://github.com/esphome/esphome.git
synced 2025-11-18 07:45:56 +00:00
Merge branch 'ble_client_automation_no_heap' into integration
This commit is contained in:
@@ -172,8 +172,7 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
|
|
||||||
* **C++ Class Pattern:**
|
* **C++ Class Pattern:**
|
||||||
```cpp
|
```cpp
|
||||||
namespace esphome {
|
namespace esphome::my_component {
|
||||||
namespace my_component {
|
|
||||||
|
|
||||||
class MyComponent : public Component {
|
class MyComponent : public Component {
|
||||||
public:
|
public:
|
||||||
@@ -189,8 +188,7 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
int param_{0};
|
int param_{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace my_component
|
} // namespace esphome::my_component
|
||||||
} // namespace esphome
|
|
||||||
```
|
```
|
||||||
|
|
||||||
* **Common Component Examples:**
|
* **Common Component Examples:**
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ esphome/components/hdc2010/* @optimusprimespace @ssieb
|
|||||||
esphome/components/he60r/* @clydebarrow
|
esphome/components/he60r/* @clydebarrow
|
||||||
esphome/components/heatpumpir/* @rob-deutsch
|
esphome/components/heatpumpir/* @rob-deutsch
|
||||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
||||||
|
esphome/components/hlk_fm22x/* @OnFreund
|
||||||
esphome/components/hm3301/* @freekode
|
esphome/components/hm3301/* @freekode
|
||||||
esphome/components/hmac_md5/* @dwmw2
|
esphome/components/hmac_md5/* @dwmw2
|
||||||
esphome/components/homeassistant/* @esphome/core @OttoWinter
|
esphome/components/homeassistant/* @esphome/core @OttoWinter
|
||||||
|
|||||||
@@ -122,16 +122,19 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
|||||||
void play_complex(const Ts &...x) override {
|
void play_complex(const Ts &...x) override {
|
||||||
this->num_running_++;
|
this->num_running_++;
|
||||||
this->var_ = std::make_tuple(x...);
|
this->var_ = std::make_tuple(x...);
|
||||||
std::vector<uint8_t> value;
|
|
||||||
|
bool result;
|
||||||
if (this->len_ >= 0) {
|
if (this->len_ >= 0) {
|
||||||
// Static mode: copy from flash to vector
|
// Static mode: write directly from flash pointer
|
||||||
value.assign(this->value_.data, this->value_.data + this->len_);
|
result = this->write(this->value_.data, this->len_);
|
||||||
} else {
|
} else {
|
||||||
// Template mode: call function
|
// Template mode: call function and write the vector
|
||||||
value = this->value_.func(x...);
|
std::vector<uint8_t> value = this->value_.func(x...);
|
||||||
|
result = this->write(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
|
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
|
||||||
if (!write(value))
|
if (!result)
|
||||||
this->play_next_(x...);
|
this->play_next_(x...);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,15 +147,15 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
|||||||
* errors.
|
* errors.
|
||||||
*/
|
*/
|
||||||
// initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event.
|
// initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event.
|
||||||
bool write(const std::vector<uint8_t> &value) {
|
bool write(const uint8_t *data, size_t len) {
|
||||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||||
esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected");
|
esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str());
|
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty(data, len).c_str());
|
||||||
esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(),
|
esp_err_t err =
|
||||||
this->char_handle_, value.size(), const_cast<uint8_t *>(value.data()),
|
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len,
|
||||||
this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
const_cast<uint8_t *>(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
|
esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
|
||||||
return false;
|
return false;
|
||||||
@@ -160,6 +163,8 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool write(const std::vector<uint8_t> &value) { return this->write(value.data(), value.size()); }
|
||||||
|
|
||||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||||
esp_ble_gattc_cb_param_t *param) override {
|
esp_ble_gattc_cb_param_t *param) override {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
|
|||||||
247
esphome/components/hlk_fm22x/__init__.py
Normal file
247
esphome/components/hlk_fm22x/__init__.py
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
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
|
||||||
21
esphome/components/hlk_fm22x/binary_sensor.py
Normal file
21
esphome/components/hlk_fm22x/binary_sensor.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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))
|
||||||
325
esphome/components/hlk_fm22x/hlk_fm22x.cpp
Normal file
325
esphome/components/hlk_fm22x/hlk_fm22x.cpp
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
#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
|
||||||
224
esphome/components/hlk_fm22x/hlk_fm22x.h
Normal file
224
esphome/components/hlk_fm22x/hlk_fm22x.h
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
#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
|
||||||
47
esphome/components/hlk_fm22x/sensor.py
Normal file
47
esphome/components/hlk_fm22x/sensor.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
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))
|
||||||
42
esphome/components/hlk_fm22x/text_sensor.py
Normal file
42
esphome/components/hlk_fm22x/text_sensor.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
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))
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
pylint==4.0.2
|
pylint==4.0.2
|
||||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
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
|
ruff==0.14.4 # also change in .pre-commit-config.yaml when updating
|
||||||
pyupgrade==3.21.0 # also change in .pre-commit-config.yaml when updating
|
pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating
|
||||||
pre-commit
|
pre-commit
|
||||||
|
|
||||||
# Unit tests
|
# Unit tests
|
||||||
pytest==8.4.2
|
pytest==9.0.0
|
||||||
pytest-cov==7.0.0
|
pytest-cov==7.0.0
|
||||||
pytest-mock==3.15.1
|
pytest-mock==3.15.1
|
||||||
pytest-asyncio==1.2.0
|
pytest-asyncio==1.3.0
|
||||||
pytest-xdist==3.8.0
|
pytest-xdist==3.8.0
|
||||||
asyncmock==0.4.2
|
asyncmock==0.4.2
|
||||||
hypothesis==6.92.1
|
hypothesis==6.92.1
|
||||||
|
|||||||
47
tests/components/hlk_fm22x/test.esp32-idf.yaml
Normal file
47
tests/components/hlk_fm22x/test.esp32-idf.yaml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
esphome:
|
||||||
|
on_boot:
|
||||||
|
then:
|
||||||
|
- hlk_fm22x.enroll:
|
||||||
|
name: "Test"
|
||||||
|
direction: 1
|
||||||
|
- hlk_fm22x.delete_all:
|
||||||
|
|
||||||
|
uart:
|
||||||
|
- id: uart_hlk_fm22x
|
||||||
|
tx_pin: 17
|
||||||
|
rx_pin: 16
|
||||||
|
baud_rate: 115200
|
||||||
|
|
||||||
|
hlk_fm22x:
|
||||||
|
on_face_scan_matched:
|
||||||
|
- logger.log: test_hlk_22x_face_scan_matched
|
||||||
|
on_face_scan_unmatched:
|
||||||
|
- logger.log: test_hlk_22x_face_scan_unmatched
|
||||||
|
on_face_scan_invalid:
|
||||||
|
- logger.log: test_hlk_22x_face_scan_invalid
|
||||||
|
on_face_info:
|
||||||
|
- logger.log: test_hlk_22x_face_info
|
||||||
|
on_enrollment_done:
|
||||||
|
- logger.log: test_hlk_22x_enrollment_done
|
||||||
|
on_enrollment_failed:
|
||||||
|
- logger.log: test_hlk_22x_enrollment_failed
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: hlk_fm22x
|
||||||
|
face_count:
|
||||||
|
name: "Face Count"
|
||||||
|
last_face_id:
|
||||||
|
name: "Last Face ID"
|
||||||
|
status:
|
||||||
|
name: "Face Status"
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: hlk_fm22x
|
||||||
|
name: "Face Enrolling"
|
||||||
|
|
||||||
|
text_sensor:
|
||||||
|
- platform: hlk_fm22x
|
||||||
|
version:
|
||||||
|
name: "HLK Version"
|
||||||
|
last_face_name:
|
||||||
|
name: "Last Face Name"
|
||||||
47
tests/components/hlk_fm22x/test.esp8266-ard.yaml
Normal file
47
tests/components/hlk_fm22x/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
esphome:
|
||||||
|
on_boot:
|
||||||
|
then:
|
||||||
|
- hlk_fm22x.enroll:
|
||||||
|
name: "Test"
|
||||||
|
direction: 1
|
||||||
|
- hlk_fm22x.delete_all:
|
||||||
|
|
||||||
|
uart:
|
||||||
|
- id: uart_hlk_fm22x
|
||||||
|
tx_pin: 4
|
||||||
|
rx_pin: 5
|
||||||
|
baud_rate: 115200
|
||||||
|
|
||||||
|
hlk_fm22x:
|
||||||
|
on_face_scan_matched:
|
||||||
|
- logger.log: test_hlk_22x_face_scan_matched
|
||||||
|
on_face_scan_unmatched:
|
||||||
|
- logger.log: test_hlk_22x_face_scan_unmatched
|
||||||
|
on_face_scan_invalid:
|
||||||
|
- logger.log: test_hlk_22x_face_scan_invalid
|
||||||
|
on_face_info:
|
||||||
|
- logger.log: test_hlk_22x_face_info
|
||||||
|
on_enrollment_done:
|
||||||
|
- logger.log: test_hlk_22x_enrollment_done
|
||||||
|
on_enrollment_failed:
|
||||||
|
- logger.log: test_hlk_22x_enrollment_failed
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: hlk_fm22x
|
||||||
|
face_count:
|
||||||
|
name: "Face Count"
|
||||||
|
last_face_id:
|
||||||
|
name: "Last Face ID"
|
||||||
|
status:
|
||||||
|
name: "Face Status"
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: hlk_fm22x
|
||||||
|
name: "Face Enrolling"
|
||||||
|
|
||||||
|
text_sensor:
|
||||||
|
- platform: hlk_fm22x
|
||||||
|
version:
|
||||||
|
name: "HLK Version"
|
||||||
|
last_face_name:
|
||||||
|
name: "Last Face Name"
|
||||||
47
tests/components/hlk_fm22x/test.rp2040-ard.yaml
Normal file
47
tests/components/hlk_fm22x/test.rp2040-ard.yaml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
esphome:
|
||||||
|
on_boot:
|
||||||
|
then:
|
||||||
|
- hlk_fm22x.enroll:
|
||||||
|
name: "Test"
|
||||||
|
direction: 1
|
||||||
|
- hlk_fm22x.delete_all:
|
||||||
|
|
||||||
|
uart:
|
||||||
|
- id: uart_hlk_fm22x
|
||||||
|
tx_pin: 4
|
||||||
|
rx_pin: 5
|
||||||
|
baud_rate: 115200
|
||||||
|
|
||||||
|
hlk_fm22x:
|
||||||
|
on_face_scan_matched:
|
||||||
|
- logger.log: test_hlk_22x_face_scan_matched
|
||||||
|
on_face_scan_unmatched:
|
||||||
|
- logger.log: test_hlk_22x_face_scan_unmatched
|
||||||
|
on_face_scan_invalid:
|
||||||
|
- logger.log: test_hlk_22x_face_scan_invalid
|
||||||
|
on_face_info:
|
||||||
|
- logger.log: test_hlk_22x_face_info
|
||||||
|
on_enrollment_done:
|
||||||
|
- logger.log: test_hlk_22x_enrollment_done
|
||||||
|
on_enrollment_failed:
|
||||||
|
- logger.log: test_hlk_22x_enrollment_failed
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: hlk_fm22x
|
||||||
|
face_count:
|
||||||
|
name: "Face Count"
|
||||||
|
last_face_id:
|
||||||
|
name: "Last Face ID"
|
||||||
|
status:
|
||||||
|
name: "Face Status"
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: hlk_fm22x
|
||||||
|
name: "Face Enrolling"
|
||||||
|
|
||||||
|
text_sensor:
|
||||||
|
- platform: hlk_fm22x
|
||||||
|
version:
|
||||||
|
name: "HLK Version"
|
||||||
|
last_face_name:
|
||||||
|
name: "Last Face Name"
|
||||||
@@ -671,44 +671,50 @@ class TestEsphomeCore:
|
|||||||
os.environ.pop("ESPHOME_DATA_DIR", None)
|
os.environ.pop("ESPHOME_DATA_DIR", None)
|
||||||
assert target.data_dir == Path(expected_default)
|
assert target.data_dir == Path(expected_default)
|
||||||
|
|
||||||
def test_platformio_cache_dir_with_env_var(self):
|
def test_web_port__none(self, target):
|
||||||
"""Test platformio_cache_dir when PLATFORMIO_CACHE_DIR env var is set."""
|
"""Test web_port returns None when web_server is not configured."""
|
||||||
target = core.EsphomeCore()
|
target.config = {}
|
||||||
test_cache_dir = "/custom/cache/dir"
|
assert target.web_port is None
|
||||||
|
|
||||||
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": test_cache_dir}):
|
def test_web_port__explicit_web_server_default_port(self, target):
|
||||||
assert target.platformio_cache_dir == test_cache_dir
|
"""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_platformio_cache_dir_without_env_var(self):
|
def test_web_port__explicit_web_server_custom_port(self, target):
|
||||||
"""Test platformio_cache_dir defaults to ~/.platformio/.cache."""
|
"""Test web_port returns custom port when web_server is configured with port."""
|
||||||
target = core.EsphomeCore()
|
target.config = {const.CONF_WEB_SERVER: {const.CONF_PORT: 8080}}
|
||||||
|
assert target.web_port == 8080
|
||||||
|
|
||||||
with patch.dict(os.environ, {}, clear=True):
|
def test_web_port__ota_web_server_platform_only(self, target):
|
||||||
# Ensure env var is not set
|
"""
|
||||||
os.environ.pop("PLATFORMIO_CACHE_DIR", None)
|
Test web_port returns None when ota.web_server platform is explicitly configured.
|
||||||
expected = os.path.expanduser("~/.platformio/.cache")
|
|
||||||
assert target.platformio_cache_dir == expected
|
|
||||||
|
|
||||||
def test_platformio_cache_dir_empty_env_var(self):
|
This is a critical test for Dashboard Issue #766:
|
||||||
"""Test platformio_cache_dir with empty env var falls back to default."""
|
https://github.com/esphome/dashboard/issues/766
|
||||||
target = core.EsphomeCore()
|
|
||||||
|
|
||||||
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": ""}):
|
When ota: platform: web_server is explicitly configured (or auto-loaded by captive_portal):
|
||||||
expected = os.path.expanduser("~/.platformio/.cache")
|
- "web_server" appears in loaded_integrations (platform name added to integrations)
|
||||||
assert target.platformio_cache_dir == expected
|
- "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
|
||||||
|
|
||||||
def test_platformio_cache_dir_whitespace_env_var(self):
|
This test ensures web_port only checks CONF_WEB_SERVER in config, not loaded_integrations.
|
||||||
"""Test platformio_cache_dir with whitespace-only env var falls back to default."""
|
"""
|
||||||
target = core.EsphomeCore()
|
# Simulate config with ota.web_server platform but no web_server component
|
||||||
|
# This happens when:
|
||||||
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": " "}):
|
# 1. User explicitly configures: ota: - platform: web_server
|
||||||
expected = os.path.expanduser("~/.platformio/.cache")
|
# 2. OR captive_portal auto-loads ota.web_server
|
||||||
assert target.platformio_cache_dir == expected
|
target.config = {
|
||||||
|
const.CONF_OTA: [
|
||||||
def test_platformio_cache_dir_docker_addon_path(self):
|
{
|
||||||
"""Test platformio_cache_dir in Docker/HA addon environment."""
|
"platform": "web_server",
|
||||||
target = core.EsphomeCore()
|
# OTA web_server platform config would be here
|
||||||
addon_cache = "/data/cache/platformio"
|
}
|
||||||
|
],
|
||||||
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": addon_cache}):
|
# Note: CONF_WEB_SERVER is NOT in config - only the OTA platform
|
||||||
assert target.platformio_cache_dir == addon_cache
|
}
|
||||||
|
# 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