mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
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>
148 lines
4.9 KiB
Python
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 == ""
|