mirror of
https://github.com/esphome/esphome.git
synced 2026-10-18 12:12:48 +00:00
[core] Parse unit values as one decimal so listed floats match, fixing at581x power consumption (#20889)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
J. Nick Koston
parent
6fa7b87e6a
commit
10d3ced897
@@ -93,7 +93,7 @@ def _megahertz(value: float) -> int:
|
||||
|
||||
|
||||
def _microamps(value: float) -> int:
|
||||
return int(value * 1000000)
|
||||
return round(value * 1000000)
|
||||
|
||||
|
||||
RADAR_SETTINGS_SCHEMA = cv.Schema(
|
||||
|
||||
@@ -16,6 +16,7 @@ from ipaddress import (
|
||||
ip_network,
|
||||
)
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
@@ -1416,12 +1417,16 @@ def float_with_unit(quantity, regex_suffix, optional_unit=False):
|
||||
if match is None:
|
||||
raise Invalid(f"Expected {quantity} with unit, got {value}")
|
||||
|
||||
mantissa = float(match.group(1))
|
||||
if match.group(2) not in METRIC_SUFFIXES:
|
||||
raise Invalid(f"Invalid {quantity} suffix {match.group(2)}")
|
||||
|
||||
multiplier = METRIC_SUFFIXES[match.group(2)]
|
||||
return mantissa * multiplier
|
||||
# Parse the scaled decimal in one go: "91uA" is then the same float as the
|
||||
# literal 91e-6, which a product of two floats is not always.
|
||||
exponent = round(math.log10(METRIC_SUFFIXES[match.group(2)]))
|
||||
try:
|
||||
return float(f"{match.group(1)}e{exponent}")
|
||||
except ValueError as err:
|
||||
raise Invalid(f"Expected {quantity} with unit, got {value}") from err
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
"""Tests for at581x power_consumption validation."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.at581x import CONF_POWER_CONSUMPTION, RADAR_SETTINGS_SCHEMA
|
||||
from esphome.const import CONF_FREQUENCY, CONF_ID
|
||||
from esphome.core import Lambda
|
||||
from esphome.types import ConfigType
|
||||
|
||||
ALLOWED_UA = [48, 56, 63, 70, 77, 91, 105, 115, 40, 44, 47, 51, 54, 61, 68, 78]
|
||||
|
||||
|
||||
def _settings(**extra: object) -> ConfigType:
|
||||
return RADAR_SETTINGS_SCHEMA(
|
||||
{CONF_ID: "waveradar", CONF_FREQUENCY: "5800MHz", **extra}
|
||||
)
|
||||
|
||||
|
||||
def _power(value: object) -> object:
|
||||
return _settings(**{CONF_POWER_CONSUMPTION: value})[CONF_POWER_CONSUMPTION]
|
||||
|
||||
|
||||
UNIT_SPELLINGS = [
|
||||
lambda ua: f"{ua}uA",
|
||||
lambda ua: f"{ua}µA",
|
||||
lambda ua: f"{ua} uA",
|
||||
lambda ua: f"{Decimal(ua) / 1000}mA",
|
||||
lambda ua: f"{Decimal(ua) / 1000000}A",
|
||||
lambda ua: f"{ua * 1000}nA",
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("spell", UNIT_SPELLINGS)
|
||||
@pytest.mark.parametrize("microamps", ALLOWED_UA)
|
||||
def test_every_listed_value_in_every_unit(
|
||||
spell: Callable[[int], str], microamps: int
|
||||
) -> None:
|
||||
result = _power(spell(microamps))
|
||||
assert result == microamps
|
||||
assert type(result) is int
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("91uA", 91),
|
||||
("105uA", 105),
|
||||
("115uA", 115),
|
||||
("40uA", 40),
|
||||
("0.07mA", 70),
|
||||
("0.091mA", 91),
|
||||
("0.000091A", 91),
|
||||
("0.000000091kA", 91),
|
||||
("+91uA", 91),
|
||||
],
|
||||
)
|
||||
def test_previously_broken_and_equivalent_spellings(value: str, expected: int) -> None:
|
||||
assert _power(value) == expected
|
||||
|
||||
|
||||
def test_default_is_70() -> None:
|
||||
assert _settings()[CONF_POWER_CONSUMPTION] == 70
|
||||
|
||||
|
||||
def test_lambda_is_passed_through() -> None:
|
||||
assert isinstance(_power(Lambda("return 70;")), Lambda)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
[
|
||||
# Near-miss values must not be rounded onto a supported setting.
|
||||
"91.00000000001uA",
|
||||
"90.99999999999uA",
|
||||
"91.00000000009uA",
|
||||
"91.0000000001uA",
|
||||
"90.9999999999uA",
|
||||
"91.000000001uA",
|
||||
"91.0001uA",
|
||||
"91.4uA",
|
||||
"0.09100000000001mA",
|
||||
],
|
||||
)
|
||||
def test_fractional_microamps_are_rejected(value: str) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
_power(value)
|
||||
|
||||
|
||||
def test_sub_ulp_digits_are_indistinguishable_parse_noise() -> None:
|
||||
# 1e-15 is below half an ulp of 91.0, so the mantissa parses to exactly 91.0.
|
||||
assert float("91.000000000000001") == 91.0
|
||||
assert _power("91.000000000000001uA") == 91
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
["92uA", "0uA", "-91uA", "-0.091mA", "9" * 400 + "uA", "infuA", "nanuA", "nan"],
|
||||
)
|
||||
def test_unsupported_and_nonfinite_are_rejected(value: str) -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
_power(value)
|
||||
@@ -17,6 +17,10 @@ esphome:
|
||||
frequency: !lambda "return 5800;"
|
||||
poweron_selfcheck_time: !lambda "return 2000;"
|
||||
power_consumption: !lambda "return 70;"
|
||||
- at581x.settings:
|
||||
id: waveradar
|
||||
frequency: 5800MHz
|
||||
power_consumption: 91uA
|
||||
- at581x.reset:
|
||||
id: waveradar
|
||||
|
||||
|
||||
@@ -2101,6 +2101,18 @@ def test_float_with_unit_with_suffix() -> None:
|
||||
assert cv.frequency("10kHz") == 10000.0
|
||||
|
||||
|
||||
def test_float_with_unit_matches_the_literal() -> None:
|
||||
"""The scaled decimal is parsed once, so it equals the same value written as a literal."""
|
||||
assert cv.current("91uA") == 91e-6
|
||||
assert cv.current("0.091mA") == 91e-6
|
||||
assert cv.frequency("5.8GHz") == 5.8e9
|
||||
|
||||
|
||||
def test_float_with_unit_empty_mantissa() -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
cv.current("uA")
|
||||
|
||||
|
||||
def test_float_with_unit_no_match() -> None:
|
||||
with pytest.raises(Invalid, match="Expected frequency with unit"):
|
||||
cv.frequency("!!")
|
||||
|
||||
Reference in New Issue
Block a user