[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:
Zebble
2026-10-12 11:44:33 -10:00
committed by GitHub
co-authored by Claude pre-commit-ci-lite[bot] J. Nick Koston
parent 93183c661e
commit 584d4878eb
11 changed files with 821 additions and 0 deletions
+147
View File
@@ -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 == ""
+67
View File
@@ -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