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

View File

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

View File

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

View File

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

View File

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

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