diff --git a/esphome/components/motion_map/README.md b/esphome/components/motion_map/README.md new file mode 100644 index 0000000000..fc250ec940 --- /dev/null +++ b/esphome/components/motion_map/README.md @@ -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) diff --git a/esphome/components/motion_map/__init__.py b/esphome/components/motion_map/__init__.py new file mode 100644 index 0000000000..b05e57bf21 --- /dev/null +++ b/esphome/components/motion_map/__init__.py @@ -0,0 +1,88 @@ +"""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 + ), + } + ), + cv.only_with_esp_idf, + 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) diff --git a/esphome/components/motion_map/binary_sensor.py b/esphome/components/motion_map/binary_sensor.py new file mode 100644 index 0000000000..6237506c22 --- /dev/null +++ b/esphome/components/motion_map/binary_sensor.py @@ -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)) diff --git a/esphome/components/motion_map/motion_map.cpp b/esphome/components/motion_map/motion_map.cpp new file mode 100644 index 0000000000..38f9385cb4 --- /dev/null +++ b/esphome/components/motion_map/motion_map.cpp @@ -0,0 +1,292 @@ +#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() { + ESP_LOGCONFIG(TAG, "Setting up Motion Map..."); + + // Reserve space for variance window + this->variance_window_.reserve(this->window_size_); + + // Initialize CSI capture + this->init_csi_(); +} + +void MotionMapComponent::loop() { + // 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", this->motion_threshold_); + ESP_LOGCONFIG(TAG, " Idle Threshold: %.2f", this->idle_threshold_); + ESP_LOGCONFIG(TAG, " Window Size: %u", this->window_size_); + ESP_LOGCONFIG(TAG, " Sensitivity: %.2f", 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]); + } + ESP_LOGCONFIG(TAG, " CSI Initialized: %s", this->csi_initialized_ ? "YES" : "NO"); +} + +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_LOGI(TAG, "CSI initialized successfully"); +} + +void MotionMapComponent::csi_callback_(void *ctx, wifi_csi_info_t *info) { + auto *component = static_cast(ctx); + if (component != nullptr && info != nullptr) { + component->process_csi_(info); + } +} + +void MotionMapComponent::process_csi_(wifi_csi_info_t *info) { + // 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 (info->mac[i] != (*this->mac_address_)[i]) { + mac_match = false; + break; + } + } + if (!mac_match) { + return; + } + } + + // Extract CSI data + const int8_t *csi_data = info->buf; + size_t csi_len = info->len; + + if (csi_data == nullptr || csi_len == 0) { + return; + } + + // 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(data[i]); + } + float mean = sum / static_cast(len); + + // Calculate variance + float variance = 0.0f; + for (size_t i = 0; i < len; i++) { + float diff = static_cast(data[i]) - mean; + variance += diff * diff; + } + variance /= static_cast(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(data[i]); + sum_sq += val * val; + } + return sqrtf(sum_sq / static_cast(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 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(((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(this->variance_window_.size()); + for (int count : histogram) { + if (count > 0) { + float p = static_cast(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(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(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_LOGD(TAG, "Motion state changed: %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 diff --git a/esphome/components/motion_map/motion_map.h b/esphome/components/motion_map/motion_map.h new file mode 100644 index 0000000000..c67d453ac3 --- /dev/null +++ b/esphome/components/motion_map/motion_map.h @@ -0,0 +1,125 @@ +#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 +#include +#include + +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, +}; + +/// CSI data structure for storing subcarrier amplitude variance +struct CSIData { + float variance{0.0f}; + float amplitude{0.0f}; + uint32_t timestamp{0}; +}; + +/** + * @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 &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) + static void csi_callback_(void *ctx, wifi_csi_info_t *info); + + /// Process CSI data + void process_csi_(wifi_csi_info_t *info); + + /// 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> 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 variance_window_; + float current_variance_{0.0f}; + float current_amplitude_{0.0f}; + uint32_t last_update_time_{0}; + bool csi_initialized_{false}; + + // Statistics for moving variance + float moving_sum_{0.0f}; + float moving_sum_sq_{0.0f}; + uint32_t sample_count_{0}; +}; + +} // namespace motion_map +} // namespace esphome + +#endif // USE_ESP_IDF diff --git a/esphome/components/motion_map/sensor.py b/esphome/components/motion_map/sensor.py new file mode 100644 index 0000000000..71d6ca09d9 --- /dev/null +++ b/esphome/components/motion_map/sensor.py @@ -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)) diff --git a/tests/components/motion_map/common.yaml b/tests/components/motion_map/common.yaml new file mode 100644 index 0000000000..219ff3793c --- /dev/null +++ b/tests/components/motion_map/common.yaml @@ -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" diff --git a/tests/components/motion_map/test.esp32-s3-idf.yaml b/tests/components/motion_map/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/motion_map/test.esp32-s3-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml