ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
motion_binary_sensor.cpp
Go to the documentation of this file.
2#include "esphome/core/log.h"
3#include "esphome/core/hal.h"
6
7namespace esphome::motion {
8
9ESPHOME_LOG_TAG(TAG, "motion.binary_sensor");
10
11// Thresholds used to decide the device is at rest for face_up / face_down detection.
12// While moving (shaking, being picked up) the orientation reading is dominated by
13// linear acceleration and cannot be trusted, so those sensors block (hold) instead.
14static constexpr float STILL_ACCEL_TOLERANCE = 0.12f; // max deviation of |accel| from 1g, in g
15static constexpr float STILL_GYRO_THRESHOLD = 15.0f; // max angular rate magnitude, in °/s
16static constexpr float GYRO_THRESHOLD_SCALE = 50.0f; // arbitrary gyro threshold scale, in °/s per g of acceleration
17
20
22 float ax = data.acceleration[X_AXIS];
23 float ay = data.acceleration[Y_AXIS];
24 float az = data.acceleration[Z_AXIS];
25 if (std::isnan(ax) || std::isnan(ay) || std::isnan(az))
26 return false;
27
28 // Total acceleration must be close to 1g; a larger deviation means the device is
29 // being accelerated (shaken / moved) and the gravity direction cannot be trusted.
30 float mag = std::sqrt(ax * ax + ay * ay + az * az);
31 if (std::fabs(mag - 1.0f) > STILL_ACCEL_TOLERANCE)
32 return false;
33
34 // If a gyroscope is present, also require the angular rate to be low.
35 float gx = data.angular_rate[X_AXIS];
36 float gy = data.angular_rate[Y_AXIS];
37 float gz = data.angular_rate[Z_AXIS];
38 if (!std::isnan(gx) && !std::isnan(gy) && !std::isnan(gz)) {
39 float gmag = std::sqrt(gx * gx + gy * gy + gz * gz);
40 if (gmag > STILL_GYRO_THRESHOLD)
41 return false;
42 }
43 return true;
44}
45
47 this->parent_->add_listener([this](MotionData const &data) { this->process_motion_data_(data); });
48 this->publish_state(false); // default to false until the first update
49}
50
51PROGMEM_STRING_TABLE(MotionBinarySensorTypeNames, "face_up", "face_down", "free_fall", "moving", "unknown");
52
54 LOG_BINARY_SENSOR("", "Motion Binary Sensor", this);
55 ESP_LOGCONFIG(
56 TAG,
57 " Type: %s\n"
58 " Threshold: %.3f\n"
59 " Duration: %" PRIu32 " ms",
60 LOG_STR_ARG(MotionBinarySensorTypeNames::get_log_str(this->type_, MotionBinarySensorTypeNames::LAST_INDEX)),
61 this->threshold_, this->duration_);
62}
63
66
67 switch (this->type_) {
70 // Block while the device is moving: hold the last stable state instead of
71 // reacting to transient acceleration spikes from shaking or handling.
72 if (!is_stationary(data))
73 break;
74
75 float ax = data.acceleration[X_AXIS];
76 float ay = data.acceleration[Y_AXIS];
77 float az = data.acceleration[Z_AXIS];
78 float mag = std::sqrt(ax * ax + ay * ay + az * az);
79 // is_stationary_() guarantees mag is close to 1g, so this is just a safety net.
80 if (mag < 0.1f)
81 break;
82
83 // threshold_ is the cosine of the maximum tilt: face_up / face_down are only
84 // reported when the device is within that tilt of horizontal. Beyond it, both
85 // sensors read false. Normalising by the magnitude makes the tilt limit
86 // independent of any residual acceleration.
87 float cos_tilt = az / mag;
89 this->publish_state(cos_tilt > this->threshold_);
90 } else {
91 this->publish_state(cos_tilt < -this->threshold_);
92 }
93 break;
94 }
96 float ax = data.acceleration[X_AXIS];
97 float ay = data.acceleration[Y_AXIS];
98 float az = data.acceleration[Z_AXIS];
99 if (std::isnan(ax) || std::isnan(ay) || std::isnan(az)) {
100 // Don't let a gap in valid data count towards the free-fall duration.
101 this->free_fall_candidate_ = false;
102 return;
103 }
104
105 float mag = std::sqrt(ax * ax + ay * ay + az * az);
106
107 if (mag < this->threshold_) {
108 if (!this->free_fall_candidate_) {
109 this->free_fall_candidate_ = true;
110 this->free_fall_start_time_ = now;
111 } else if (now - this->free_fall_start_time_ >= this->duration_) {
112 this->publish_state(true);
113 }
114 } else {
115 this->free_fall_candidate_ = false;
116 this->publish_state(false);
117 }
118 break;
119 }
121 float ax = data.acceleration[X_AXIS];
122 float ay = data.acceleration[Y_AXIS];
123 float az = data.acceleration[Z_AXIS];
124 float gx = data.angular_rate[X_AXIS];
125 float gy = data.angular_rate[Y_AXIS];
126 float gz = data.angular_rate[Z_AXIS];
127
128 bool moving = false;
129
130 // Check acceleration delta. Require all three axes to be valid so a NaN on any
131 // axis can't poison last_accel_ and silently stop motion detection.
132 bool accel_valid = !std::isnan(ax) && !std::isnan(ay) && !std::isnan(az);
133 if (accel_valid) {
134 if (!std::isnan(this->last_accel_[0])) {
135 float dx = ax - this->last_accel_[0];
136 float dy = ay - this->last_accel_[1];
137 float dz = az - this->last_accel_[2];
138 float accel_diff = std::sqrt(dx * dx + dy * dy + dz * dz);
139 if (accel_diff > this->threshold_) {
140 moving = true;
141 }
142 }
143 this->last_accel_[0] = ax;
144 this->last_accel_[1] = ay;
145 this->last_accel_[2] = az;
146 }
147
148 // Check angular rate delta. Require all three axes to be valid for the same reason.
149 bool gyro_valid = !std::isnan(gx) && !std::isnan(gy) && !std::isnan(gz);
150 if (gyro_valid) {
151 if (!std::isnan(this->last_gyro_[0])) {
152 float dgx = gx - this->last_gyro_[0];
153 float dgy = gy - this->last_gyro_[1];
154 float dgz = gz - this->last_gyro_[2];
155 float gyro_diff = std::sqrt(dgx * dgx + dgy * dgy + dgz * dgz);
156 if (gyro_diff > this->threshold_ * GYRO_THRESHOLD_SCALE) {
157 moving = true;
158 }
159 }
160 this->last_gyro_[0] = gx;
161 this->last_gyro_[1] = gy;
162 this->last_gyro_[2] = gz;
163 }
164
165 // With no usable data this sample, don't assert "not moving" -- just wait for
166 // the next one.
167 if (!accel_valid && !gyro_valid)
168 break;
169
170 if (moving) {
171 this->publish_state(true);
172 this->last_event_time_ = now;
173 } else {
174 if (this->state && (now - this->last_event_time_ >= this->duration_)) {
175 this->publish_state(false);
176 }
177 }
178 break;
179 }
180 }
181}
182
183} // namespace esphome::motion
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void publish_state(bool new_state)
Publish a new state to the front-end.
bool state
The current state of this binary sensor. Also used as the backing storage for StatefulEntityBase.
static bool is_stationary(const MotionData &data)
True when the device is at rest: total acceleration is close to 1g and (if a gyroscope is present) th...
MotionBinarySensor(MotionComponent *parent, MotionBinarySensorType type)
void process_motion_data_(const MotionData &data)
uint8_t type
PROGMEM_STRING_TABLE(MotionBinarySensorTypeNames, "face_up", "face_down", "free_fall", "moving", "unknown")
ESPHOME_LOG_TAG(TAG, "motion.binary_sensor")
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t