Normalize temperature statistics to °C (#52297)
* Normalize temperature statistics to °C * Fix tests * Support temperature conversion to and from K, improve tests * Fix test * Add tests, pylint
This commit is contained in:
parent
508f9a8296
commit
0476c7f9ee
7 changed files with 153 additions and 12 deletions
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||
import datetime
|
||||
import itertools
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from homeassistant.components.recorder import history, statistics
|
||||
from homeassistant.components.sensor import (
|
||||
|
@ -31,10 +32,13 @@ from homeassistant.const import (
|
|||
PRESSURE_PA,
|
||||
PRESSURE_PSI,
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
TEMP_KELVIN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
import homeassistant.util.dt as dt_util
|
||||
import homeassistant.util.pressure as pressure_util
|
||||
import homeassistant.util.temperature as temperature_util
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
|
@ -57,7 +61,7 @@ DEVICE_CLASS_UNITS = {
|
|||
DEVICE_CLASS_TEMPERATURE: TEMP_CELSIUS,
|
||||
}
|
||||
|
||||
UNIT_CONVERSIONS = {
|
||||
UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = {
|
||||
DEVICE_CLASS_ENERGY: {
|
||||
ENERGY_KILO_WATT_HOUR: lambda x: x,
|
||||
ENERGY_WATT_HOUR: lambda x: x / 1000,
|
||||
|
@ -74,6 +78,11 @@ UNIT_CONVERSIONS = {
|
|||
PRESSURE_PA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PA],
|
||||
PRESSURE_PSI: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PSI],
|
||||
},
|
||||
DEVICE_CLASS_TEMPERATURE: {
|
||||
TEMP_CELSIUS: lambda x: x,
|
||||
TEMP_FAHRENHEIT: temperature_util.fahrenheit_to_celsius,
|
||||
TEMP_KELVIN: temperature_util.kelvin_to_celsius,
|
||||
},
|
||||
}
|
||||
|
||||
WARN_UNSUPPORTED_UNIT = set()
|
||||
|
@ -169,7 +178,7 @@ def _normalize_states(
|
|||
_LOGGER.warning("%s has unknown unit %s", entity_id, unit)
|
||||
continue
|
||||
|
||||
fstates.append((UNIT_CONVERSIONS[device_class][unit](fstate), state)) # type: ignore
|
||||
fstates.append((UNIT_CONVERSIONS[device_class][unit](fstate), state))
|
||||
|
||||
return DEVICE_CLASS_UNITS[device_class], fstates
|
||||
|
||||
|
@ -229,6 +238,7 @@ def compile_statistics(
|
|||
_sum = last_stats[entity_id][0]["sum"]
|
||||
|
||||
for fstate, state in fstates:
|
||||
|
||||
if "last_reset" not in state.attributes:
|
||||
continue
|
||||
if (last_reset := state.attributes["last_reset"]) != old_last_reset:
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
from homeassistant.const import (
|
||||
TEMP_CELSIUS,
|
||||
TEMP_FAHRENHEIT,
|
||||
TEMP_KELVIN,
|
||||
TEMPERATURE,
|
||||
UNIT_NOT_RECOGNIZED_TEMPLATE,
|
||||
)
|
||||
|
@ -14,6 +15,13 @@ def fahrenheit_to_celsius(fahrenheit: float, interval: bool = False) -> float:
|
|||
return (fahrenheit - 32.0) / 1.8
|
||||
|
||||
|
||||
def kelvin_to_celsius(kelvin: float, interval: bool = False) -> float:
|
||||
"""Convert a temperature in Kelvin to Celsius."""
|
||||
if interval:
|
||||
return kelvin
|
||||
return kelvin - 273.15
|
||||
|
||||
|
||||
def celsius_to_fahrenheit(celsius: float, interval: bool = False) -> float:
|
||||
"""Convert a temperature in Celsius to Fahrenheit."""
|
||||
if interval:
|
||||
|
@ -21,17 +29,39 @@ def celsius_to_fahrenheit(celsius: float, interval: bool = False) -> float:
|
|||
return celsius * 1.8 + 32.0
|
||||
|
||||
|
||||
def celsius_to_kelvin(celsius: float, interval: bool = False) -> float:
|
||||
"""Convert a temperature in Celsius to Fahrenheit."""
|
||||
if interval:
|
||||
return celsius
|
||||
return celsius + 273.15
|
||||
|
||||
|
||||
def convert(
|
||||
temperature: float, from_unit: str, to_unit: str, interval: bool = False
|
||||
) -> float:
|
||||
"""Convert a temperature from one unit to another."""
|
||||
if from_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT):
|
||||
if from_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN):
|
||||
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(from_unit, TEMPERATURE))
|
||||
if to_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT):
|
||||
if to_unit not in (TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN):
|
||||
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, TEMPERATURE))
|
||||
|
||||
if from_unit == to_unit:
|
||||
return temperature
|
||||
|
||||
if from_unit == TEMP_CELSIUS:
|
||||
return celsius_to_fahrenheit(temperature, interval)
|
||||
return fahrenheit_to_celsius(temperature, interval)
|
||||
if to_unit == TEMP_FAHRENHEIT:
|
||||
return celsius_to_fahrenheit(temperature, interval)
|
||||
# kelvin
|
||||
return celsius_to_kelvin(temperature, interval)
|
||||
|
||||
if from_unit == TEMP_FAHRENHEIT:
|
||||
if to_unit == TEMP_CELSIUS:
|
||||
return fahrenheit_to_celsius(temperature, interval)
|
||||
# kelvin
|
||||
return celsius_to_kelvin(fahrenheit_to_celsius(temperature, interval), interval)
|
||||
|
||||
# from_unit == kelvin
|
||||
if to_unit == TEMP_CELSIUS:
|
||||
return kelvin_to_celsius(temperature, interval)
|
||||
# fahrenheit
|
||||
return celsius_to_fahrenheit(kelvin_to_celsius(temperature, interval), interval)
|
||||
|
|
|
@ -840,7 +840,11 @@ async def test_statistics_during_period(hass, hass_ws_client):
|
|||
hass.states.async_set(
|
||||
"sensor.test",
|
||||
10,
|
||||
attributes={"device_class": "temperature", "state_class": "measurement"},
|
||||
attributes={
|
||||
"device_class": "temperature",
|
||||
"state_class": "measurement",
|
||||
"unit_of_measurement": "°C",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ from homeassistant.components.recorder import history
|
|||
from homeassistant.components.recorder.const import DATA_INSTANCE
|
||||
from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat
|
||||
from homeassistant.components.recorder.statistics import statistics_during_period
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.setup import setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
|
@ -53,7 +54,11 @@ def record_states(hass):
|
|||
sns1 = "sensor.test1"
|
||||
sns2 = "sensor.test2"
|
||||
sns3 = "sensor.test3"
|
||||
sns1_attr = {"device_class": "temperature", "state_class": "measurement"}
|
||||
sns1_attr = {
|
||||
"device_class": "temperature",
|
||||
"state_class": "measurement",
|
||||
"unit_of_measurement": TEMP_CELSIUS,
|
||||
}
|
||||
sns2_attr = {"device_class": "temperature"}
|
||||
sns3_attr = {}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ from homeassistant.components.recorder import history
|
|||
from homeassistant.components.recorder.const import DATA_INSTANCE
|
||||
from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat
|
||||
from homeassistant.components.recorder.statistics import statistics_during_period
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.const import STATE_UNAVAILABLE, TEMP_CELSIUS
|
||||
from homeassistant.setup import setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
|
@ -324,7 +324,11 @@ def record_states(hass):
|
|||
sns1 = "sensor.test1"
|
||||
sns2 = "sensor.test2"
|
||||
sns3 = "sensor.test3"
|
||||
sns1_attr = {"device_class": "temperature", "state_class": "measurement"}
|
||||
sns1_attr = {
|
||||
"device_class": "temperature",
|
||||
"state_class": "measurement",
|
||||
"unit_of_measurement": TEMP_CELSIUS,
|
||||
}
|
||||
sns2_attr = {"device_class": "temperature"}
|
||||
sns3_attr = {}
|
||||
|
||||
|
@ -466,7 +470,11 @@ def record_states_partially_unavailable(hass):
|
|||
sns1 = "sensor.test1"
|
||||
sns2 = "sensor.test2"
|
||||
sns3 = "sensor.test3"
|
||||
sns1_attr = {"device_class": "temperature", "state_class": "measurement"}
|
||||
sns1_attr = {
|
||||
"device_class": "temperature",
|
||||
"state_class": "measurement",
|
||||
"unit_of_measurement": TEMP_CELSIUS,
|
||||
}
|
||||
sns2_attr = {"device_class": "temperature"}
|
||||
sns3_attr = {}
|
||||
|
||||
|
|
84
tests/util/test_temperature.py
Normal file
84
tests/util/test_temperature.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
"""Test Home Assistant temperature utility functions."""
|
||||
import pytest
|
||||
|
||||
from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT, TEMP_KELVIN
|
||||
import homeassistant.util.temperature as temperature_util
|
||||
|
||||
INVALID_SYMBOL = "bob"
|
||||
VALID_SYMBOL = TEMP_CELSIUS
|
||||
|
||||
|
||||
def test_convert_same_unit():
|
||||
"""Test conversion from any unit to same unit."""
|
||||
assert temperature_util.convert(2, TEMP_CELSIUS, TEMP_CELSIUS) == 2
|
||||
assert temperature_util.convert(3, TEMP_FAHRENHEIT, TEMP_FAHRENHEIT) == 3
|
||||
assert temperature_util.convert(4, TEMP_KELVIN, TEMP_KELVIN) == 4
|
||||
|
||||
|
||||
def test_convert_invalid_unit():
|
||||
"""Test exception is thrown for invalid units."""
|
||||
with pytest.raises(ValueError):
|
||||
temperature_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
temperature_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL)
|
||||
|
||||
|
||||
def test_convert_nonnumeric_value():
|
||||
"""Test exception is thrown for nonnumeric type."""
|
||||
with pytest.raises(TypeError):
|
||||
temperature_util.convert("a", TEMP_CELSIUS, TEMP_FAHRENHEIT)
|
||||
|
||||
|
||||
def test_convert_from_celsius():
|
||||
"""Test conversion from C to other units."""
|
||||
celsius = 100
|
||||
assert temperature_util.convert(
|
||||
celsius, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
) == pytest.approx(212.0)
|
||||
assert temperature_util.convert(
|
||||
celsius, TEMP_CELSIUS, TEMP_KELVIN
|
||||
) == pytest.approx(373.15)
|
||||
# Interval
|
||||
assert temperature_util.convert(
|
||||
celsius, TEMP_CELSIUS, TEMP_FAHRENHEIT, True
|
||||
) == pytest.approx(180.0)
|
||||
assert temperature_util.convert(
|
||||
celsius, TEMP_CELSIUS, TEMP_KELVIN, True
|
||||
) == pytest.approx(100)
|
||||
|
||||
|
||||
def test_convert_from_fahrenheit():
|
||||
"""Test conversion from F to other units."""
|
||||
fahrenheit = 100
|
||||
assert temperature_util.convert(
|
||||
fahrenheit, TEMP_FAHRENHEIT, TEMP_CELSIUS
|
||||
) == pytest.approx(37.77777777777778)
|
||||
assert temperature_util.convert(
|
||||
fahrenheit, TEMP_FAHRENHEIT, TEMP_KELVIN
|
||||
) == pytest.approx(310.92777777777775)
|
||||
# Interval
|
||||
assert temperature_util.convert(
|
||||
fahrenheit, TEMP_FAHRENHEIT, TEMP_CELSIUS, True
|
||||
) == pytest.approx(55.55555555555556)
|
||||
assert temperature_util.convert(
|
||||
fahrenheit, TEMP_FAHRENHEIT, TEMP_KELVIN, True
|
||||
) == pytest.approx(55.55555555555556)
|
||||
|
||||
|
||||
def test_convert_from_kelvin():
|
||||
"""Test conversion from K to other units."""
|
||||
kelvin = 100
|
||||
assert temperature_util.convert(kelvin, TEMP_KELVIN, TEMP_CELSIUS) == pytest.approx(
|
||||
-173.15
|
||||
)
|
||||
assert temperature_util.convert(
|
||||
kelvin, TEMP_KELVIN, TEMP_FAHRENHEIT
|
||||
) == pytest.approx(-279.66999999999996)
|
||||
# Interval
|
||||
assert temperature_util.convert(
|
||||
kelvin, TEMP_KELVIN, TEMP_FAHRENHEIT, True
|
||||
) == pytest.approx(180.0)
|
||||
assert temperature_util.convert(
|
||||
kelvin, TEMP_KELVIN, TEMP_KELVIN, True
|
||||
) == pytest.approx(100)
|
|
@ -106,7 +106,7 @@ def test_temperature_same_unit():
|
|||
def test_temperature_unknown_unit():
|
||||
"""Test no conversion happens if unknown unit."""
|
||||
with pytest.raises(ValueError):
|
||||
METRIC_SYSTEM.temperature(5, "K")
|
||||
METRIC_SYSTEM.temperature(5, "abc")
|
||||
|
||||
|
||||
def test_temperature_to_metric():
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue