mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +00:00
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
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
import datetime
|
import datetime
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
from homeassistant.components.recorder import history, statistics
|
from homeassistant.components.recorder import history, statistics
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
@ -31,10 +32,13 @@ from homeassistant.const import (
|
|||||||
PRESSURE_PA,
|
PRESSURE_PA,
|
||||||
PRESSURE_PSI,
|
PRESSURE_PSI,
|
||||||
TEMP_CELSIUS,
|
TEMP_CELSIUS,
|
||||||
|
TEMP_FAHRENHEIT,
|
||||||
|
TEMP_KELVIN,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant, State
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
import homeassistant.util.pressure as pressure_util
|
import homeassistant.util.pressure as pressure_util
|
||||||
|
import homeassistant.util.temperature as temperature_util
|
||||||
|
|
||||||
from . import DOMAIN
|
from . import DOMAIN
|
||||||
|
|
||||||
@ -57,7 +61,7 @@ DEVICE_CLASS_UNITS = {
|
|||||||
DEVICE_CLASS_TEMPERATURE: TEMP_CELSIUS,
|
DEVICE_CLASS_TEMPERATURE: TEMP_CELSIUS,
|
||||||
}
|
}
|
||||||
|
|
||||||
UNIT_CONVERSIONS = {
|
UNIT_CONVERSIONS: dict[str, dict[str, Callable]] = {
|
||||||
DEVICE_CLASS_ENERGY: {
|
DEVICE_CLASS_ENERGY: {
|
||||||
ENERGY_KILO_WATT_HOUR: lambda x: x,
|
ENERGY_KILO_WATT_HOUR: lambda x: x,
|
||||||
ENERGY_WATT_HOUR: lambda x: x / 1000,
|
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_PA: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PA],
|
||||||
PRESSURE_PSI: lambda x: x / pressure_util.UNIT_CONVERSION[PRESSURE_PSI],
|
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()
|
WARN_UNSUPPORTED_UNIT = set()
|
||||||
@ -169,7 +178,7 @@ def _normalize_states(
|
|||||||
_LOGGER.warning("%s has unknown unit %s", entity_id, unit)
|
_LOGGER.warning("%s has unknown unit %s", entity_id, unit)
|
||||||
continue
|
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
|
return DEVICE_CLASS_UNITS[device_class], fstates
|
||||||
|
|
||||||
@ -229,6 +238,7 @@ def compile_statistics(
|
|||||||
_sum = last_stats[entity_id][0]["sum"]
|
_sum = last_stats[entity_id][0]["sum"]
|
||||||
|
|
||||||
for fstate, state in fstates:
|
for fstate, state in fstates:
|
||||||
|
|
||||||
if "last_reset" not in state.attributes:
|
if "last_reset" not in state.attributes:
|
||||||
continue
|
continue
|
||||||
if (last_reset := state.attributes["last_reset"]) != old_last_reset:
|
if (last_reset := state.attributes["last_reset"]) != old_last_reset:
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
TEMP_CELSIUS,
|
TEMP_CELSIUS,
|
||||||
TEMP_FAHRENHEIT,
|
TEMP_FAHRENHEIT,
|
||||||
|
TEMP_KELVIN,
|
||||||
TEMPERATURE,
|
TEMPERATURE,
|
||||||
UNIT_NOT_RECOGNIZED_TEMPLATE,
|
UNIT_NOT_RECOGNIZED_TEMPLATE,
|
||||||
)
|
)
|
||||||
@ -14,6 +15,13 @@ def fahrenheit_to_celsius(fahrenheit: float, interval: bool = False) -> float:
|
|||||||
return (fahrenheit - 32.0) / 1.8
|
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:
|
def celsius_to_fahrenheit(celsius: float, interval: bool = False) -> float:
|
||||||
"""Convert a temperature in Celsius to Fahrenheit."""
|
"""Convert a temperature in Celsius to Fahrenheit."""
|
||||||
if interval:
|
if interval:
|
||||||
@ -21,17 +29,39 @@ def celsius_to_fahrenheit(celsius: float, interval: bool = False) -> float:
|
|||||||
return celsius * 1.8 + 32.0
|
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(
|
def convert(
|
||||||
temperature: float, from_unit: str, to_unit: str, interval: bool = False
|
temperature: float, from_unit: str, to_unit: str, interval: bool = False
|
||||||
) -> float:
|
) -> float:
|
||||||
"""Convert a temperature from one unit to another."""
|
"""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))
|
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))
|
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, TEMPERATURE))
|
||||||
|
|
||||||
if from_unit == to_unit:
|
if from_unit == to_unit:
|
||||||
return temperature
|
return temperature
|
||||||
|
|
||||||
if from_unit == TEMP_CELSIUS:
|
if from_unit == TEMP_CELSIUS:
|
||||||
|
if to_unit == TEMP_FAHRENHEIT:
|
||||||
return celsius_to_fahrenheit(temperature, interval)
|
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)
|
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(
|
hass.states.async_set(
|
||||||
"sensor.test",
|
"sensor.test",
|
||||||
10,
|
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()
|
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.const import DATA_INSTANCE
|
||||||
from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat
|
from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat
|
||||||
from homeassistant.components.recorder.statistics import statistics_during_period
|
from homeassistant.components.recorder.statistics import statistics_during_period
|
||||||
|
from homeassistant.const import TEMP_CELSIUS
|
||||||
from homeassistant.setup import setup_component
|
from homeassistant.setup import setup_component
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
@ -53,7 +54,11 @@ def record_states(hass):
|
|||||||
sns1 = "sensor.test1"
|
sns1 = "sensor.test1"
|
||||||
sns2 = "sensor.test2"
|
sns2 = "sensor.test2"
|
||||||
sns3 = "sensor.test3"
|
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"}
|
sns2_attr = {"device_class": "temperature"}
|
||||||
sns3_attr = {}
|
sns3_attr = {}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ from homeassistant.components.recorder import history
|
|||||||
from homeassistant.components.recorder.const import DATA_INSTANCE
|
from homeassistant.components.recorder.const import DATA_INSTANCE
|
||||||
from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat
|
from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat
|
||||||
from homeassistant.components.recorder.statistics import statistics_during_period
|
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
|
from homeassistant.setup import setup_component
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
@ -324,7 +324,11 @@ def record_states(hass):
|
|||||||
sns1 = "sensor.test1"
|
sns1 = "sensor.test1"
|
||||||
sns2 = "sensor.test2"
|
sns2 = "sensor.test2"
|
||||||
sns3 = "sensor.test3"
|
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"}
|
sns2_attr = {"device_class": "temperature"}
|
||||||
sns3_attr = {}
|
sns3_attr = {}
|
||||||
|
|
||||||
@ -466,7 +470,11 @@ def record_states_partially_unavailable(hass):
|
|||||||
sns1 = "sensor.test1"
|
sns1 = "sensor.test1"
|
||||||
sns2 = "sensor.test2"
|
sns2 = "sensor.test2"
|
||||||
sns3 = "sensor.test3"
|
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"}
|
sns2_attr = {"device_class": "temperature"}
|
||||||
sns3_attr = {}
|
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():
|
def test_temperature_unknown_unit():
|
||||||
"""Test no conversion happens if unknown unit."""
|
"""Test no conversion happens if unknown unit."""
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
METRIC_SYSTEM.temperature(5, "K")
|
METRIC_SYSTEM.temperature(5, "abc")
|
||||||
|
|
||||||
|
|
||||||
def test_temperature_to_metric():
|
def test_temperature_to_metric():
|
||||||
|
Loading…
x
Reference in New Issue
Block a user