From 6ad3b60adf37f1696926393438820dc5d8c2702f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 22 Jun 2023 19:52:14 +0200 Subject: [PATCH] Implement Apparent temperature in Weather entity component (#95070) --- homeassistant/components/weather/__init__.py | 43 +++++++++++++++++++ homeassistant/components/weather/const.py | 1 + homeassistant/components/weather/strings.json | 3 ++ tests/components/weather/test_init.py | 20 ++++++++- .../custom_components/test/weather.py | 7 +++ 5 files changed, 73 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 0a99b6aaaf7..0efaea949e1 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -30,6 +30,7 @@ from homeassistant.helpers.typing import ConfigType from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM from .const import ( + ATTR_WEATHER_APPARENT_TEMPERATURE, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_OZONE, ATTR_WEATHER_PRECIPITATION_UNIT, @@ -73,6 +74,8 @@ ATTR_FORECAST_PRECIPITATION: Final = "precipitation" ATTR_FORECAST_PRECIPITATION_PROBABILITY: Final = "precipitation_probability" ATTR_FORECAST_NATIVE_PRESSURE: Final = "native_pressure" ATTR_FORECAST_PRESSURE: Final = "pressure" +ATTR_FORECAST_NATIVE_APPARENT_TEMP: Final = "native_apparent_temperature" +ATTR_FORECAST_APPARENT_TEMP: Final = "apparent_temperature" ATTR_FORECAST_NATIVE_TEMP: Final = "native_temperature" ATTR_FORECAST_TEMP: Final = "temperature" ATTR_FORECAST_NATIVE_TEMP_LOW: Final = "native_templow" @@ -199,6 +202,7 @@ class WeatherEntity(Entity): _attr_native_pressure: float | None = None _attr_native_pressure_unit: str | None = None + _attr_native_apparent_temperature: float | None = None _attr_native_temperature: float | None = None _attr_native_temperature_unit: str | None = None _attr_native_visibility: float | None = None @@ -272,6 +276,11 @@ class WeatherEntity(Entity): return self.async_registry_entry_updated() + @property + def native_apparent_temperature(self) -> float | None: + """Return the apparent temperature in native units.""" + return self._attr_native_temperature + @final @property def temperature(self) -> float | None: @@ -600,6 +609,20 @@ class WeatherEntity(Entity): except (TypeError, ValueError): data[ATTR_WEATHER_TEMPERATURE] = temperature + if (apparent_temperature := self.native_apparent_temperature) is not None: + from_unit = self.native_temperature_unit or self._default_temperature_unit + to_unit = self._temperature_unit + try: + apparent_temperature_f = float(apparent_temperature) + value_apparent_temp = UNIT_CONVERSIONS[ATTR_WEATHER_TEMPERATURE_UNIT]( + apparent_temperature_f, from_unit, to_unit + ) + data[ATTR_WEATHER_APPARENT_TEMPERATURE] = round_temperature( + value_apparent_temp, precision + ) + except (TypeError, ValueError): + data[ATTR_WEATHER_APPARENT_TEMPERATURE] = apparent_temperature + data[ATTR_WEATHER_TEMPERATURE_UNIT] = self._temperature_unit if (humidity := self.humidity) is not None: @@ -686,6 +709,26 @@ class WeatherEntity(Entity): value_temp, precision ) + if ( + forecast_apparent_temp := forecast_entry.pop( + ATTR_FORECAST_NATIVE_APPARENT_TEMP, + forecast_entry.get(ATTR_FORECAST_NATIVE_APPARENT_TEMP), + ) + ) is not None: + with suppress(TypeError, ValueError): + forecast_apparent_temp = float(forecast_apparent_temp) + value_apparent_temp = UNIT_CONVERSIONS[ + ATTR_WEATHER_TEMPERATURE_UNIT + ]( + forecast_apparent_temp, + from_temp_unit, + to_temp_unit, + ) + + forecast_entry[ATTR_FORECAST_APPARENT_TEMP] = round_temperature( + value_apparent_temp, precision + ) + if ( forecast_temp_low := forecast_entry.pop( ATTR_FORECAST_NATIVE_TEMP_LOW, diff --git a/homeassistant/components/weather/const.py b/homeassistant/components/weather/const.py index 2dcfd8a2ddc..95094850ff2 100644 --- a/homeassistant/components/weather/const.py +++ b/homeassistant/components/weather/const.py @@ -22,6 +22,7 @@ ATTR_WEATHER_HUMIDITY = "humidity" ATTR_WEATHER_OZONE = "ozone" ATTR_WEATHER_PRESSURE = "pressure" ATTR_WEATHER_PRESSURE_UNIT = "pressure_unit" +ATTR_WEATHER_APPARENT_TEMPERATURE = "apparent_temperature" ATTR_WEATHER_TEMPERATURE = "temperature" ATTR_WEATHER_TEMPERATURE_UNIT = "temperature_unit" ATTR_WEATHER_VISIBILITY = "visibility" diff --git a/homeassistant/components/weather/strings.json b/homeassistant/components/weather/strings.json index 461f715c8db..e319d42c943 100644 --- a/homeassistant/components/weather/strings.json +++ b/homeassistant/components/weather/strings.json @@ -39,6 +39,9 @@ "pressure_unit": { "name": "Pressure unit" }, + "apparent_temperature": { + "name": "Apparent temperature" + }, "temperature": { "name": "Temperature" }, diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 76483491bf8..6ac27f1c2c9 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -6,12 +6,14 @@ import pytest from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_FORECAST, + ATTR_FORECAST_APPARENT_TEMP, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, + ATTR_WEATHER_APPARENT_TEMPERATURE, ATTR_WEATHER_OZONE, ATTR_WEATHER_PRECIPITATION_UNIT, ATTR_WEATHER_PRESSURE, @@ -63,6 +65,7 @@ class MockWeatherEntity(WeatherEntity): self._attr_native_pressure = 10 self._attr_native_pressure_unit = UnitOfPressure.HPA self._attr_native_temperature = 20 + self._attr_native_apparent_temperature = 25 self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS self._attr_native_visibility = 30 self._attr_native_visibility_unit = UnitOfLength.KILOMETERS @@ -85,6 +88,7 @@ class MockWeatherEntityPrecision(WeatherEntity): super().__init__() self._attr_condition = ATTR_CONDITION_SUNNY self._attr_native_temperature = 20.3 + self._attr_native_apparent_temperature = 25.3 self._attr_native_temperature_unit = UnitOfTemperature.CELSIUS self._attr_precision = PRECISION_HALVES @@ -153,21 +157,35 @@ async def test_temperature( """Test temperature.""" hass.config.units = unit_system native_value = 38 + apparent_native_value = 45 state_value = TemperatureConverter.convert(native_value, native_unit, state_unit) + apparent_state_value = TemperatureConverter.convert( + apparent_native_value, native_unit, state_unit + ) entity0 = await create_entity( - hass, native_temperature=native_value, native_temperature_unit=native_unit + hass, + native_temperature=native_value, + native_temperature_unit=native_unit, + native_apparent_temperature=apparent_native_value, ) state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] expected = state_value + apparent_expected = apparent_state_value assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == pytest.approx( expected, rel=0.1 ) + assert float(state.attributes[ATTR_WEATHER_APPARENT_TEMPERATURE]) == pytest.approx( + apparent_expected, rel=0.1 + ) assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == state_unit assert float(forecast[ATTR_FORECAST_TEMP]) == pytest.approx(expected, rel=0.1) + assert float(forecast[ATTR_FORECAST_APPARENT_TEMP]) == pytest.approx( + apparent_expected, rel=0.1 + ) assert float(forecast[ATTR_FORECAST_TEMP_LOW]) == pytest.approx(expected, rel=0.1) diff --git a/tests/testing_config/custom_components/test/weather.py b/tests/testing_config/custom_components/test/weather.py index a2977e114e4..df9a3faea3f 100644 --- a/tests/testing_config/custom_components/test/weather.py +++ b/tests/testing_config/custom_components/test/weather.py @@ -5,6 +5,7 @@ Call init before using it in your tests to ensure clean test data. from __future__ import annotations from homeassistant.components.weather import ( + ATTR_FORECAST_NATIVE_APPARENT_TEMP, ATTR_FORECAST_NATIVE_PRECIPITATION, ATTR_FORECAST_NATIVE_PRESSURE, ATTR_FORECAST_NATIVE_TEMP, @@ -46,6 +47,11 @@ class MockWeather(MockEntity, WeatherEntity): """Return the platform temperature.""" return self._handle("native_temperature") + @property + def native_apparent_temperature(self) -> float | None: + """Return the platform apparent temperature.""" + return self._handle("native_apparent_temperature") + @property def native_temperature_unit(self) -> str | None: """Return the unit of measurement for temperature.""" @@ -195,6 +201,7 @@ class MockWeatherMockForecast(MockWeather): return [ { ATTR_FORECAST_NATIVE_TEMP: self.native_temperature, + ATTR_FORECAST_NATIVE_APPARENT_TEMP: self.native_apparent_temperature, ATTR_FORECAST_NATIVE_TEMP_LOW: self.native_temperature, ATTR_FORECAST_NATIVE_PRESSURE: self.native_pressure, ATTR_FORECAST_NATIVE_WIND_SPEED: self.native_wind_speed,