Files
esphome/tests/component_tests/lis3dh/test_motion.py
T
584d4878eb [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>
2026-10-12 11:44:33 -10:00

148 lines
4.9 KiB
Python

"""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 == ""