From 0476c7f9eef8aacbfbf6e332290ce2699ac27a57 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 30 Jun 2021 14:17:58 +0200 Subject: [PATCH] =?UTF-8?q?Normalize=20temperature=20statistics=20to=20?= =?UTF-8?q?=C2=B0C=20(#52297)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Normalize temperature statistics to °C * Fix tests * Support temperature conversion to and from K, improve tests * Fix test * Add tests, pylint --- homeassistant/components/sensor/recorder.py | 14 +++- homeassistant/util/temperature.py | 38 ++++++++- tests/components/history/test_init.py | 6 +- tests/components/recorder/test_statistics.py | 7 +- tests/components/sensor/test_recorder.py | 14 +++- tests/util/test_temperature.py | 84 ++++++++++++++++++++ tests/util/test_unit_system.py | 2 +- 7 files changed, 153 insertions(+), 12 deletions(-) create mode 100644 tests/util/test_temperature.py diff --git a/homeassistant/components/sensor/recorder.py b/homeassistant/components/sensor/recorder.py index 95f9a6faebf..ac26e06e07d 100644 --- a/homeassistant/components/sensor/recorder.py +++ b/homeassistant/components/sensor/recorder.py @@ -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: diff --git a/homeassistant/util/temperature.py b/homeassistant/util/temperature.py index 0b3edc6ef57..bc3cb4c1017 100644 --- a/homeassistant/util/temperature.py +++ b/homeassistant/util/temperature.py @@ -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) diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index a6c386ea319..ac57069acf0 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -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() diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 679ef7597c2..104617aee2c 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -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 = {} diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index b8411e69a7f..65346f1feba 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -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 = {} diff --git a/tests/util/test_temperature.py b/tests/util/test_temperature.py new file mode 100644 index 00000000000..7730a89cbb8 --- /dev/null +++ b/tests/util/test_temperature.py @@ -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) diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py index 74abfef452f..f32e731f9b3 100644 --- a/tests/util/test_unit_system.py +++ b/tests/util/test_unit_system.py @@ -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():