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:
Erik Montnemery 2021-06-30 14:17:58 +02:00 committed by GitHub
parent 508f9a8296
commit 0476c7f9ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 153 additions and 12 deletions

View file

@ -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:

View file

@ -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)

View file

@ -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()

View file

@ -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 = {}

View file

@ -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 = {}

View 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)

View file

@ -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():