mirror of
https://github.com/esphome/esphome.git
synced 2025-11-17 23:35:47 +00:00
Compare commits
2 Commits
memory_api
...
claude/cre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23624aff09 | ||
|
|
755357b7c6 |
153
esphome/components/motion_map/README.md
Normal file
153
esphome/components/motion_map/README.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# Motion Map Component
|
||||
|
||||
## Overview
|
||||
|
||||
The Motion Map component uses Wi-Fi Channel State Information (CSI) to detect motion without cameras or microphones, providing privacy-preserving presence detection for home automation.
|
||||
|
||||
## Features
|
||||
|
||||
- **Privacy-First**: No cameras or microphones required
|
||||
- **CSI-Based Detection**: Analyzes Wi-Fi signal variations to detect movement
|
||||
- **Multiple Sensors**: Provides variance, amplitude, entropy, and skewness measurements
|
||||
- **Configurable**: Adjustable thresholds and sensitivity
|
||||
- **ESP32-S3 Optimized**: Takes advantage of ESP32-S3's CSI capabilities
|
||||
|
||||
## Platform Support
|
||||
|
||||
- **ESP32-S3** with **ESP-IDF** framework only (CSI support required)
|
||||
|
||||
## Configuration
|
||||
|
||||
### Basic Example
|
||||
|
||||
```yaml
|
||||
wifi:
|
||||
ssid: "YourSSID"
|
||||
password: "YourPassword"
|
||||
|
||||
motion_map:
|
||||
id: motion_map_component
|
||||
motion_threshold: 0.6
|
||||
idle_threshold: 0.2
|
||||
window_size: 100
|
||||
sensitivity: 1.5
|
||||
|
||||
binary_sensor:
|
||||
- platform: motion_map
|
||||
motion_map_id: motion_map_component
|
||||
name: "Motion Detected"
|
||||
|
||||
sensor:
|
||||
- platform: motion_map
|
||||
motion_map_id: motion_map_component
|
||||
variance:
|
||||
name: "CSI Variance"
|
||||
amplitude:
|
||||
name: "CSI Amplitude"
|
||||
entropy:
|
||||
name: "CSI Entropy"
|
||||
skewness:
|
||||
name: "CSI Skewness"
|
||||
```
|
||||
|
||||
### Configuration Variables
|
||||
|
||||
#### Motion Map Component
|
||||
|
||||
- **motion_threshold** (*Optional*, float): Variance threshold for detecting motion. Range: 0.0-1.0. Default: 0.5
|
||||
- **idle_threshold** (*Optional*, float): Variance threshold for detecting idle state. Range: 0.0-1.0. Default: 0.2
|
||||
- **window_size** (*Optional*, int): Number of samples for moving window analysis. Range: 10-500. Default: 100
|
||||
- **sensitivity** (*Optional*, float): Sensitivity multiplier for variance detection. Range: 0.1-5.0. Default: 1.0
|
||||
- **mac_address** (*Optional*, MAC address): Filter CSI data by specific MAC address
|
||||
|
||||
#### Binary Sensor
|
||||
|
||||
- **motion_map_id** (*Required*, ID): The ID of the motion_map component
|
||||
- All standard binary sensor options
|
||||
|
||||
#### Sensors
|
||||
|
||||
- **motion_map_id** (*Required*, ID): The ID of the motion_map component
|
||||
- **variance** (*Optional*): CSI variance sensor configuration
|
||||
- **amplitude** (*Optional*): CSI amplitude sensor configuration
|
||||
- **entropy** (*Optional*): Signal entropy sensor configuration
|
||||
- **skewness** (*Optional*): Signal skewness sensor configuration
|
||||
|
||||
Each sensor supports all standard sensor configuration options.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **CSI Capture**: The component captures Channel State Information from Wi-Fi packets
|
||||
2. **Signal Analysis**: Calculates variance and amplitude from CSI subcarrier data
|
||||
3. **Motion Detection**: Uses variance thresholds to determine motion vs. idle state
|
||||
4. **Feature Extraction**: Computes statistical features (entropy, skewness) from the signal window
|
||||
5. **State Publishing**: Updates sensors and binary sensors for Home Assistant integration
|
||||
|
||||
## Technical Details
|
||||
|
||||
### CSI (Channel State Information)
|
||||
|
||||
CSI provides detailed radio channel data that reveals how Wi-Fi signals propagate through space. When people move, they alter these propagation patterns, creating detectable electromagnetic changes.
|
||||
|
||||
### Detection Algorithm
|
||||
|
||||
The component uses a Moving Variance Segmentation algorithm:
|
||||
- Continuously calculates variance from CSI subcarrier amplitudes
|
||||
- Compares variance against configurable thresholds
|
||||
- Transitions between IDLE and MOTION states based on signal characteristics
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
- **Update Rate**: Sensors update every 1 second
|
||||
- **Memory Usage**: Window size affects RAM usage (default: 100 samples ≈ 400 bytes)
|
||||
- **CPU Usage**: CSI processing runs in Wi-Fi callback context
|
||||
|
||||
## Use Cases
|
||||
|
||||
- **Room Occupancy**: Detect presence in rooms without cameras
|
||||
- **Smart Lighting**: Trigger lights based on motion
|
||||
- **Security**: Privacy-preserving motion alerts
|
||||
- **Multi-Room Mapping**: Deploy multiple sensors for whole-home coverage
|
||||
- **Activity Recognition**: Use extracted features for ML-based activity classification
|
||||
|
||||
## Comparison with PIR Sensors
|
||||
|
||||
| Feature | Motion Map (CSI) | PIR Sensor |
|
||||
|---------|------------------|------------|
|
||||
| Privacy | High (no imaging) | High |
|
||||
| Detection Area | 360° coverage | Directional |
|
||||
| Sensitivity | Configurable | Fixed |
|
||||
| Through Walls | Limited | No |
|
||||
| Setup Complexity | Medium | Low |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### CSI Not Initializing
|
||||
|
||||
- Verify you're using ESP32-S3 with ESP-IDF framework
|
||||
- Check that Wi-Fi is enabled and connected
|
||||
- Review logs for CSI initialization errors
|
||||
|
||||
### No Motion Detection
|
||||
|
||||
- Adjust `sensitivity` parameter (try 2.0-3.0 for higher sensitivity)
|
||||
- Lower `motion_threshold` (try 0.3-0.4)
|
||||
- Increase `window_size` for more stable detection
|
||||
- Check CSI variance sensor values to verify signal capture
|
||||
|
||||
### False Positives
|
||||
|
||||
- Increase `motion_threshold` (try 0.7-0.8)
|
||||
- Increase `idle_threshold` to add hysteresis
|
||||
- Reduce `sensitivity` parameter
|
||||
- Use `mac_address` filter to focus on specific transmitter
|
||||
|
||||
## Credits
|
||||
|
||||
Inspired by the [ESPectre project](https://github.com/francescopace/espectre) by Francesco Pace.
|
||||
|
||||
## See Also
|
||||
|
||||
- [ESPHome Binary Sensor](https://esphome.io/components/binary_sensor/)
|
||||
- [ESPHome Sensor](https://esphome.io/components/sensor/)
|
||||
- [ESP32 CSI Documentation](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-guides/wifi.html#wi-fi-channel-state-information)
|
||||
87
esphome/components/motion_map/__init__.py
Normal file
87
esphome/components/motion_map/__init__.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Motion Map Component for ESPHome.
|
||||
|
||||
This component uses Wi-Fi Channel State Information (CSI) to detect motion
|
||||
without cameras or microphones, providing privacy-preserving presence detection.
|
||||
"""
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32S3,
|
||||
add_idf_sdkconfig_option,
|
||||
only_on_variant,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["esp32", "wifi"]
|
||||
AUTO_LOAD = []
|
||||
|
||||
motion_map_ns = cg.esphome_ns.namespace("motion_map")
|
||||
MotionMapComponent = motion_map_ns.class_("MotionMapComponent", cg.Component)
|
||||
|
||||
# For sub-components to reference the parent
|
||||
CONF_MOTION_MAP_ID = "motion_map_id"
|
||||
|
||||
# Configuration keys
|
||||
CONF_MOTION_THRESHOLD = "motion_threshold"
|
||||
CONF_IDLE_THRESHOLD = "idle_threshold"
|
||||
CONF_WINDOW_SIZE = "window_size"
|
||||
CONF_MAC_ADDRESS = "mac_address"
|
||||
CONF_SENSITIVITY = "sensitivity"
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.COMPONENT_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(MotionMapComponent),
|
||||
cv.Optional(CONF_MOTION_THRESHOLD, default=0.5): cv.float_range(
|
||||
min=0.0, max=1.0
|
||||
),
|
||||
cv.Optional(CONF_IDLE_THRESHOLD, default=0.2): cv.float_range(
|
||||
min=0.0, max=1.0
|
||||
),
|
||||
cv.Optional(CONF_WINDOW_SIZE, default=100): cv.int_range(
|
||||
min=10, max=500
|
||||
),
|
||||
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_SENSITIVITY, default=1.0): cv.float_range(
|
||||
min=0.1, max=5.0
|
||||
),
|
||||
}
|
||||
),
|
||||
only_on_variant(supported=[VARIANT_ESP32S3]),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
"""Generate C++ code for the motion map component."""
|
||||
# Enable CSI in ESP-IDF SDK config
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLE_CSI", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_CSI_ENABLED", True)
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(var.set_motion_threshold(config[CONF_MOTION_THRESHOLD]))
|
||||
cg.add(var.set_idle_threshold(config[CONF_IDLE_THRESHOLD]))
|
||||
cg.add(var.set_window_size(config[CONF_WINDOW_SIZE]))
|
||||
cg.add(var.set_sensitivity(config[CONF_SENSITIVITY]))
|
||||
|
||||
if CONF_MAC_ADDRESS in config:
|
||||
mac_address = config[CONF_MAC_ADDRESS].parts
|
||||
cg.add(
|
||||
var.set_mac_address(
|
||||
[
|
||||
mac_address[0],
|
||||
mac_address[1],
|
||||
mac_address[2],
|
||||
mac_address[3],
|
||||
mac_address[4],
|
||||
mac_address[5],
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
# Add ESP-IDF component dependencies
|
||||
if CORE.using_esp_idf:
|
||||
cg.add_library("esp_wifi", None)
|
||||
35
esphome/components/motion_map/binary_sensor.py
Normal file
35
esphome/components/motion_map/binary_sensor.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Binary sensor platform for Motion Map component."""
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_MOTION,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
)
|
||||
|
||||
from . import CONF_MOTION_MAP_ID, MotionMapComponent, motion_map_ns
|
||||
|
||||
DEPENDENCIES = ["motion_map"]
|
||||
|
||||
MotionMapBinarySensor = motion_map_ns.class_(
|
||||
"MotionMapBinarySensor", binary_sensor.BinarySensor, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(
|
||||
MotionMapBinarySensor,
|
||||
device_class=DEVICE_CLASS_MOTION,
|
||||
).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_MOTION_MAP_ID): cv.use_id(MotionMapComponent),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
"""Generate code for the motion binary sensor."""
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
parent = await cg.get_variable(config[CONF_MOTION_MAP_ID])
|
||||
cg.add(parent.set_motion_binary_sensor(var))
|
||||
305
esphome/components/motion_map/motion_map.cpp
Normal file
305
esphome/components/motion_map/motion_map.cpp
Normal file
@@ -0,0 +1,305 @@
|
||||
#include "motion_map.h"
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace motion_map {
|
||||
|
||||
static const char *const TAG = "motion_map";
|
||||
|
||||
void MotionMapComponent::setup() {
|
||||
// Reserve space for variance window
|
||||
this->variance_window_.reserve(this->window_size_);
|
||||
|
||||
// Initialize CSI capture
|
||||
this->init_csi_();
|
||||
}
|
||||
|
||||
void MotionMapComponent::loop() {
|
||||
// Process new CSI data if available
|
||||
if (this->new_csi_data_) {
|
||||
this->new_csi_data_ = false;
|
||||
this->process_csi_data_();
|
||||
}
|
||||
|
||||
// Periodic sensor publishing (every 1 second)
|
||||
uint32_t now = millis();
|
||||
if (now - this->last_update_time_ >= 1000) {
|
||||
this->publish_sensors_();
|
||||
this->last_update_time_ = now;
|
||||
}
|
||||
}
|
||||
|
||||
void MotionMapComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Motion Map:");
|
||||
ESP_LOGCONFIG(TAG, " Motion Threshold: %.2f\n Idle Threshold: %.2f\n Window Size: %u\n Sensitivity: %.2f",
|
||||
this->motion_threshold_, this->idle_threshold_, this->window_size_, this->sensitivity_);
|
||||
if (this->mac_address_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " MAC Filter: %02X:%02X:%02X:%02X:%02X:%02X", (*this->mac_address_)[0],
|
||||
(*this->mac_address_)[1], (*this->mac_address_)[2], (*this->mac_address_)[3],
|
||||
(*this->mac_address_)[4], (*this->mac_address_)[5]);
|
||||
}
|
||||
if (!this->csi_initialized_) {
|
||||
ESP_LOGW(TAG, "CSI not initialized");
|
||||
}
|
||||
}
|
||||
|
||||
void MotionMapComponent::init_csi_() {
|
||||
// Configure CSI
|
||||
wifi_csi_config_t csi_config = {};
|
||||
csi_config.lltf_en = true; // Enable Long Training Field
|
||||
csi_config.htltf_en = true; // Enable HT Long Training Field
|
||||
csi_config.stbc_htltf2_en = false; // Disable STBC HT-LTF2
|
||||
csi_config.ltf_merge_en = true; // Merge LTF
|
||||
csi_config.channel_filter_en = true; // Enable channel filter
|
||||
csi_config.manu_scale = false; // Auto scale
|
||||
csi_config.shift = 0; // No shift
|
||||
|
||||
esp_err_t err = esp_wifi_set_csi_config(&csi_config);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set CSI config: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
// Register CSI callback
|
||||
err = esp_wifi_set_csi_rx_cb(MotionMapComponent::csi_callback_, this);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to set CSI RX callback: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable CSI
|
||||
err = esp_wifi_set_csi(true);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to enable CSI: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
this->csi_initialized_ = true;
|
||||
ESP_LOGD(TAG, "CSI initialized");
|
||||
}
|
||||
|
||||
void MotionMapComponent::csi_callback_(void *ctx, wifi_csi_info_t *info) {
|
||||
auto *component = static_cast<MotionMapComponent *>(ctx);
|
||||
if (component == nullptr || info == nullptr || info->buf == nullptr || info->len == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy CSI data to buffer for processing in main loop
|
||||
// This callback runs in WiFi task context
|
||||
size_t len = std::min(static_cast<size_t>(info->len), MAX_CSI_LEN);
|
||||
memcpy(component->csi_buffer_.data.data(), info->buf, len);
|
||||
component->csi_buffer_.len = len;
|
||||
memcpy(component->csi_buffer_.mac.data(), info->mac, 6);
|
||||
component->csi_buffer_.valid = true;
|
||||
component->new_csi_data_ = true;
|
||||
}
|
||||
|
||||
void MotionMapComponent::process_csi_data_() {
|
||||
if (!this->csi_buffer_.valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter by MAC address if configured
|
||||
if (this->mac_address_.has_value()) {
|
||||
bool mac_match = true;
|
||||
for (size_t i = 0; i < 6; i++) {
|
||||
if (this->csi_buffer_.mac[i] != (*this->mac_address_)[i]) {
|
||||
mac_match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!mac_match) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract CSI data from buffer
|
||||
const int8_t *csi_data = this->csi_buffer_.data.data();
|
||||
size_t csi_len = this->csi_buffer_.len;
|
||||
|
||||
// Calculate variance and amplitude
|
||||
float variance = this->calculate_variance_(csi_data, csi_len);
|
||||
float amplitude = this->calculate_amplitude_(csi_data, csi_len);
|
||||
|
||||
// Apply sensitivity scaling
|
||||
variance *= this->sensitivity_;
|
||||
|
||||
// Update moving window
|
||||
this->variance_window_.push_back(variance);
|
||||
if (this->variance_window_.size() > this->window_size_) {
|
||||
this->variance_window_.erase(this->variance_window_.begin());
|
||||
}
|
||||
|
||||
// Store current values
|
||||
this->current_variance_ = variance;
|
||||
this->current_amplitude_ = amplitude;
|
||||
|
||||
// Update motion state
|
||||
this->update_motion_state_(variance);
|
||||
}
|
||||
|
||||
float MotionMapComponent::calculate_variance_(const int8_t *data, size_t len) {
|
||||
if (len == 0)
|
||||
return 0.0f;
|
||||
|
||||
// Calculate mean
|
||||
float sum = 0.0f;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
sum += static_cast<float>(data[i]);
|
||||
}
|
||||
float mean = sum / static_cast<float>(len);
|
||||
|
||||
// Calculate variance
|
||||
float variance = 0.0f;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
float diff = static_cast<float>(data[i]) - mean;
|
||||
variance += diff * diff;
|
||||
}
|
||||
variance /= static_cast<float>(len);
|
||||
|
||||
return variance;
|
||||
}
|
||||
|
||||
float MotionMapComponent::calculate_amplitude_(const int8_t *data, size_t len) {
|
||||
if (len == 0)
|
||||
return 0.0f;
|
||||
|
||||
// Calculate RMS amplitude
|
||||
float sum_sq = 0.0f;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
float val = static_cast<float>(data[i]);
|
||||
sum_sq += val * val;
|
||||
}
|
||||
return sqrtf(sum_sq / static_cast<float>(len));
|
||||
}
|
||||
|
||||
float MotionMapComponent::calculate_entropy_() {
|
||||
if (this->variance_window_.empty())
|
||||
return 0.0f;
|
||||
|
||||
// Simple entropy calculation using histogram
|
||||
const int num_bins = 10;
|
||||
std::array<int, num_bins> histogram = {};
|
||||
|
||||
// Find min/max for binning
|
||||
float min_val = this->variance_window_[0];
|
||||
float max_val = this->variance_window_[0];
|
||||
for (float val : this->variance_window_) {
|
||||
min_val = std::min(min_val, val);
|
||||
max_val = std::max(max_val, val);
|
||||
}
|
||||
|
||||
float range = max_val - min_val;
|
||||
if (range < 0.0001f)
|
||||
return 0.0f;
|
||||
|
||||
// Build histogram
|
||||
for (float val : this->variance_window_) {
|
||||
int bin = static_cast<int>(((val - min_val) / range) * (num_bins - 1));
|
||||
bin = std::max(0, std::min(num_bins - 1, bin));
|
||||
histogram[bin]++;
|
||||
}
|
||||
|
||||
// Calculate entropy
|
||||
float entropy = 0.0f;
|
||||
float total = static_cast<float>(this->variance_window_.size());
|
||||
for (int count : histogram) {
|
||||
if (count > 0) {
|
||||
float p = static_cast<float>(count) / total;
|
||||
entropy -= p * logf(p);
|
||||
}
|
||||
}
|
||||
|
||||
return entropy;
|
||||
}
|
||||
|
||||
float MotionMapComponent::calculate_skewness_() {
|
||||
if (this->variance_window_.size() < 3)
|
||||
return 0.0f;
|
||||
|
||||
// Calculate mean
|
||||
float sum = 0.0f;
|
||||
for (float val : this->variance_window_) {
|
||||
sum += val;
|
||||
}
|
||||
float mean = sum / static_cast<float>(this->variance_window_.size());
|
||||
|
||||
// Calculate standard deviation and skewness
|
||||
float variance = 0.0f;
|
||||
float skewness_sum = 0.0f;
|
||||
for (float val : this->variance_window_) {
|
||||
float diff = val - mean;
|
||||
variance += diff * diff;
|
||||
skewness_sum += diff * diff * diff;
|
||||
}
|
||||
|
||||
float n = static_cast<float>(this->variance_window_.size());
|
||||
variance /= n;
|
||||
float std_dev = sqrtf(variance);
|
||||
|
||||
if (std_dev < 0.0001f)
|
||||
return 0.0f;
|
||||
|
||||
float skewness = (skewness_sum / n) / (std_dev * std_dev * std_dev);
|
||||
return skewness;
|
||||
}
|
||||
|
||||
void MotionMapComponent::update_motion_state_(float variance) {
|
||||
MotionState new_state = this->current_state_;
|
||||
|
||||
// Simple threshold-based state machine
|
||||
if (this->current_state_ == MotionState::IDLE) {
|
||||
if (variance > this->motion_threshold_) {
|
||||
new_state = MotionState::MOTION;
|
||||
}
|
||||
} else { // MOTION
|
||||
if (variance < this->idle_threshold_) {
|
||||
new_state = MotionState::IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
// Update state if changed
|
||||
if (new_state != this->current_state_) {
|
||||
this->current_state_ = new_state;
|
||||
ESP_LOGV(TAG, "State: %s", new_state == MotionState::MOTION ? "MOTION" : "IDLE");
|
||||
|
||||
// Publish binary sensor immediately on state change
|
||||
if (this->motion_binary_sensor_ != nullptr) {
|
||||
this->motion_binary_sensor_->publish_state(new_state == MotionState::MOTION);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MotionMapComponent::publish_sensors_() {
|
||||
// Publish variance sensor
|
||||
if (this->variance_sensor_ != nullptr) {
|
||||
this->variance_sensor_->publish_state(this->current_variance_);
|
||||
}
|
||||
|
||||
// Publish amplitude sensor
|
||||
if (this->amplitude_sensor_ != nullptr) {
|
||||
this->amplitude_sensor_->publish_state(this->current_amplitude_);
|
||||
}
|
||||
|
||||
// Publish entropy sensor
|
||||
if (this->entropy_sensor_ != nullptr && !this->variance_window_.empty()) {
|
||||
float entropy = this->calculate_entropy_();
|
||||
this->entropy_sensor_->publish_state(entropy);
|
||||
}
|
||||
|
||||
// Publish skewness sensor
|
||||
if (this->skewness_sensor_ != nullptr && this->variance_window_.size() >= 3) {
|
||||
float skewness = this->calculate_skewness_();
|
||||
this->skewness_sensor_->publish_state(skewness);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace motion_map
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP_IDF
|
||||
128
esphome/components/motion_map/motion_map.h
Normal file
128
esphome/components/motion_map/motion_map.h
Normal file
@@ -0,0 +1,128 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
#include "esp_wifi.h"
|
||||
#include "esp_wifi_types.h"
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// Forward declarations
|
||||
namespace binary_sensor {
|
||||
class BinarySensor;
|
||||
}
|
||||
namespace sensor {
|
||||
class Sensor;
|
||||
}
|
||||
|
||||
namespace motion_map {
|
||||
|
||||
/// Motion state enumeration
|
||||
enum class MotionState : uint8_t {
|
||||
IDLE = 0,
|
||||
MOTION = 1,
|
||||
};
|
||||
|
||||
/// Maximum CSI buffer size for ESP32-S3
|
||||
static constexpr size_t MAX_CSI_LEN = 384;
|
||||
|
||||
/// CSI data buffer for cross-task communication
|
||||
struct CSIDataBuffer {
|
||||
std::array<int8_t, MAX_CSI_LEN> data;
|
||||
size_t len{0};
|
||||
std::array<uint8_t, 6> mac;
|
||||
bool valid{false};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Motion Map Component using Wi-Fi CSI for motion detection
|
||||
*
|
||||
* This component captures Channel State Information (CSI) from Wi-Fi packets
|
||||
* and analyzes signal variations to detect motion without cameras or microphones.
|
||||
*/
|
||||
class MotionMapComponent : public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
// Configuration setters
|
||||
void set_motion_threshold(float threshold) { this->motion_threshold_ = threshold; }
|
||||
void set_idle_threshold(float threshold) { this->idle_threshold_ = threshold; }
|
||||
void set_window_size(uint32_t size) { this->window_size_ = size; }
|
||||
void set_sensitivity(float sensitivity) { this->sensitivity_ = sensitivity; }
|
||||
void set_mac_address(const std::array<uint8_t, 6> &mac) { this->mac_address_ = mac; }
|
||||
|
||||
// Sensor setters
|
||||
void set_motion_binary_sensor(binary_sensor::BinarySensor *sensor) { this->motion_binary_sensor_ = sensor; }
|
||||
void set_variance_sensor(sensor::Sensor *sensor) { this->variance_sensor_ = sensor; }
|
||||
void set_amplitude_sensor(sensor::Sensor *sensor) { this->amplitude_sensor_ = sensor; }
|
||||
void set_entropy_sensor(sensor::Sensor *sensor) { this->entropy_sensor_ = sensor; }
|
||||
void set_skewness_sensor(sensor::Sensor *sensor) { this->skewness_sensor_ = sensor; }
|
||||
|
||||
protected:
|
||||
/// Initialize CSI capture
|
||||
void init_csi_();
|
||||
|
||||
/// CSI callback (static wrapper for ESP-IDF) - runs in WiFi task
|
||||
static void csi_callback_(void *ctx, wifi_csi_info_t *info);
|
||||
|
||||
/// Process CSI data in main loop
|
||||
void process_csi_data_();
|
||||
|
||||
/// Calculate variance from CSI data
|
||||
float calculate_variance_(const int8_t *data, size_t len);
|
||||
|
||||
/// Calculate amplitude from CSI data
|
||||
float calculate_amplitude_(const int8_t *data, size_t len);
|
||||
|
||||
/// Calculate entropy from variance window
|
||||
float calculate_entropy_();
|
||||
|
||||
/// Calculate skewness from variance window
|
||||
float calculate_skewness_();
|
||||
|
||||
/// Update motion state based on current variance
|
||||
void update_motion_state_(float variance);
|
||||
|
||||
/// Publish sensor values
|
||||
void publish_sensors_();
|
||||
|
||||
// Configuration parameters
|
||||
float motion_threshold_{0.5f};
|
||||
float idle_threshold_{0.2f};
|
||||
uint32_t window_size_{100};
|
||||
float sensitivity_{1.0f};
|
||||
optional<std::array<uint8_t, 6>> mac_address_;
|
||||
|
||||
// Sensors
|
||||
binary_sensor::BinarySensor *motion_binary_sensor_{nullptr};
|
||||
sensor::Sensor *variance_sensor_{nullptr};
|
||||
sensor::Sensor *amplitude_sensor_{nullptr};
|
||||
sensor::Sensor *entropy_sensor_{nullptr};
|
||||
sensor::Sensor *skewness_sensor_{nullptr};
|
||||
|
||||
// Runtime state
|
||||
MotionState current_state_{MotionState::IDLE};
|
||||
std::vector<float> variance_window_;
|
||||
float current_variance_{0.0f};
|
||||
float current_amplitude_{0.0f};
|
||||
uint32_t last_update_time_{0};
|
||||
bool csi_initialized_{false};
|
||||
|
||||
// CSI data buffer (written by WiFi task, read by main loop)
|
||||
CSIDataBuffer csi_buffer_;
|
||||
volatile bool new_csi_data_{false};
|
||||
};
|
||||
|
||||
} // namespace motion_map
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP_IDF
|
||||
70
esphome/components/motion_map/sensor.py
Normal file
70
esphome/components/motion_map/sensor.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""Sensor platform for Motion Map component - CSI feature sensors."""
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
from . import CONF_MOTION_MAP_ID, MotionMapComponent, motion_map_ns
|
||||
|
||||
DEPENDENCIES = ["motion_map"]
|
||||
|
||||
# Sensor types for CSI features
|
||||
CONF_VARIANCE = "variance"
|
||||
CONF_AMPLITUDE = "amplitude"
|
||||
CONF_ENTROPY = "entropy"
|
||||
CONF_SKEWNESS = "skewness"
|
||||
|
||||
MotionMapSensor = motion_map_ns.class_(
|
||||
"MotionMapSensor", sensor.Sensor, cg.Component
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_MOTION_MAP_ID): cv.use_id(MotionMapComponent),
|
||||
cv.Optional(CONF_VARIANCE): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_AMPLITUDE): sensor.sensor_schema(
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_ENTROPY): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_SKEWNESS): sensor.sensor_schema(
|
||||
accuracy_decimals=3,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
"""Generate code for the motion map sensors."""
|
||||
parent = await cg.get_variable(config[CONF_MOTION_MAP_ID])
|
||||
|
||||
if variance_config := config.get(CONF_VARIANCE):
|
||||
sens = await sensor.new_sensor(variance_config)
|
||||
cg.add(parent.set_variance_sensor(sens))
|
||||
|
||||
if amplitude_config := config.get(CONF_AMPLITUDE):
|
||||
sens = await sensor.new_sensor(amplitude_config)
|
||||
cg.add(parent.set_amplitude_sensor(sens))
|
||||
|
||||
if entropy_config := config.get(CONF_ENTROPY):
|
||||
sens = await sensor.new_sensor(entropy_config)
|
||||
cg.add(parent.set_entropy_sensor(sens))
|
||||
|
||||
if skewness_config := config.get(CONF_SKEWNESS):
|
||||
sens = await sensor.new_sensor(skewness_config)
|
||||
cg.add(parent.set_skewness_sensor(sens))
|
||||
27
tests/components/motion_map/common.yaml
Normal file
27
tests/components/motion_map/common.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
motion_map:
|
||||
id: motion_map_component
|
||||
motion_threshold: 0.6
|
||||
idle_threshold: 0.2
|
||||
window_size: 100
|
||||
sensitivity: 1.5
|
||||
|
||||
binary_sensor:
|
||||
- platform: motion_map
|
||||
motion_map_id: motion_map_component
|
||||
name: "Motion Detected"
|
||||
|
||||
sensor:
|
||||
- platform: motion_map
|
||||
motion_map_id: motion_map_component
|
||||
variance:
|
||||
name: "CSI Variance"
|
||||
amplitude:
|
||||
name: "CSI Amplitude"
|
||||
entropy:
|
||||
name: "CSI Entropy"
|
||||
skewness:
|
||||
name: "CSI Skewness"
|
||||
1
tests/components/motion_map/test.esp32-s3-idf.yaml
Normal file
1
tests/components/motion_map/test.esp32-s3-idf.yaml
Normal file
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
Reference in New Issue
Block a user