mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[lis3dh] Add LIS3DH accelerometer component (#17393)
Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
Claude
pre-commit-ci-lite[bot]
J. Nick Koston
parent
93183c661e
commit
584d4878eb
@@ -0,0 +1,147 @@
|
||||
"""Tests for the lis3dh config cross-checks and interrupt threshold maths."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.lis3dh.motion import (
|
||||
CONF_ACCELEROMETER_ODR,
|
||||
CONF_ACCELEROMETER_RANGE,
|
||||
CONF_DURATION,
|
||||
CONF_HIGH_PASS_FILTER,
|
||||
CONF_INTERRUPT,
|
||||
CONF_OPERATING_MODE,
|
||||
CONF_THRESHOLD,
|
||||
_duration_register,
|
||||
_threshold_register,
|
||||
_validate_interrupt,
|
||||
_validate_odr_mode,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import TimePeriod
|
||||
|
||||
|
||||
def _interrupt_config(
|
||||
range_key: str,
|
||||
odr_key: str,
|
||||
threshold_g: float,
|
||||
duration_ms: int,
|
||||
high_pass: bool = True,
|
||||
) -> dict:
|
||||
return {
|
||||
CONF_ACCELEROMETER_RANGE: range_key,
|
||||
CONF_ACCELEROMETER_ODR: odr_key,
|
||||
CONF_INTERRUPT: {
|
||||
CONF_THRESHOLD: threshold_g,
|
||||
CONF_DURATION: TimePeriod(milliseconds=duration_ms),
|
||||
CONF_HIGH_PASS_FILTER: high_pass,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["NORMAL", "HIGH_RESOLUTION"])
|
||||
def test_1620hz_needs_low_power(mode: str) -> None:
|
||||
"""1620Hz exists only in low power mode."""
|
||||
config = {CONF_ACCELEROMETER_ODR: "1620HZ", CONF_OPERATING_MODE: mode}
|
||||
with pytest.raises(cv.Invalid, match="only available with"):
|
||||
_validate_odr_mode(config)
|
||||
|
||||
|
||||
def test_1344hz_is_rejected_in_low_power() -> None:
|
||||
"""The 1344Hz code runs at 5376Hz in low power mode."""
|
||||
config = {CONF_ACCELEROMETER_ODR: "1344HZ", CONF_OPERATING_MODE: "LOW_POWER"}
|
||||
with pytest.raises(cv.Invalid, match="not available with"):
|
||||
_validate_odr_mode(config)
|
||||
|
||||
|
||||
def test_1620hz_low_power_is_accepted() -> None:
|
||||
"""The one valid 1620Hz combination passes through unchanged."""
|
||||
config = {CONF_ACCELEROMETER_ODR: "1620HZ", CONF_OPERATING_MODE: "LOW_POWER"}
|
||||
assert _validate_odr_mode(config) is config
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("threshold_g", "range_key", "expected"),
|
||||
[
|
||||
(0.0, "2G", 1),
|
||||
(0.05, "16G", 1),
|
||||
(1.1, "2G", 69),
|
||||
(0.2, "2G", 13),
|
||||
(2.032, "2G", 127),
|
||||
(23.622, "16G", 127),
|
||||
],
|
||||
)
|
||||
def test_threshold_register(threshold_g: float, range_key: str, expected: int) -> None:
|
||||
"""INT1_THS is the nearest step and never zero."""
|
||||
assert _threshold_register(threshold_g, range_key) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("threshold_g", "range_key"),
|
||||
[(2.04, "2G"), (3.0, "2G"), (4.07, "4G"), (23.7, "16G")],
|
||||
)
|
||||
def test_threshold_above_range_is_rejected(threshold_g: float, range_key: str) -> None:
|
||||
"""A threshold the range cannot represent would never fire, so it fails validation."""
|
||||
config = _interrupt_config(range_key, "100HZ", threshold_g, 0)
|
||||
with pytest.raises(cv.Invalid, match="'threshold' can be at most"):
|
||||
_validate_interrupt(config)
|
||||
|
||||
|
||||
def test_threshold_within_range_is_accepted() -> None:
|
||||
"""The largest representable threshold is still valid."""
|
||||
config = _interrupt_config("2G", "100HZ", 2.032, 0)
|
||||
assert _validate_interrupt(config) is config
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("duration_ms", "odr_key", "expected"),
|
||||
[
|
||||
(0, "100HZ", 0),
|
||||
(20, "100HZ", 2),
|
||||
(1, "1620HZ", 2),
|
||||
(10000, "1HZ", 10),
|
||||
(4, "100HZ", 0),
|
||||
],
|
||||
)
|
||||
def test_duration_register(duration_ms: int, odr_key: str, expected: int) -> None:
|
||||
"""INT1_DURATION is the nearest number of samples at the data rate."""
|
||||
assert _duration_register(duration_ms, odr_key) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("duration_ms", "odr_key"), [(1280, "100HZ"), (128000, "1HZ"), (79, "1620HZ")]
|
||||
)
|
||||
def test_duration_above_register_is_rejected(duration_ms: int, odr_key: str) -> None:
|
||||
"""A duration that needs more than 127 samples fails validation."""
|
||||
config = _interrupt_config("2G", odr_key, 1.1, duration_ms)
|
||||
with pytest.raises(cv.Invalid, match="'duration' can be at most"):
|
||||
_validate_interrupt(config)
|
||||
|
||||
|
||||
def test_duration_at_register_limit_is_accepted() -> None:
|
||||
"""127 samples is the largest INT1_DURATION value."""
|
||||
config = _interrupt_config("2G", "100HZ", 1.1, 1270)
|
||||
assert _validate_interrupt(config) is config
|
||||
|
||||
|
||||
def test_sub_gravity_threshold_without_filter_warns(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Gravity alone can hold the pad asserted below 1g unless the high-pass filter is on."""
|
||||
config = _interrupt_config("2G", "100HZ", 0.5, 0, high_pass=False)
|
||||
with caplog.at_level(logging.WARNING):
|
||||
assert _validate_interrupt(config) is config
|
||||
assert "high_pass_filter: true" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("threshold_g", "high_pass"), [(1.1, False), (0.5, True)])
|
||||
def test_threshold_without_gravity_problem_is_silent(
|
||||
caplog: pytest.LogCaptureFixture, threshold_g: float, high_pass: bool
|
||||
) -> None:
|
||||
"""Above 1g, or with the filter on, no warning is logged."""
|
||||
config = _interrupt_config("2G", "100HZ", threshold_g, 0, high_pass=high_pass)
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_validate_interrupt(config)
|
||||
assert caplog.text == ""
|
||||
@@ -0,0 +1,67 @@
|
||||
motion:
|
||||
- platform: lis3dh
|
||||
id: lis3dh_main
|
||||
i2c_id: i2c_bus
|
||||
accelerometer_range: 2G
|
||||
accelerometer_odr: 100HZ
|
||||
operating_mode: HIGH_RESOLUTION
|
||||
axis_map:
|
||||
x: y
|
||||
y: x
|
||||
z: -z
|
||||
# Motion (activity) interrupt - keeps the INT pad asserted while moving so it
|
||||
# can be used as the deep_sleep wakeup_pin.
|
||||
interrupt:
|
||||
pin: INT1
|
||||
threshold: 1.1g
|
||||
duration: 0ms
|
||||
axes: [x, y, z]
|
||||
latched: true
|
||||
active_high: true
|
||||
high_pass_filter: false
|
||||
|
||||
# A second device covering the other side of each option: low power mode with
|
||||
# the rate that only exists there, routed to INT2, active low, unlatched, and
|
||||
# with the high-pass filter on.
|
||||
- platform: lis3dh
|
||||
id: lis3dh_low_power
|
||||
i2c_id: i2c_bus
|
||||
address: 0x19
|
||||
accelerometer_range: 16G
|
||||
accelerometer_odr: 1620HZ
|
||||
operating_mode: LOW_POWER
|
||||
interrupt:
|
||||
pin: INT2
|
||||
threshold: 0.05g
|
||||
duration: 1ms
|
||||
axes: [z]
|
||||
latched: false
|
||||
active_high: false
|
||||
high_pass_filter: true
|
||||
|
||||
sensor:
|
||||
- platform: lis3dh
|
||||
lis3dh_id: lis3dh_main
|
||||
type: temperature
|
||||
name: "LIS3DH Temperature"
|
||||
|
||||
- platform: motion
|
||||
motion_id: lis3dh_main
|
||||
type: acceleration_x
|
||||
name: "Accel X"
|
||||
- platform: motion
|
||||
motion_id: lis3dh_main
|
||||
type: acceleration_y
|
||||
name: "Accel Y"
|
||||
- platform: motion
|
||||
motion_id: lis3dh_main
|
||||
type: acceleration_z
|
||||
name: "Accel Z"
|
||||
- platform: motion
|
||||
motion_id: lis3dh_main
|
||||
type: pitch
|
||||
name: "Pitch"
|
||||
- platform: motion
|
||||
motion_id: lis3dh_main
|
||||
type: roll
|
||||
name: "Roll"
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
lis3dh: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
lis3dh: !include common.yaml
|
||||
Reference in New Issue
Block a user