From 1285448b5d799189708c3ff2067ac948fde66b5a Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 13 Jun 2022 14:41:21 +0000 Subject: [PATCH 01/27] Initial commit --- homeassistant/components/weather/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 2e0f8912867..e7809183528 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -116,15 +116,20 @@ class WeatherEntity(Entity): _attr_precision: float _attr_pressure: float | None = None _attr_pressure_unit: str | None = None + _attr_native_pressure_unit: str | None = None _attr_state: None = None _attr_temperature_unit: str + _attr_native_temperature_unit: str _attr_temperature: float | None _attr_visibility: float | None = None _attr_visibility_unit: str | None = None + _attr_native_visibility_unit: str | None = None _attr_precipitation_unit: str | None = None + _attr_native_precipitation_unit: str | None = None _attr_wind_bearing: float | str | None = None _attr_wind_speed: float | None = None _attr_wind_speed_unit: str | None = None + _attr_native_wind_speed_unit: str | None = None @property def temperature(self) -> float | None: From aeeb4da72cffdae9fb617a82f8f84d1aabd2e385 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 13 Jun 2022 16:59:34 +0000 Subject: [PATCH 02/27] Additions WIP --- homeassistant/components/weather/__init__.py | 170 +++++++++++++++++-- 1 file changed, 157 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index e7809183528..143204296a7 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -1,14 +1,31 @@ """Weather component that handles meteorological data for your location.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass from datetime import timedelta import logging from typing import Final, TypedDict, final from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_MILES, + LENGTH_MILLIMETERS, + PRECISION_TENTHS, + PRECISION_WHOLE, + PRESSURE_HPA, + PRESSURE_INHG, + PRESSURE_MBAR, + PRESSURE_MMHG, + SPEED_KILOMETERS_PER_HOUR, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -17,6 +34,12 @@ from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType +from homeassistant.util import ( + distance as distance_util, + pressure as pressure_util, + speed as speed_util, + temperature as temperature_util, +) # mypy: allow-untyped-defs, no-check-untyped-defs @@ -56,6 +79,12 @@ ATTR_WEATHER_VISIBILITY = "visibility" ATTR_WEATHER_WIND_BEARING = "wind_bearing" ATTR_WEATHER_WIND_SPEED = "wind_speed" +CONF_PRECIPITATION_UOM = "precipitation_unit_of_measurement" +CONF_PRESSURE_UOM = "pressure_unit_of_measurement" +CONF_TEMPERATURE_UOM = "temperature_unit_of_measurement" +CONF_VISIBILITY_UOM = "visibility_unit_of_measurement" +CONF_WIND_SPEED_UOM = "wind_speed_unit_of_measurement" + DOMAIN = "weather" ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -64,6 +93,57 @@ SCAN_INTERVAL = timedelta(seconds=30) ROUNDING_PRECISION = 2 +VALID_UNITS_PRESSURE: tuple[str, ...] = ( + PRESSURE_HPA, + PRESSURE_MBAR, + PRESSURE_INHG, + PRESSURE_MMHG, +) +VALID_UNITS_TEMPERATURE: tuple[str, ...] = ( + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) +VALID_UNITS_PRECIPITATION: tuple[str, ...] = ( + LENGTH_MILLIMETERS, + LENGTH_INCHES, +) +VALID_UNITS_VISIBILITY: tuple[str, ...] = ( + LENGTH_KILOMETERS, + LENGTH_MILES, +) +VALID_UNITS_SPEED: tuple[str, ...] = ( + SPEED_METERS_PER_SECOND, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, +) + +UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { + CONF_PRESSURE_UOM: pressure_util.convert, + CONF_TEMPERATURE_UOM: temperature_util.convert, + CONF_VISIBILITY_UOM: distance_util.convert, + CONF_PRECIPITATION_UOM: distance_util.convert, + CONF_WIND_SPEED_UOM: speed_util.convert, +} + +UNIT_RATIOS: dict[str, dict[str, float]] = { + CONF_PRESSURE_UOM: pressure_util.UNIT_CONVERSION, + CONF_TEMPERATURE_UOM: { + TEMP_CELSIUS: 1.0, + TEMP_FAHRENHEIT: 1.8, + }, + CONF_VISIBILITY_UOM: distance_util.METERS_TO, + CONF_PRECIPITATION_UOM: distance_util.METERS_TO, + CONF_WIND_SPEED_UOM: speed_util.UNIT_CONVERSION, +} + +VALID_UNITS: dict[str, tuple[str, ...]] = { + CONF_PRESSURE_UOM: VALID_UNITS_PRESSURE, + CONF_TEMPERATURE_UOM: VALID_UNITS_TEMPERATURE, + CONF_VISIBILITY_UOM: VALID_UNITS_VISIBILITY, + CONF_PRECIPITATION_UOM: VALID_UNITS_PRECIPITATION, + CONF_WIND_SPEED_UOM: VALID_UNITS_SPEED, +} + class Forecast(TypedDict, total=False): """Typed weather forecast dict.""" @@ -115,31 +195,55 @@ class WeatherEntity(Entity): _attr_ozone: float | None = None _attr_precision: float _attr_pressure: float | None = None - _attr_pressure_unit: str | None = None + _attr_pressure_unit: None = None # Subclasses of WeatherEntity should not set this _attr_native_pressure_unit: str | None = None _attr_state: None = None - _attr_temperature_unit: str + _attr_temperature_unit: None = ( + None # Subclasses of WeatherEntity should not set this + ) _attr_native_temperature_unit: str _attr_temperature: float | None _attr_visibility: float | None = None - _attr_visibility_unit: str | None = None + _attr_visibility_unit: None = ( + None # Subclasses of WeatherEntity should not set this + ) _attr_native_visibility_unit: str | None = None - _attr_precipitation_unit: str | None = None + _attr_precipitation_unit: None = ( + None # Subclasses of WeatherEntity should not set this + ) _attr_native_precipitation_unit: str | None = None _attr_wind_bearing: float | str | None = None _attr_wind_speed: float | None = None - _attr_wind_speed_unit: str | None = None + _attr_wind_speed_unit: None = ( + None # Subclasses of WeatherEntity should not set this + ) _attr_native_wind_speed_unit: str | None = None + _weather_option_temperature_uom: str | None = None + _weather_option_pressure_uom: str | None = None + _weather_option_visibility_uom: str | None = None + _weather_option_precipitation_uom: str | None = None + _weather_option_wind_speed_uom: str | None = None + @property def temperature(self) -> float | None: """Return the platform temperature in native units (i.e. not converted).""" return self._attr_temperature @property - def temperature_unit(self) -> str: + def native_temperature_unit(self) -> str | None: """Return the native unit of measurement for temperature.""" - return self._attr_temperature_unit + if hasattr(self, "_attr_native_temperature_unit"): + return self._attr_native_temperature_unit + return None + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement for temperature.""" + if self._weather_option_temperature_uom: + return self._weather_option_temperature_uom + + return self.hass.config.units.temperature_unit @property def pressure(self) -> float | None: @@ -147,9 +251,21 @@ class WeatherEntity(Entity): return self._attr_pressure @property - def pressure_unit(self) -> str | None: + def native_pressure_unit(self) -> str | None: """Return the native unit of measurement for pressure.""" - return self._attr_pressure_unit + if hasattr(self, "_attr_native_pressure_unit"): + return self._attr_native_pressure_unit + return None + + @property + def pressure_unit(self) -> str | None: + """Return the unit of measurement for pressure.""" + if self._weather_option_pressure_uom: + return self._weather_option_pressure_uom + + if self.hass.config.units.is_metric: + return PRESSURE_HPA + return PRESSURE_INHG @property def humidity(self) -> float | None: @@ -162,9 +278,21 @@ class WeatherEntity(Entity): return self._attr_wind_speed @property - def wind_speed_unit(self) -> str | None: + def native_wind_speed_unit(self) -> str | None: """Return the native unit of measurement for wind speed.""" - return self._attr_wind_speed_unit + if hasattr(self, "_attr_native_wind_speed_unit"): + return self._attr_native_wind_speed_unit + return None + + @property + def wind_speed_unit(self) -> str | None: + """Return the unit of measurement for wind speed.""" + if self._weather_option_wind_speed_uom: + return self._weather_option_wind_speed_uom + + if self.hass.config.units.is_metric: + return SPEED_METERS_PER_SECOND + return SPEED_MILES_PER_HOUR @property def wind_bearing(self) -> float | str | None: @@ -314,3 +442,19 @@ class WeatherEntity(Entity): def condition(self) -> str | None: """Return the current condition.""" return self._attr_condition + + @callback + def async_registry_entry_updated(self) -> None: + """Run when the entity registry entry has been updated.""" + assert self.registry_entry + weather_options = self.registry_entry.options.get(DOMAIN) + if ( + weather_options + and (custom_unit_temperature := weather_options.get(CONF_TEMPERATURE_UOM)) + and self.native_temperature_unit in VALID_UNITS[CONF_TEMPERATURE_UOM] + and custom_unit_temperature in VALID_UNITS[CONF_TEMPERATURE_UOM] + ): + self._weather_option_temperature_uom = custom_unit_temperature + return + + self._weather_option_temperature_uom = None From 3a8d2c67bdc5c059f6e4e1e15cb971cc2fa28007 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 14 Jun 2022 18:39:45 +0000 Subject: [PATCH 03/27] Finalize weather units --- homeassistant/components/weather/__init__.py | 265 +++++++++++++------ 1 file changed, 181 insertions(+), 84 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 143204296a7..a652bf00a58 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable +from contextlib import suppress from dataclasses import dataclass from datetime import timedelta import logging @@ -32,7 +33,6 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType from homeassistant.util import ( distance as distance_util, @@ -74,10 +74,16 @@ ATTR_FORECAST_WIND_SPEED: Final = "wind_speed" ATTR_WEATHER_HUMIDITY = "humidity" ATTR_WEATHER_OZONE = "ozone" ATTR_WEATHER_PRESSURE = "pressure" +ATTR_WEATHER_PRESSURE_UNIT = "pressure_unit" ATTR_WEATHER_TEMPERATURE = "temperature" +ATTR_WEATHER_TEMPERATURE_UNIT = "temperature_unit" ATTR_WEATHER_VISIBILITY = "visibility" +ATTR_WEATHER_VISIBILITY_UNIT = "visibility_unit" ATTR_WEATHER_WIND_BEARING = "wind_bearing" ATTR_WEATHER_WIND_SPEED = "wind_speed" +ATTR_WEATHER_WIND_SPEED_UNIT = "wind_speed_unit" +ATTR_WEATHER_PRECIPITATION_UNIT = "precipitation_unit" + CONF_PRECIPITATION_UOM = "precipitation_unit_of_measurement" CONF_PRESSURE_UOM = "pressure_unit_of_measurement" @@ -125,17 +131,6 @@ UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { CONF_WIND_SPEED_UOM: speed_util.convert, } -UNIT_RATIOS: dict[str, dict[str, float]] = { - CONF_PRESSURE_UOM: pressure_util.UNIT_CONVERSION, - CONF_TEMPERATURE_UOM: { - TEMP_CELSIUS: 1.0, - TEMP_FAHRENHEIT: 1.8, - }, - CONF_VISIBILITY_UOM: distance_util.METERS_TO, - CONF_PRECIPITATION_UOM: distance_util.METERS_TO, - CONF_WIND_SPEED_UOM: speed_util.UNIT_CONVERSION, -} - VALID_UNITS: dict[str, tuple[str, ...]] = { CONF_PRESSURE_UOM: VALID_UNITS_PRESSURE, CONF_TEMPERATURE_UOM: VALID_UNITS_TEMPERATURE, @@ -225,6 +220,13 @@ class WeatherEntity(Entity): _weather_option_precipitation_uom: str | None = None _weather_option_wind_speed_uom: str | None = None + async def async_internal_added_to_hass(self) -> None: + """Call when the sensor entity is added to hass.""" + await super().async_internal_added_to_hass() + if not self.registry_entry: + return + self.async_registry_entry_updated() + @property def temperature(self) -> float | None: """Return the platform temperature in native units (i.e. not converted).""" @@ -310,9 +312,21 @@ class WeatherEntity(Entity): return self._attr_visibility @property - def visibility_unit(self) -> str | None: + def native_visibility_unit(self) -> str | None: """Return the native unit of measurement for visibility.""" - return self._attr_visibility_unit + if hasattr(self, "_attr_native_visibility_unit"): + return self._attr_native_visibility_unit + return None + + @property + def visibility_unit(self) -> str | None: + """Return the unit of measurement for visibility.""" + if self._weather_option_visibility_uom: + return self._weather_option_visibility_uom + + if self.hass.config.units.is_metric: + return LENGTH_KILOMETERS + return LENGTH_MILES @property def forecast(self) -> list[Forecast] | None: @@ -320,9 +334,21 @@ class WeatherEntity(Entity): return self._attr_forecast @property - def precipitation_unit(self) -> str | None: + def native_precipitation_unit(self) -> str | None: """Return the native unit of measurement for accumulated precipitation.""" - return self._attr_precipitation_unit + if hasattr(self, "_attr_native_precipitation_unit"): + return self._attr_native_precipitation_unit + return None + + @property + def precipitation_unit(self) -> str | None: + """Return the unit of measurement for precipitation.""" + if self._weather_option_precipitation_uom: + return self._weather_option_precipitation_uom + + if self.hass.config.units.is_metric: + return LENGTH_MILLIMETERS + return LENGTH_INCHES @property def precision(self) -> float: @@ -331,7 +357,7 @@ class WeatherEntity(Entity): return self._attr_precision return ( PRECISION_TENTHS - if self.hass.config.units.temperature_unit == TEMP_CELSIUS + if self.temperature_unit == TEMP_CELSIUS else PRECISION_WHOLE ) @@ -340,13 +366,26 @@ class WeatherEntity(Entity): def state_attributes(self): """Return the state attributes, converted from native units to user-configured units.""" data = {} - if self.temperature is not None: - data[ATTR_WEATHER_TEMPERATURE] = show_temp( - self.hass, - self.temperature, - self.temperature_unit, - self.precision, - ) + + precision_str = str(self.precision) + precision = ( + len(precision_str) - precision_str.index(".") - 1 + if "." in precision_str + else 0 + ) + + if (temperature := self.temperature) is not None: + with suppress(ValueError): + float(temperature) + value_temp = UNIT_CONVERSIONS[CONF_TEMPERATURE_UOM]( + temperature, self.native_temperature_unit, self.temperature_unit + ) + data[ATTR_WEATHER_TEMPERATURE] = ( + round(value_temp) + if precision == 0 + else round(value_temp, precision) + ) + data[ATTR_WEATHER_TEMPERATURE_UNIT] = self.temperature_unit if (humidity := self.humidity) is not None: data[ATTR_WEATHER_HUMIDITY] = round(humidity) @@ -355,78 +394,105 @@ class WeatherEntity(Entity): data[ATTR_WEATHER_OZONE] = ozone if (pressure := self.pressure) is not None: - if (unit := self.pressure_unit) is not None: - pressure = round( - self.hass.config.units.pressure(pressure, unit), ROUNDING_PRECISION + with suppress(ValueError): + float(pressure) + value_pressure = UNIT_CONVERSIONS[CONF_PRESSURE_UOM]( + pressure, self.native_pressure_unit, self.pressure_unit ) - data[ATTR_WEATHER_PRESSURE] = pressure + data[ATTR_WEATHER_PRESSURE] = round(value_pressure, ROUNDING_PRECISION) + data[ATTR_WEATHER_PRESSURE_UNIT] = self.pressure_unit if (wind_bearing := self.wind_bearing) is not None: data[ATTR_WEATHER_WIND_BEARING] = wind_bearing if (wind_speed := self.wind_speed) is not None: - if (unit := self.wind_speed_unit) is not None: - wind_speed = round( - self.hass.config.units.wind_speed(wind_speed, unit), - ROUNDING_PRECISION, + with suppress(ValueError): + float(wind_speed) + value_wind_speed = UNIT_CONVERSIONS[CONF_WIND_SPEED_UOM]( + wind_speed, self.native_wind_speed_unit, self.wind_speed_unit ) - data[ATTR_WEATHER_WIND_SPEED] = wind_speed + data[ATTR_WEATHER_WIND_SPEED] = round( + value_wind_speed, ROUNDING_PRECISION + ) + data[ATTR_WEATHER_WIND_SPEED_UNIT] = self.wind_speed_unit if (visibility := self.visibility) is not None: - if (unit := self.visibility_unit) is not None: - visibility = round( - self.hass.config.units.length(visibility, unit), ROUNDING_PRECISION + with suppress(ValueError): + float(visibility) + value_visibility = UNIT_CONVERSIONS[CONF_VISIBILITY_UOM]( + visibility, self.native_visibility_unit, self.visibility_unit ) - data[ATTR_WEATHER_VISIBILITY] = visibility + data[ATTR_WEATHER_VISIBILITY] = round( + value_visibility, ROUNDING_PRECISION + ) + data[ATTR_WEATHER_VISIBILITY_UNIT] = self.visibility_unit + + if precipitation_unit := self.precipitation_unit: + data[ATTR_WEATHER_PRECIPITATION_UNIT] = precipitation_unit if self.forecast is not None: forecast = [] for forecast_entry in self.forecast: + forecast_entry_new = {} forecast_entry = dict(forecast_entry) - forecast_entry[ATTR_FORECAST_TEMP] = show_temp( - self.hass, - forecast_entry[ATTR_FORECAST_TEMP], - self.temperature_unit, - self.precision, - ) - if ATTR_FORECAST_TEMP_LOW in forecast_entry: - forecast_entry[ATTR_FORECAST_TEMP_LOW] = show_temp( - self.hass, - forecast_entry[ATTR_FORECAST_TEMP_LOW], + temperature = forecast_entry[ATTR_FORECAST_TEMP] + + with suppress(ValueError): + value_temp = UNIT_CONVERSIONS[CONF_TEMPERATURE_UOM]( + temperature, + self.native_temperature_unit, self.temperature_unit, - self.precision, ) - if ( - native_pressure := forecast_entry.get(ATTR_FORECAST_PRESSURE) - ) is not None: - if (unit := self.pressure_unit) is not None: - pressure = round( - self.hass.config.units.pressure(native_pressure, unit), - ROUNDING_PRECISION, - ) - forecast_entry[ATTR_FORECAST_PRESSURE] = pressure - if ( - native_wind_speed := forecast_entry.get(ATTR_FORECAST_WIND_SPEED) - ) is not None: - if (unit := self.wind_speed_unit) is not None: - wind_speed = round( - self.hass.config.units.wind_speed(native_wind_speed, unit), - ROUNDING_PRECISION, - ) - forecast_entry[ATTR_FORECAST_WIND_SPEED] = wind_speed - if ( - native_precip := forecast_entry.get(ATTR_FORECAST_PRECIPITATION) - ) is not None: - if (unit := self.precipitation_unit) is not None: - precipitation = round( - self.hass.config.units.accumulated_precipitation( - native_precip, unit + forecast_entry_new[ATTR_FORECAST_TEMP] = ( + round(value_temp) + if precision == 0 + else round(value_temp, precision) + ) + + if temp_low := forecast_entry.get(ATTR_FORECAST_TEMP_LOW): + with suppress(ValueError): + forecast_entry_new[ATTR_FORECAST_TEMP_LOW] = round( + UNIT_CONVERSIONS[CONF_TEMPERATURE_UOM]( + temp_low, + self.native_temperature_unit, + self.temperature_unit, ), ROUNDING_PRECISION, ) - forecast_entry[ATTR_FORECAST_PRECIPITATION] = precipitation - forecast.append(forecast_entry) + if pressure := forecast_entry.get(ATTR_FORECAST_PRESSURE): + with suppress(ValueError): + forecast_entry_new[ATTR_FORECAST_PRESSURE] = round( + UNIT_CONVERSIONS[CONF_PRESSURE_UOM]( + pressure, + self.native_pressure_unit, + self.pressure_unit, + ), + ROUNDING_PRECISION, + ) + if wind_speed := forecast_entry.get(ATTR_FORECAST_WIND_SPEED): + with suppress(ValueError): + forecast_entry_new[ATTR_FORECAST_WIND_SPEED] = round( + UNIT_CONVERSIONS[CONF_WIND_SPEED_UOM]( + wind_speed, + self.native_wind_speed_unit, + self.wind_speed_unit, + ), + ROUNDING_PRECISION, + ) + + if precipitation := forecast_entry.get(ATTR_FORECAST_PRECIPITATION): + with suppress(ValueError): + forecast_entry_new[ATTR_FORECAST_PRECIPITATION] = round( + UNIT_CONVERSIONS[CONF_PRECIPITATION_UOM]( + precipitation, + self.native_precipitation_unit, + self.precipitation_unit, + ), + ROUNDING_PRECISION, + ) + + forecast.append({**forecast_entry, **forecast_entry_new}) data[ATTR_FORECAST] = forecast @@ -448,13 +514,44 @@ class WeatherEntity(Entity): """Run when the entity registry entry has been updated.""" assert self.registry_entry weather_options = self.registry_entry.options.get(DOMAIN) - if ( - weather_options - and (custom_unit_temperature := weather_options.get(CONF_TEMPERATURE_UOM)) - and self.native_temperature_unit in VALID_UNITS[CONF_TEMPERATURE_UOM] - and custom_unit_temperature in VALID_UNITS[CONF_TEMPERATURE_UOM] - ): - self._weather_option_temperature_uom = custom_unit_temperature - return - self._weather_option_temperature_uom = None + self._weather_option_pressure_uom = None + self._weather_option_precipitation_uom = None + self._weather_option_wind_speed_uom = None + self._weather_option_visibility_uom = None + if weather_options := self.registry_entry.options.get(DOMAIN): + if ( + (custom_unit_temperature := weather_options.get(CONF_TEMPERATURE_UOM)) + and custom_unit_temperature in VALID_UNITS[CONF_TEMPERATURE_UOM] + and self.native_temperature_unit in VALID_UNITS[CONF_TEMPERATURE_UOM] + ): + self._weather_option_temperature_uom = custom_unit_temperature + if ( + (custom_unit_pressure := weather_options.get(CONF_PRESSURE_UOM)) + and custom_unit_pressure in VALID_UNITS[CONF_PRESSURE_UOM] + and self.native_pressure_unit in VALID_UNITS[CONF_PRESSURE_UOM] + ): + self._weather_option_pressure_uom = custom_unit_pressure + if ( + ( + custom_unit_precipitation := weather_options.get( + CONF_PRECIPITATION_UOM + ) + ) + and custom_unit_precipitation in VALID_UNITS[CONF_PRECIPITATION_UOM] + and self.native_precipitation_unit + in VALID_UNITS[CONF_PRECIPITATION_UOM] + ): + self._weather_option_precipitation_uom = custom_unit_precipitation + if ( + (custom_unit_wind_speed := weather_options.get(CONF_WIND_SPEED_UOM)) + and custom_unit_wind_speed in VALID_UNITS[CONF_WIND_SPEED_UOM] + and self.native_wind_speed_unit in VALID_UNITS[CONF_WIND_SPEED_UOM] + ): + self._weather_option_wind_speed_uom = custom_unit_wind_speed + if ( + (custom_unit_visibility := weather_options.get(CONF_VISIBILITY_UOM)) + and custom_unit_visibility in VALID_UNITS[CONF_VISIBILITY_UOM] + and self.native_visibility_unit in VALID_UNITS[CONF_VISIBILITY_UOM] + ): + self._weather_option_visibility_uom = custom_unit_visibility From 91460f1bc492b84075e32fe0d14adf4bbbba0f0f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 14 Jun 2022 19:42:44 +0000 Subject: [PATCH 04/27] Change to native --- homeassistant/components/weather/__init__.py | 32 ++++++++++---------- tests/components/weather/test_init.py | 27 +++++++++-------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index a652bf00a58..d57fc8da809 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -189,7 +189,7 @@ class WeatherEntity(Entity): _attr_humidity: float | None = None _attr_ozone: float | None = None _attr_precision: float - _attr_pressure: float | None = None + _attr_native_pressure: float | None = None _attr_pressure_unit: None = None # Subclasses of WeatherEntity should not set this _attr_native_pressure_unit: str | None = None _attr_state: None = None @@ -197,8 +197,8 @@ class WeatherEntity(Entity): None # Subclasses of WeatherEntity should not set this ) _attr_native_temperature_unit: str - _attr_temperature: float | None - _attr_visibility: float | None = None + _attr_native_temperature: float | None + _attr_native_visibility: float | None = None _attr_visibility_unit: None = ( None # Subclasses of WeatherEntity should not set this ) @@ -208,7 +208,7 @@ class WeatherEntity(Entity): ) _attr_native_precipitation_unit: str | None = None _attr_wind_bearing: float | str | None = None - _attr_wind_speed: float | None = None + _attr_native_wind_speed: float | None = None _attr_wind_speed_unit: None = ( None # Subclasses of WeatherEntity should not set this ) @@ -228,9 +228,9 @@ class WeatherEntity(Entity): self.async_registry_entry_updated() @property - def temperature(self) -> float | None: + def native_temperature(self) -> float | None: """Return the platform temperature in native units (i.e. not converted).""" - return self._attr_temperature + return self._attr_native_temperature @property def native_temperature_unit(self) -> str | None: @@ -248,9 +248,9 @@ class WeatherEntity(Entity): return self.hass.config.units.temperature_unit @property - def pressure(self) -> float | None: + def native_pressure(self) -> float | None: """Return the pressure in native units.""" - return self._attr_pressure + return self._attr_native_pressure @property def native_pressure_unit(self) -> str | None: @@ -275,9 +275,9 @@ class WeatherEntity(Entity): return self._attr_humidity @property - def wind_speed(self) -> float | None: + def native_wind_speed(self) -> float | None: """Return the wind speed in native units.""" - return self._attr_wind_speed + return self._attr_native_wind_speed @property def native_wind_speed_unit(self) -> str | None: @@ -307,9 +307,9 @@ class WeatherEntity(Entity): return self._attr_ozone @property - def visibility(self) -> float | None: + def native_visibility(self) -> float | None: """Return the visibility in native units.""" - return self._attr_visibility + return self._attr_native_visibility @property def native_visibility_unit(self) -> str | None: @@ -374,7 +374,7 @@ class WeatherEntity(Entity): else 0 ) - if (temperature := self.temperature) is not None: + if (temperature := self.native_temperature) is not None: with suppress(ValueError): float(temperature) value_temp = UNIT_CONVERSIONS[CONF_TEMPERATURE_UOM]( @@ -393,7 +393,7 @@ class WeatherEntity(Entity): if (ozone := self.ozone) is not None: data[ATTR_WEATHER_OZONE] = ozone - if (pressure := self.pressure) is not None: + if (pressure := self.native_pressure) is not None: with suppress(ValueError): float(pressure) value_pressure = UNIT_CONVERSIONS[CONF_PRESSURE_UOM]( @@ -405,7 +405,7 @@ class WeatherEntity(Entity): if (wind_bearing := self.wind_bearing) is not None: data[ATTR_WEATHER_WIND_BEARING] = wind_bearing - if (wind_speed := self.wind_speed) is not None: + if (wind_speed := self.native_wind_speed) is not None: with suppress(ValueError): float(wind_speed) value_wind_speed = UNIT_CONVERSIONS[CONF_WIND_SPEED_UOM]( @@ -416,7 +416,7 @@ class WeatherEntity(Entity): ) data[ATTR_WEATHER_WIND_SPEED_UNIT] = self.wind_speed_unit - if (visibility := self.visibility) is not None: + if (visibility := self.native_visibility) is not None: with suppress(ValueError): float(visibility) value_visibility = UNIT_CONVERSIONS[CONF_VISIBILITY_UOM]( diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 9849a6abe18..ca2ed9153ac 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -22,15 +22,16 @@ from homeassistant.const import ( SPEED_METERS_PER_SECOND, TEMP_FAHRENHEIT, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure from homeassistant.util.speed import convert as convert_speed from homeassistant.util.temperature import convert as convert_temperature -from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem -async def create_entity(hass, **kwargs): +async def create_entity(hass: HomeAssistant, **kwargs): """Create the weather entity to run tests on.""" kwargs = {"temperature": None, "temperature_unit": None, **kwargs} platform = getattr(hass.components, "test.weather") @@ -51,9 +52,9 @@ async def create_entity(hass, **kwargs): @pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) async def test_temperature_conversion( - hass, + hass: HomeAssistant, enable_custom_integrations, - unit_system, + unit_system: UnitSystem, ): """Test temperature conversion.""" hass.config.units = unit_system @@ -79,9 +80,9 @@ async def test_temperature_conversion( @pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) async def test_pressure_conversion( - hass, + hass: HomeAssistant, enable_custom_integrations, - unit_system, + unit_system: UnitSystem, ): """Test pressure conversion.""" hass.config.units = unit_system @@ -101,9 +102,9 @@ async def test_pressure_conversion( @pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) async def test_wind_speed_conversion( - hass, + hass: HomeAssistant, enable_custom_integrations, - unit_system, + unit_system: UnitSystem, ): """Test wind speed conversion.""" hass.config.units = unit_system @@ -126,9 +127,9 @@ async def test_wind_speed_conversion( @pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) async def test_visibility_conversion( - hass, + hass: HomeAssistant, enable_custom_integrations, - unit_system, + unit_system: UnitSystem, ): """Test visibility conversion.""" hass.config.units = unit_system @@ -148,9 +149,9 @@ async def test_visibility_conversion( @pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) async def test_precipitation_conversion( - hass, + hass: HomeAssistant, enable_custom_integrations, - unit_system, + unit_system: UnitSystem, ): """Test precipitation conversion.""" hass.config.units = unit_system @@ -171,7 +172,7 @@ async def test_precipitation_conversion( async def test_none_forecast( - hass, + hass: HomeAssistant, enable_custom_integrations, ): """Test that conversion with None values succeeds.""" From 37849143005c7902795709bc887bdfac18c9fa33 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 15 Jun 2022 09:27:01 +0000 Subject: [PATCH 05/27] wip fix tests --- homeassistant/components/demo/weather.py | 51 ++++++-- homeassistant/components/weather/__init__.py | 25 +++- tests/components/weather/test_init.py | 119 ++++++++++++------ .../custom_components/test/weather.py | 51 ++++---- 4 files changed, 169 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/demo/weather.py b/homeassistant/components/demo/weather.py index 916083c5ad1..eed3e970b12 100644 --- a/homeassistant/components/demo/weather.py +++ b/homeassistant/components/demo/weather.py @@ -27,7 +27,14 @@ from homeassistant.components.weather import ( WeatherEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.const import ( + PRESSURE_HPA, + PRESSURE_INHG, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType @@ -77,6 +84,8 @@ def setup_platform( 1099, 0.5, TEMP_CELSIUS, + PRESSURE_HPA, + SPEED_METERS_PER_SECOND, [ [ATTR_CONDITION_RAINY, 1, 22, 15, 60], [ATTR_CONDITION_RAINY, 5, 19, 8, 30], @@ -95,6 +104,8 @@ def setup_platform( 987, 4.8, TEMP_FAHRENHEIT, + PRESSURE_INHG, + SPEED_MILES_PER_HOUR, [ [ATTR_CONDITION_SNOWY, 2, -10, -15, 60], [ATTR_CONDITION_PARTLYCLOUDY, 1, -13, -14, 25], @@ -121,16 +132,20 @@ class DemoWeather(WeatherEntity): pressure, wind_speed, temperature_unit, + pressure_unit, + wind_speed_unit, forecast, ): """Initialize the Demo weather.""" self._name = name self._condition = condition - self._temperature = temperature - self._temperature_unit = temperature_unit + self._native_temperature = temperature + self._native_temperature_unit = temperature_unit self._humidity = humidity - self._pressure = pressure - self._wind_speed = wind_speed + self._native_pressure = pressure + self._native_pressure_unit = pressure_unit + self._native_wind_speed = wind_speed + self._native_wind_speed_unit = wind_speed_unit self._forecast = forecast @property @@ -144,14 +159,14 @@ class DemoWeather(WeatherEntity): return False @property - def temperature(self): + def native_temperature(self): """Return the temperature.""" - return self._temperature + return self._native_temperature @property - def temperature_unit(self): + def native_temperature_unit(self): """Return the unit of measurement.""" - return self._temperature_unit + return self._native_temperature_unit @property def humidity(self): @@ -159,14 +174,24 @@ class DemoWeather(WeatherEntity): return self._humidity @property - def wind_speed(self): + def native_wind_speed(self): """Return the wind speed.""" - return self._wind_speed + return self._native_wind_speed @property - def pressure(self): + def native_wind_speed_unit(self): + """Return the wind speed.""" + return self._native_wind_speed_unit + + @property + def native_pressure(self): """Return the pressure.""" - return self._pressure + return self._native_pressure + + @property + def native_pressure_unit(self): + """Return the pressure.""" + return self._native_pressure_unit @property def condition(self): diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index d57fc8da809..6e98e1287db 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -82,6 +82,7 @@ ATTR_WEATHER_VISIBILITY_UNIT = "visibility_unit" ATTR_WEATHER_WIND_BEARING = "wind_bearing" ATTR_WEATHER_WIND_SPEED = "wind_speed" ATTR_WEATHER_WIND_SPEED_UNIT = "wind_speed_unit" +ATTR_WEATHER_PRECIPITATION = "precipitation" ATTR_WEATHER_PRECIPITATION_UNIT = "precipitation_unit" @@ -197,12 +198,13 @@ class WeatherEntity(Entity): None # Subclasses of WeatherEntity should not set this ) _attr_native_temperature_unit: str - _attr_native_temperature: float | None + _attr_native_temperature: float _attr_native_visibility: float | None = None _attr_visibility_unit: None = ( None # Subclasses of WeatherEntity should not set this ) _attr_native_visibility_unit: str | None = None + _attr_native_precipitation: float | None = None _attr_precipitation_unit: None = ( None # Subclasses of WeatherEntity should not set this ) @@ -228,7 +230,7 @@ class WeatherEntity(Entity): self.async_registry_entry_updated() @property - def native_temperature(self) -> float | None: + def native_temperature(self) -> float: """Return the platform temperature in native units (i.e. not converted).""" return self._attr_native_temperature @@ -333,6 +335,11 @@ class WeatherEntity(Entity): """Return the forecast in native units.""" return self._attr_forecast + @property + def native_precipitation(self) -> float | None: + """Return the precipitation in native units.""" + return self._attr_native_precipitation + @property def native_precipitation_unit(self) -> str | None: """Return the native unit of measurement for accumulated precipitation.""" @@ -427,8 +434,18 @@ class WeatherEntity(Entity): ) data[ATTR_WEATHER_VISIBILITY_UNIT] = self.visibility_unit - if precipitation_unit := self.precipitation_unit: - data[ATTR_WEATHER_PRECIPITATION_UNIT] = precipitation_unit + if (precipitation := self.native_precipitation) is not None: + with suppress(ValueError): + float(precipitation) + value_precipitation = UNIT_CONVERSIONS[CONF_PRECIPITATION_UOM]( + precipitation, + self.native_precipitation_unit, + self.precipitation_unit, + ) + data[ATTR_WEATHER_PRECIPITATION] = round( + value_precipitation, ROUNDING_PRECISION + ) + data[ATTR_WEATHER_PRECIPITATION_UNIT] = self.precipitation_unit if self.forecast is not None: forecast = [] diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index ca2ed9153ac..0364066f025 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -16,13 +16,19 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_SPEED, ) from homeassistant.const import ( + LENGTH_INCHES, + LENGTH_KILOMETERS, LENGTH_MILES, LENGTH_MILLIMETERS, + PRESSURE_HPA, PRESSURE_INHG, SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + TEMP_CELSIUS, TEMP_FAHRENHEIT, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure @@ -30,11 +36,13 @@ from homeassistant.util.speed import convert as convert_speed from homeassistant.util.temperature import convert as convert_temperature from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem +from tests.testing_config.custom_components.test import weather as WeatherPlatform + async def create_entity(hass: HomeAssistant, **kwargs): """Create the weather entity to run tests on.""" - kwargs = {"temperature": None, "temperature_unit": None, **kwargs} - platform = getattr(hass.components, "test.weather") + kwargs = {"native_temperature": None, "native_temperature_unit": None, **kwargs} + platform: WeatherPlatform = getattr(hass.components, "test.weather") platform.init(empty=True) platform.ENTITIES.append( platform.MockWeatherMockForecast( @@ -52,9 +60,7 @@ async def create_entity(hass: HomeAssistant, **kwargs): @pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) async def test_temperature_conversion( - hass: HomeAssistant, - enable_custom_integrations, - unit_system: UnitSystem, + hass: HomeAssistant, enable_custom_integrations, unit_system: UnitSystem ): """Test temperature conversion.""" hass.config.units = unit_system @@ -62,15 +68,15 @@ async def test_temperature_conversion( native_unit = TEMP_FAHRENHEIT entity0 = await create_entity( - hass, temperature=native_value, temperature_unit=native_unit + hass, native_temperature=native_value, native_temperature_unit=native_unit ) state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - expected = convert_temperature( - native_value, native_unit, unit_system.temperature_unit - ) + expected = convert_temperature(native_value, native_unit, TEMP_FAHRENHEIT) + if unit_system == METRIC_SYSTEM: + expected = convert_temperature(native_value, native_unit, TEMP_CELSIUS) assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( expected, rel=0.1 ) @@ -80,9 +86,7 @@ async def test_temperature_conversion( @pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) async def test_pressure_conversion( - hass: HomeAssistant, - enable_custom_integrations, - unit_system: UnitSystem, + hass: HomeAssistant, enable_custom_integrations, unit_system: UnitSystem ): """Test pressure conversion.""" hass.config.units = unit_system @@ -90,21 +94,21 @@ async def test_pressure_conversion( native_unit = PRESSURE_INHG entity0 = await create_entity( - hass, pressure=native_value, pressure_unit=native_unit + hass, native_pressure=native_value, native_pressure_unit=native_unit ) state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - expected = convert_pressure(native_value, native_unit, unit_system.pressure_unit) + expected = convert_pressure(native_value, native_unit, PRESSURE_INHG) + if unit_system == METRIC_SYSTEM: + expected = convert_pressure(native_value, native_unit, PRESSURE_HPA) assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == approx(expected, rel=1e-2) assert float(forecast[ATTR_FORECAST_PRESSURE]) == approx(expected, rel=1e-2) @pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) async def test_wind_speed_conversion( - hass: HomeAssistant, - enable_custom_integrations, - unit_system: UnitSystem, + hass: HomeAssistant, enable_custom_integrations, unit_system: UnitSystem ): """Test wind speed conversion.""" hass.config.units = unit_system @@ -112,13 +116,15 @@ async def test_wind_speed_conversion( native_unit = SPEED_METERS_PER_SECOND entity0 = await create_entity( - hass, wind_speed=native_value, wind_speed_unit=native_unit + hass, native_wind_speed=native_value, native_wind_speed_unit=native_unit ) state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - expected = convert_speed(native_value, native_unit, unit_system.wind_speed_unit) + expected = convert_speed(native_value, native_unit, SPEED_MILES_PER_HOUR) + if unit_system == METRIC_SYSTEM: + expected = convert_speed(native_value, native_unit, SPEED_METERS_PER_SECOND) assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx( expected, rel=1e-2 ) @@ -127,9 +133,7 @@ async def test_wind_speed_conversion( @pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) async def test_visibility_conversion( - hass: HomeAssistant, - enable_custom_integrations, - unit_system: UnitSystem, + hass: HomeAssistant, enable_custom_integrations, unit_system: UnitSystem ): """Test visibility conversion.""" hass.config.units = unit_system @@ -137,11 +141,13 @@ async def test_visibility_conversion( native_unit = LENGTH_MILES entity0 = await create_entity( - hass, visibility=native_value, visibility_unit=native_unit + hass, native_visibility=native_value, native_visibility_unit=native_unit ) state = hass.states.get(entity0.entity_id) - expected = convert_distance(native_value, native_unit, unit_system.length_unit) + expected = convert_distance(native_value, native_unit, LENGTH_MILES) + if unit_system == METRIC_SYSTEM: + expected = convert_distance(native_value, native_unit, LENGTH_KILOMETERS) assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx( expected, rel=1e-2 ) @@ -149,9 +155,7 @@ async def test_visibility_conversion( @pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) async def test_precipitation_conversion( - hass: HomeAssistant, - enable_custom_integrations, - unit_system: UnitSystem, + hass: HomeAssistant, enable_custom_integrations, unit_system: UnitSystem ): """Test precipitation conversion.""" hass.config.units = unit_system @@ -159,15 +163,15 @@ async def test_precipitation_conversion( native_unit = LENGTH_MILLIMETERS entity0 = await create_entity( - hass, precipitation=native_value, precipitation_unit=native_unit + hass, native_precipitation=native_value, native_precipitation_unit=native_unit ) state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - expected = convert_distance( - native_value, native_unit, unit_system.accumulated_precipitation_unit - ) + expected = convert_distance(native_value, native_unit, LENGTH_INCHES) + if unit_system == METRIC_SYSTEM: + expected = convert_distance(native_value, native_unit, LENGTH_MILLIMETERS) assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx(expected, rel=1e-2) @@ -178,12 +182,12 @@ async def test_none_forecast( """Test that conversion with None values succeeds.""" entity0 = await create_entity( hass, - pressure=None, - pressure_unit=PRESSURE_INHG, - wind_speed=None, - wind_speed_unit=SPEED_METERS_PER_SECOND, - precipitation=None, - precipitation_unit=LENGTH_MILLIMETERS, + native_pressure=None, + native_pressure_unit=PRESSURE_INHG, + native_wind_speed=None, + native_wind_speed_unit=SPEED_METERS_PER_SECOND, + native_precipitation=None, + native_precipitation_unit=LENGTH_MILLIMETERS, ) state = hass.states.get(entity0.entity_id) @@ -192,3 +196,44 @@ async def test_none_forecast( assert forecast[ATTR_FORECAST_PRESSURE] is None assert forecast[ATTR_FORECAST_WIND_SPEED] is None assert forecast[ATTR_FORECAST_PRECIPITATION] is None + + +async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> None: + """Test custom unit.""" + native_value = 5 + native_unit = SPEED_METERS_PER_SECOND + custom_unit = SPEED_MILES_PER_HOUR + + entity_registry = er.async_get(hass) + + entry = entity_registry.async_get_or_create("weather", "test", "very_unique") + entity_registry.async_update_entity_options( + entry.entity_id, "weather", {"wind_speed_unit_of_measurement": custom_unit} + ) + await hass.async_block_till_done() + + platform: WeatherPlatform = getattr(hass.components, "test.weather") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockWeather( + name="Test", + condition=ATTR_CONDITION_SUNNY, + native_temperature=None, + native_temperature_unit=None, + native_wind_speed=native_value, + native_wind_speed_unit=native_unit, + unique_id="very_unique", + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component( + hass, "weather", {"weather": {"platform": "test"}} + ) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + expected = convert_speed(native_value, native_unit, SPEED_MILES_PER_HOUR) + assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx( + expected, rel=1e-2 + ) diff --git a/tests/testing_config/custom_components/test/weather.py b/tests/testing_config/custom_components/test/weather.py index 224d6495548..f57d270980d 100644 --- a/tests/testing_config/custom_components/test/weather.py +++ b/tests/testing_config/custom_components/test/weather.py @@ -38,24 +38,24 @@ class MockWeather(MockEntity, WeatherEntity): """Mock weather class.""" @property - def temperature(self) -> float | None: + def native_temperature(self) -> float | None: """Return the platform temperature.""" - return self._handle("temperature") + return self._handle("native_temperature") @property - def temperature_unit(self) -> str | None: + def native_temperature_unit(self) -> str | None: """Return the unit of measurement for temperature.""" - return self._handle("temperature_unit") + return self._handle("native_temperature_unit") @property - def pressure(self) -> float | None: + def native_pressure(self) -> float | None: """Return the pressure.""" - return self._handle("pressure") + return self._handle("native_pressure") @property - def pressure_unit(self) -> str | None: + def native_pressure_unit(self) -> str | None: """Return the unit of measurement for pressure.""" - return self._handle("pressure_unit") + return self._handle("native_pressure_unit") @property def humidity(self) -> float | None: @@ -63,14 +63,14 @@ class MockWeather(MockEntity, WeatherEntity): return self._handle("humidity") @property - def wind_speed(self) -> float | None: + def native_wind_speed(self) -> float | None: """Return the wind speed.""" - return self._handle("wind_speed") + return self._handle("native_wind_speed") @property - def wind_speed_unit(self) -> str | None: + def native_wind_speed_unit(self) -> str | None: """Return the unit of measurement for wind speed.""" - return self._handle("wind_speed_unit") + return self._handle("native_wind_speed_unit") @property def wind_bearing(self) -> float | str | None: @@ -83,14 +83,14 @@ class MockWeather(MockEntity, WeatherEntity): return self._handle("ozone") @property - def visibility(self) -> float | None: + def native_visibility(self) -> float | None: """Return the visibility.""" - return self._handle("visibility") + return self._handle("native_visibility") @property - def visibility_unit(self) -> str | None: + def native_visibility_unit(self) -> str | None: """Return the unit of measurement for visibility.""" - return self._handle("visibility_unit") + return self._handle("native_visibility_unit") @property def forecast(self) -> list[Forecast] | None: @@ -98,9 +98,14 @@ class MockWeather(MockEntity, WeatherEntity): return self._handle("forecast") @property - def precipitation_unit(self) -> str | None: + def native_precipitation(self) -> float | None: + """Return the accumulated precipitation.""" + return self._handle("native_precipitation") + + @property + def native_precipitation_unit(self) -> str | None: """Return the native unit of measurement for accumulated precipitation.""" - return self._handle("precipitation_unit") + return self._handle("native_precipitation_unit") @property def condition(self) -> str | None: @@ -116,11 +121,11 @@ class MockWeatherMockForecast(MockWeather): """Return the forecast.""" return [ { - ATTR_FORECAST_TEMP: self.temperature, - ATTR_FORECAST_TEMP_LOW: self.temperature, - ATTR_FORECAST_PRESSURE: self.pressure, - ATTR_FORECAST_WIND_SPEED: self.wind_speed, + ATTR_FORECAST_TEMP: self.native_temperature, + ATTR_FORECAST_TEMP_LOW: self.native_temperature, + ATTR_FORECAST_PRESSURE: self.native_pressure, + ATTR_FORECAST_WIND_SPEED: self.native_wind_speed, ATTR_FORECAST_WIND_BEARING: self.wind_bearing, - ATTR_FORECAST_PRECIPITATION: self._values.get("precipitation"), + ATTR_FORECAST_PRECIPITATION: self.native_precipitation, } ] From e1ca64378560151f83303959da98421b7bd33235 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 15 Jun 2022 20:56:28 +0000 Subject: [PATCH 06/27] From review and other changes --- homeassistant/components/weather/__init__.py | 273 ++++++++++++------- tests/components/weather/test_init.py | 155 ++++++----- 2 files changed, 268 insertions(+), 160 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 6e98e1287db..f9c6a283ae6 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -6,7 +6,7 @@ from contextlib import suppress from dataclasses import dataclass from datetime import timedelta import logging -from typing import Final, TypedDict, final +from typing import Any, Final, TypedDict, final from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -191,29 +191,21 @@ class WeatherEntity(Entity): _attr_ozone: float | None = None _attr_precision: float _attr_native_pressure: float | None = None - _attr_pressure_unit: None = None # Subclasses of WeatherEntity should not set this + _attr_pressure_unit: str | None = None _attr_native_pressure_unit: str | None = None _attr_state: None = None - _attr_temperature_unit: None = ( - None # Subclasses of WeatherEntity should not set this - ) + _attr_temperature_unit: str | None = None _attr_native_temperature_unit: str _attr_native_temperature: float _attr_native_visibility: float | None = None - _attr_visibility_unit: None = ( - None # Subclasses of WeatherEntity should not set this - ) + _attr_visibility_unit: str | None = None _attr_native_visibility_unit: str | None = None _attr_native_precipitation: float | None = None - _attr_precipitation_unit: None = ( - None # Subclasses of WeatherEntity should not set this - ) + _attr_precipitation_unit: str | None = None _attr_native_precipitation_unit: str | None = None _attr_wind_bearing: float | str | None = None _attr_native_wind_speed: float | None = None - _attr_wind_speed_unit: None = ( - None # Subclasses of WeatherEntity should not set this - ) + _attr_wind_speed_unit: str | None = None _attr_native_wind_speed_unit: str | None = None _weather_option_temperature_uom: str | None = None @@ -222,6 +214,15 @@ class WeatherEntity(Entity): _weather_option_precipitation_uom: str | None = None _weather_option_wind_speed_uom: str | None = None + _deprecated_weather_entity_report = False + _deprecated_weather_entity_reported = False + + def __init_subclass__(cls, **kwargs: Any) -> None: + """Post initialisation processing.""" + super().__init_subclass__(**kwargs) + if "temperature_unit" in cls.__dict__: + cls._deprecated_weather_entity_report = True + async def async_internal_added_to_hass(self) -> None: """Call when the sensor entity is added to hass.""" await super().async_internal_added_to_hass() @@ -244,10 +245,19 @@ class WeatherEntity(Entity): @property def temperature_unit(self) -> str: """Return the unit of measurement for temperature.""" + if ( + self._deprecated_weather_entity_report is True + and self._deprecated_weather_entity_reported is False + ): + self._report_deprecated_weather_entity() + + if self.native_temperature_unit is None: + return None # type: ignore[return-value] + if self._weather_option_temperature_uom: return self._weather_option_temperature_uom - return self.hass.config.units.temperature_unit + return self.native_temperature_unit @property def native_pressure(self) -> float | None: @@ -264,12 +274,13 @@ class WeatherEntity(Entity): @property def pressure_unit(self) -> str | None: """Return the unit of measurement for pressure.""" + if self.native_pressure_unit is None: + return None + if self._weather_option_pressure_uom: return self._weather_option_pressure_uom - if self.hass.config.units.is_metric: - return PRESSURE_HPA - return PRESSURE_INHG + return self.native_pressure_unit @property def humidity(self) -> float | None: @@ -291,12 +302,13 @@ class WeatherEntity(Entity): @property def wind_speed_unit(self) -> str | None: """Return the unit of measurement for wind speed.""" + if self.native_wind_speed_unit is None: + return None + if self._weather_option_wind_speed_uom: return self._weather_option_wind_speed_uom - if self.hass.config.units.is_metric: - return SPEED_METERS_PER_SECOND - return SPEED_MILES_PER_HOUR + return self.native_wind_speed_unit @property def wind_bearing(self) -> float | str | None: @@ -323,12 +335,13 @@ class WeatherEntity(Entity): @property def visibility_unit(self) -> str | None: """Return the unit of measurement for visibility.""" + if self.native_visibility_unit is None: + return None + if self._weather_option_visibility_uom: return self._weather_option_visibility_uom - if self.hass.config.units.is_metric: - return LENGTH_KILOMETERS - return LENGTH_MILES + return self.native_visibility_unit @property def forecast(self) -> list[Forecast] | None: @@ -350,12 +363,13 @@ class WeatherEntity(Entity): @property def precipitation_unit(self) -> str | None: """Return the unit of measurement for precipitation.""" + if self.native_precipitation_unit is None: + return None + if self._weather_option_precipitation_uom: return self._weather_option_precipitation_uom - if self.hass.config.units.is_metric: - return LENGTH_MILLIMETERS - return LENGTH_INCHES + return self.native_precipitation_unit @property def precision(self) -> float: @@ -381,18 +395,22 @@ class WeatherEntity(Entity): else 0 ) - if (temperature := self.native_temperature) is not None: + if ( + (temperature := self.native_temperature) is not None + and (native_temp_unit := self.native_temperature_unit) is not None + and (temp_unit := self.temperature_unit) is not None + ): with suppress(ValueError): - float(temperature) + temperature_f = float(temperature) value_temp = UNIT_CONVERSIONS[CONF_TEMPERATURE_UOM]( - temperature, self.native_temperature_unit, self.temperature_unit + temperature_f, native_temp_unit, temp_unit ) data[ATTR_WEATHER_TEMPERATURE] = ( round(value_temp) if precision == 0 else round(value_temp, precision) ) - data[ATTR_WEATHER_TEMPERATURE_UNIT] = self.temperature_unit + data[ATTR_WEATHER_TEMPERATURE_UNIT] = temp_unit if (humidity := self.humidity) is not None: data[ATTR_WEATHER_HUMIDITY] = round(humidity) @@ -400,52 +418,69 @@ class WeatherEntity(Entity): if (ozone := self.ozone) is not None: data[ATTR_WEATHER_OZONE] = ozone - if (pressure := self.native_pressure) is not None: + if ( + (pressure := self.native_pressure) is not None + and (pressure_unit := self.pressure_unit) is not None + and (native_pressure_unit := self.native_pressure_unit) is not None + ): with suppress(ValueError): - float(pressure) + pressure_f = float(pressure) value_pressure = UNIT_CONVERSIONS[CONF_PRESSURE_UOM]( - pressure, self.native_pressure_unit, self.pressure_unit + pressure_f, native_pressure_unit, pressure_unit ) data[ATTR_WEATHER_PRESSURE] = round(value_pressure, ROUNDING_PRECISION) - data[ATTR_WEATHER_PRESSURE_UNIT] = self.pressure_unit + data[ATTR_WEATHER_PRESSURE_UNIT] = pressure_unit if (wind_bearing := self.wind_bearing) is not None: data[ATTR_WEATHER_WIND_BEARING] = wind_bearing - if (wind_speed := self.native_wind_speed) is not None: + if ( + (wind_speed := self.native_wind_speed) is not None + and (wind_speed_unit := self.wind_speed_unit) is not None + and (native_wind_speed_unit := self.native_wind_speed_unit) is not None + ): with suppress(ValueError): - float(wind_speed) + wind_speed_f = float(wind_speed) value_wind_speed = UNIT_CONVERSIONS[CONF_WIND_SPEED_UOM]( - wind_speed, self.native_wind_speed_unit, self.wind_speed_unit + wind_speed_f, native_wind_speed_unit, wind_speed_unit ) data[ATTR_WEATHER_WIND_SPEED] = round( value_wind_speed, ROUNDING_PRECISION ) - data[ATTR_WEATHER_WIND_SPEED_UNIT] = self.wind_speed_unit + data[ATTR_WEATHER_WIND_SPEED_UNIT] = wind_speed_unit - if (visibility := self.native_visibility) is not None: + if ( + (visibility := self.native_visibility) is not None + and (visibility_unit := self.visibility_unit) is not None + and (native_visibility_unit := self.native_visibility_unit) is not None + ): with suppress(ValueError): - float(visibility) + visibility_f = float(visibility) value_visibility = UNIT_CONVERSIONS[CONF_VISIBILITY_UOM]( - visibility, self.native_visibility_unit, self.visibility_unit + visibility_f, native_visibility_unit, visibility_unit ) data[ATTR_WEATHER_VISIBILITY] = round( value_visibility, ROUNDING_PRECISION ) - data[ATTR_WEATHER_VISIBILITY_UNIT] = self.visibility_unit + data[ATTR_WEATHER_VISIBILITY_UNIT] = visibility_unit - if (precipitation := self.native_precipitation) is not None: + if ( + (precipitation := self.native_precipitation) is not None + and (precipitation_unit := self.precipitation_unit) is not None + and (native_precipitation_unit := self.native_precipitation_unit) + is not None + ): with suppress(ValueError): - float(precipitation) + precipitation_f = float(precipitation) value_precipitation = UNIT_CONVERSIONS[CONF_PRECIPITATION_UOM]( - precipitation, - self.native_precipitation_unit, - self.precipitation_unit, + precipitation_f, + native_precipitation_unit, + precipitation_unit, ) data[ATTR_WEATHER_PRECIPITATION] = round( value_precipitation, ROUNDING_PRECISION ) - data[ATTR_WEATHER_PRECIPITATION_UNIT] = self.precipitation_unit + data[ATTR_WEATHER_PRECIPITATION_UNIT] = precipitation_unit if self.forecast is not None: forecast = [] @@ -453,61 +488,92 @@ class WeatherEntity(Entity): forecast_entry_new = {} forecast_entry = dict(forecast_entry) temperature = forecast_entry[ATTR_FORECAST_TEMP] - - with suppress(ValueError): - value_temp = UNIT_CONVERSIONS[CONF_TEMPERATURE_UOM]( - temperature, - self.native_temperature_unit, - self.temperature_unit, - ) - forecast_entry_new[ATTR_FORECAST_TEMP] = ( - round(value_temp) - if precision == 0 - else round(value_temp, precision) - ) + print(f"{self.temperature_unit} and {self.native_temperature_unit}") + if ( + self.temperature_unit is not None + and self.native_temperature_unit is not None + ): + with suppress(ValueError): + value_temp = UNIT_CONVERSIONS[CONF_TEMPERATURE_UOM]( + temperature, + self.native_temperature_unit, + self.temperature_unit, + ) + forecast_entry_new[ATTR_FORECAST_TEMP] = ( + round(value_temp) + if precision == 0 + else round(value_temp, precision) + ) + else: + forecast_entry_new[ATTR_FORECAST_TEMP] = temperature if temp_low := forecast_entry.get(ATTR_FORECAST_TEMP_LOW): - with suppress(ValueError): - forecast_entry_new[ATTR_FORECAST_TEMP_LOW] = round( - UNIT_CONVERSIONS[CONF_TEMPERATURE_UOM]( - temp_low, - self.native_temperature_unit, - self.temperature_unit, - ), - ROUNDING_PRECISION, - ) + if ( + self.temperature_unit is not None + and self.native_temperature_unit is not None + ): + with suppress(ValueError): + forecast_entry_new[ATTR_FORECAST_TEMP_LOW] = round( + UNIT_CONVERSIONS[CONF_TEMPERATURE_UOM]( + temp_low, + self.native_temperature_unit, + self.temperature_unit, + ), + ROUNDING_PRECISION, + ) + else: + forecast_entry_new[ATTR_FORECAST_TEMP_LOW] = temp_low if pressure := forecast_entry.get(ATTR_FORECAST_PRESSURE): - with suppress(ValueError): - forecast_entry_new[ATTR_FORECAST_PRESSURE] = round( - UNIT_CONVERSIONS[CONF_PRESSURE_UOM]( - pressure, - self.native_pressure_unit, - self.pressure_unit, - ), - ROUNDING_PRECISION, - ) + if ( + self.pressure_unit is not None + and self.native_pressure_unit is not None + ): + with suppress(ValueError): + forecast_entry_new[ATTR_FORECAST_PRESSURE] = round( + UNIT_CONVERSIONS[CONF_PRESSURE_UOM]( + pressure, + self.native_pressure_unit, + self.pressure_unit, + ), + ROUNDING_PRECISION, + ) + else: + forecast_entry_new[ATTR_FORECAST_PRESSURE] = pressure + if wind_speed := forecast_entry.get(ATTR_FORECAST_WIND_SPEED): - with suppress(ValueError): - forecast_entry_new[ATTR_FORECAST_WIND_SPEED] = round( - UNIT_CONVERSIONS[CONF_WIND_SPEED_UOM]( - wind_speed, - self.native_wind_speed_unit, - self.wind_speed_unit, - ), - ROUNDING_PRECISION, - ) + if ( + self.wind_speed_unit is not None + and self.native_wind_speed_unit is not None + ): + with suppress(ValueError): + forecast_entry_new[ATTR_FORECAST_WIND_SPEED] = round( + UNIT_CONVERSIONS[CONF_WIND_SPEED_UOM]( + wind_speed, + self.native_wind_speed_unit, + self.wind_speed_unit, + ), + ROUNDING_PRECISION, + ) + else: + forecast_entry_new[ATTR_FORECAST_WIND_SPEED] = wind_speed if precipitation := forecast_entry.get(ATTR_FORECAST_PRECIPITATION): - with suppress(ValueError): - forecast_entry_new[ATTR_FORECAST_PRECIPITATION] = round( - UNIT_CONVERSIONS[CONF_PRECIPITATION_UOM]( - precipitation, - self.native_precipitation_unit, - self.precipitation_unit, - ), - ROUNDING_PRECISION, - ) + if ( + self.precipitation_unit is not None + and self.native_precipitation_unit is not None + ): + with suppress(ValueError): + forecast_entry_new[ATTR_FORECAST_PRECIPITATION] = round( + UNIT_CONVERSIONS[CONF_PRECIPITATION_UOM]( + precipitation, + self.native_precipitation_unit, + self.precipitation_unit, + ), + ROUNDING_PRECISION, + ) + else: + forecast_entry_new[ATTR_FORECAST_PRECIPITATION] = precipitation forecast.append({**forecast_entry, **forecast_entry_new}) @@ -572,3 +638,16 @@ class WeatherEntity(Entity): and self.native_visibility_unit in VALID_UNITS[CONF_VISIBILITY_UOM] ): self._weather_option_visibility_uom = custom_unit_visibility + + def _report_deprecated_weather_entity(self) -> None: + """Report that the weather entity has not been upgraded.""" + if not self._deprecated_weather_entity_reported: + self._deprecated_weather_entity_reported = True + report_issue = self._suggest_report_issue() + _LOGGER.warning( + "Entity %s (%s) is using deprecated WeatherEntity features which will " + "be unsupported from Home Assistant Core 2022.10, please %s", + self.entity_id, + type(self), + report_issue, + ) diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 0364066f025..08a3a60ee69 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -10,10 +10,12 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_WIND_SPEED, + ATTR_WEATHER_PRECIPITATION, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_VISIBILITY, ATTR_WEATHER_WIND_SPEED, + ROUNDING_PRECISION, ) from homeassistant.const import ( LENGTH_INCHES, @@ -34,7 +36,6 @@ from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure from homeassistant.util.speed import convert as convert_speed from homeassistant.util.temperature import convert as convert_temperature -from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem from tests.testing_config.custom_components.test import weather as WeatherPlatform @@ -58,14 +59,11 @@ async def create_entity(hass: HomeAssistant, **kwargs): return entity0 -@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) -async def test_temperature_conversion( - hass: HomeAssistant, enable_custom_integrations, unit_system: UnitSystem -): - """Test temperature conversion.""" - hass.config.units = unit_system +@pytest.mark.parametrize("unit", [TEMP_FAHRENHEIT, TEMP_CELSIUS]) +async def test_temperature(hass: HomeAssistant, enable_custom_integrations, unit: str): + """Test temperature.""" native_value = 38 - native_unit = TEMP_FAHRENHEIT + native_unit = unit entity0 = await create_entity( hass, native_temperature=native_value, native_temperature_unit=native_unit @@ -74,9 +72,7 @@ async def test_temperature_conversion( state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - expected = convert_temperature(native_value, native_unit, TEMP_FAHRENHEIT) - if unit_system == METRIC_SYSTEM: - expected = convert_temperature(native_value, native_unit, TEMP_CELSIUS) + expected = native_value assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( expected, rel=0.1 ) @@ -84,14 +80,11 @@ async def test_temperature_conversion( assert float(forecast[ATTR_FORECAST_TEMP_LOW]) == approx(expected, rel=0.1) -@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) -async def test_pressure_conversion( - hass: HomeAssistant, enable_custom_integrations, unit_system: UnitSystem -): - """Test pressure conversion.""" - hass.config.units = unit_system +@pytest.mark.parametrize("unit", [PRESSURE_INHG, PRESSURE_HPA]) +async def test_pressure(hass: HomeAssistant, enable_custom_integrations, unit: str): + """Test pressure.""" native_value = 30 - native_unit = PRESSURE_INHG + native_unit = unit entity0 = await create_entity( hass, native_pressure=native_value, native_pressure_unit=native_unit @@ -99,21 +92,16 @@ async def test_pressure_conversion( state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - expected = convert_pressure(native_value, native_unit, PRESSURE_INHG) - if unit_system == METRIC_SYSTEM: - expected = convert_pressure(native_value, native_unit, PRESSURE_HPA) + expected = native_value assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == approx(expected, rel=1e-2) assert float(forecast[ATTR_FORECAST_PRESSURE]) == approx(expected, rel=1e-2) -@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) -async def test_wind_speed_conversion( - hass: HomeAssistant, enable_custom_integrations, unit_system: UnitSystem -): - """Test wind speed conversion.""" - hass.config.units = unit_system +@pytest.mark.parametrize("unit", [SPEED_MILES_PER_HOUR, SPEED_METERS_PER_SECOND]) +async def test_wind_speed(hass: HomeAssistant, enable_custom_integrations, unit: str): + """Test wind speed.""" native_value = 10 - native_unit = SPEED_METERS_PER_SECOND + native_unit = unit entity0 = await create_entity( hass, native_wind_speed=native_value, native_wind_speed_unit=native_unit @@ -122,45 +110,37 @@ async def test_wind_speed_conversion( state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - expected = convert_speed(native_value, native_unit, SPEED_MILES_PER_HOUR) - if unit_system == METRIC_SYSTEM: - expected = convert_speed(native_value, native_unit, SPEED_METERS_PER_SECOND) + expected = native_value assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx( expected, rel=1e-2 ) assert float(forecast[ATTR_FORECAST_WIND_SPEED]) == approx(expected, rel=1e-2) -@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) -async def test_visibility_conversion( - hass: HomeAssistant, enable_custom_integrations, unit_system: UnitSystem -): - """Test visibility conversion.""" - hass.config.units = unit_system +@pytest.mark.parametrize("unit", [LENGTH_KILOMETERS, LENGTH_MILES]) +async def test_visibility(hass: HomeAssistant, enable_custom_integrations, unit: str): + """Test visibility.""" native_value = 10 - native_unit = LENGTH_MILES + native_unit = unit entity0 = await create_entity( hass, native_visibility=native_value, native_visibility_unit=native_unit ) state = hass.states.get(entity0.entity_id) - expected = convert_distance(native_value, native_unit, LENGTH_MILES) - if unit_system == METRIC_SYSTEM: - expected = convert_distance(native_value, native_unit, LENGTH_KILOMETERS) + expected = native_value assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx( expected, rel=1e-2 ) -@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) -async def test_precipitation_conversion( - hass: HomeAssistant, enable_custom_integrations, unit_system: UnitSystem +@pytest.mark.parametrize("unit", [LENGTH_INCHES, LENGTH_MILLIMETERS]) +async def test_precipitation( + hass: HomeAssistant, enable_custom_integrations, unit: str ): - """Test precipitation conversion.""" - hass.config.units = unit_system + """Test precipitation.""" native_value = 30 - native_unit = LENGTH_MILLIMETERS + native_unit = unit entity0 = await create_entity( hass, native_precipitation=native_value, native_precipitation_unit=native_unit @@ -169,9 +149,10 @@ async def test_precipitation_conversion( state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - expected = convert_distance(native_value, native_unit, LENGTH_INCHES) - if unit_system == METRIC_SYSTEM: - expected = convert_distance(native_value, native_unit, LENGTH_MILLIMETERS) + expected = native_value + assert float(state.attributes[ATTR_WEATHER_PRECIPITATION]) == approx( + expected, rel=1e-2 + ) assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx(expected, rel=1e-2) @@ -200,16 +181,29 @@ async def test_none_forecast( async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> None: """Test custom unit.""" - native_value = 5 - native_unit = SPEED_METERS_PER_SECOND - custom_unit = SPEED_MILES_PER_HOUR + wind_speed_value = 5 + wind_speed_unit = SPEED_METERS_PER_SECOND + precipitation_value = 2 + precipitation_unit = LENGTH_MILLIMETERS + pressure_value = 110 + pressure_unit = PRESSURE_HPA + temperature_value = 20 + temperature_unit = TEMP_CELSIUS + visibility_value = 11 + visibility_unit = LENGTH_KILOMETERS + + set_options = { + "wind_speed_unit_of_measurement": SPEED_MILES_PER_HOUR, + "precipitation_unit_of_measurement": LENGTH_INCHES, + "pressure_unit_of_measurement": PRESSURE_INHG, + "temperature_unit_of_measurement": TEMP_FAHRENHEIT, + "visibility_unit_of_measurement": LENGTH_MILES, + } entity_registry = er.async_get(hass) entry = entity_registry.async_get_or_create("weather", "test", "very_unique") - entity_registry.async_update_entity_options( - entry.entity_id, "weather", {"wind_speed_unit_of_measurement": custom_unit} - ) + entity_registry.async_update_entity_options(entry.entity_id, "weather", set_options) await hass.async_block_till_done() platform: WeatherPlatform = getattr(hass.components, "test.weather") @@ -218,10 +212,16 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> platform.MockWeather( name="Test", condition=ATTR_CONDITION_SUNNY, - native_temperature=None, - native_temperature_unit=None, - native_wind_speed=native_value, - native_wind_speed_unit=native_unit, + native_temperature=temperature_value, + native_temperature_unit=temperature_unit, + native_wind_speed=wind_speed_value, + native_wind_speed_unit=wind_speed_unit, + native_pressure=pressure_value, + native_pressure_unit=pressure_unit, + native_precipitation=precipitation_value, + native_precipitation_unit=precipitation_unit, + native_visibility=visibility_value, + native_visibility_unit=visibility_unit, unique_id="very_unique", ) ) @@ -233,7 +233,36 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) - expected = convert_speed(native_value, native_unit, SPEED_MILES_PER_HOUR) - assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx( - expected, rel=1e-2 + expected_wind_speed = round( + convert_speed(wind_speed_value, wind_speed_unit, SPEED_MILES_PER_HOUR), + ROUNDING_PRECISION, + ) + expected_temperature = convert_temperature( + temperature_value, temperature_unit, TEMP_FAHRENHEIT + ) + expected_pressure = round( + convert_pressure(pressure_value, pressure_unit, PRESSURE_INHG), + ROUNDING_PRECISION, + ) + expected_precipitation = round( + convert_distance(precipitation_value, precipitation_unit, LENGTH_INCHES), + ROUNDING_PRECISION, + ) + expected_visibility = round( + convert_distance(visibility_value, visibility_unit, LENGTH_MILES), + ROUNDING_PRECISION, + ) + + assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx( + expected_wind_speed + ) + assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( + expected_temperature, rel=0.1 + ) + assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == approx(expected_pressure) + assert float(state.attributes[ATTR_WEATHER_PRECIPITATION]) == approx( + expected_precipitation + ) + assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx( + expected_visibility ) From f0c06d599a0bbf6ee0ba8e2aca93748deaaadf4d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 16 Jun 2022 20:36:48 +0000 Subject: [PATCH 07/27] From review discussions --- homeassistant/components/weather/__init__.py | 213 +++++++++++------- tests/components/weather/test_init.py | 15 -- .../custom_components/test/weather.py | 7 +- 3 files changed, 135 insertions(+), 100 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index f9c6a283ae6..9a6980eed47 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -5,6 +5,7 @@ from collections.abc import Callable from contextlib import suppress from dataclasses import dataclass from datetime import timedelta +import inspect import logging from typing import Any, Final, TypedDict, final @@ -82,10 +83,8 @@ ATTR_WEATHER_VISIBILITY_UNIT = "visibility_unit" ATTR_WEATHER_WIND_BEARING = "wind_bearing" ATTR_WEATHER_WIND_SPEED = "wind_speed" ATTR_WEATHER_WIND_SPEED_UNIT = "wind_speed_unit" -ATTR_WEATHER_PRECIPITATION = "precipitation" ATTR_WEATHER_PRECIPITATION_UNIT = "precipitation_unit" - CONF_PRECIPITATION_UOM = "precipitation_unit_of_measurement" CONF_PRESSURE_UOM = "pressure_unit_of_measurement" CONF_TEMPERATURE_UOM = "temperature_unit_of_measurement" @@ -190,22 +189,23 @@ class WeatherEntity(Entity): _attr_humidity: float | None = None _attr_ozone: float | None = None _attr_precision: float - _attr_native_pressure: float | None = None _attr_pressure_unit: str | None = None - _attr_native_pressure_unit: str | None = None _attr_state: None = None _attr_temperature_unit: str | None = None - _attr_native_temperature_unit: str - _attr_native_temperature: float - _attr_native_visibility: float | None = None _attr_visibility_unit: str | None = None + _attr_precipitation_unit: str | None = None + _attr_wind_bearing: float | str | None = None + _attr_wind_speed_unit: str | None = None + + _attr_native_pressure: float | None = None + _attr_native_pressure_unit: str | None = None + _attr_native_temperature: float + _attr_native_temperature_unit: str + _attr_native_visibility: float | None = None _attr_native_visibility_unit: str | None = None _attr_native_precipitation: float | None = None - _attr_precipitation_unit: str | None = None _attr_native_precipitation_unit: str | None = None - _attr_wind_bearing: float | str | None = None _attr_native_wind_speed: float | None = None - _attr_wind_speed_unit: str | None = None _attr_native_wind_speed_unit: str | None = None _weather_option_temperature_uom: str | None = None @@ -214,14 +214,69 @@ class WeatherEntity(Entity): _weather_option_precipitation_uom: str | None = None _weather_option_wind_speed_uom: str | None = None - _deprecated_weather_entity_report = False - _deprecated_weather_entity_reported = False + _override_temperature: bool = False + _override_pressure: bool = False + _override_visibility: bool = False + _override_precipitation: bool = False + _override_wind_speed: bool = False def __init_subclass__(cls, **kwargs: Any) -> None: """Post initialisation processing.""" super().__init_subclass__(**kwargs) - if "temperature_unit" in cls.__dict__: - cls._deprecated_weather_entity_report = True + _reported = False + for method in ( + "temperature", + "temperature_unit", + "_attr_temperature", + "_attr_temperature_unit", + "pressure", + "pressure_unit", + "_attr_pressure", + "_attr_pressure_unit", + "wind_speed", + "wind_speed_unit", + "_attr_wind_speed", + "_attr_wind_speed_unit", + "visibility", + "visibility_unit", + "_attr_visibility", + "_attr_visibility_unit", + "precipitation_unit", + "_attr_precipitation_unit", + ): + if method in cls.__dict__: + if "temperature" in method: + WeatherEntity._override_temperature = True + if "pressure" in method: + WeatherEntity._override_pressure = True + if "wind_speed" in method: + WeatherEntity._override_wind_speed = True + if "visibility" in method: + WeatherEntity._override_visibility = True + if "precipitation" in method: + WeatherEntity._override_precipitation = True + module = inspect.getmodule(cls) + if _reported is False: + _reported = True + if ( + module + and module.__file__ + and "custom_components" in module.__file__ + ): + report_issue = "report it to the custom component author." + else: + report_issue = ( + "create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" + ) + _LOGGER.warning( + "%s::%s is overriding deprecated methods on an instance of " + "WeatherEntity, this is not valid and will be unsupported " + "from Home Assistant 2022.10. Please %s", + cls.__module__, + cls.__name__, + report_issue, + ) async def async_internal_added_to_hass(self) -> None: """Call when the sensor entity is added to hass.""" @@ -233,11 +288,21 @@ class WeatherEntity(Entity): @property def native_temperature(self) -> float: """Return the platform temperature in native units (i.e. not converted).""" + if self._override_temperature: + if hasattr(self, "_attr_temperature"): + return self._attr_temperature # type: ignore[no-any-return, attr-defined] + if hasattr(self, "temperature"): + return self.temperature # type: ignore[no-any-return, attr-defined] return self._attr_native_temperature @property def native_temperature_unit(self) -> str | None: """Return the native unit of measurement for temperature.""" + if self._override_temperature: + if hasattr(self, "_attr_temperature_unit"): + return self._attr_temperature_unit + if hasattr(self, "temperature"): + return self.temperature_unit if hasattr(self, "_attr_native_temperature_unit"): return self._attr_native_temperature_unit return None @@ -245,28 +310,29 @@ class WeatherEntity(Entity): @property def temperature_unit(self) -> str: """Return the unit of measurement for temperature.""" - if ( - self._deprecated_weather_entity_report is True - and self._deprecated_weather_entity_reported is False - ): - self._report_deprecated_weather_entity() + if weather_option_temperature_uom := self._weather_option_temperature_uom: + return weather_option_temperature_uom - if self.native_temperature_unit is None: - return None # type: ignore[return-value] - - if self._weather_option_temperature_uom: - return self._weather_option_temperature_uom - - return self.native_temperature_unit + return self.native_temperature_unit # type: ignore[return-value] @property def native_pressure(self) -> float | None: """Return the pressure in native units.""" + if self._override_pressure: + if hasattr(self, "_attr_pressure"): + return self._attr_pressure # type: ignore[no-any-return, attr-defined] + if hasattr(self, "pressure"): + return self.pressure # type: ignore[no-any-return, attr-defined] return self._attr_native_pressure @property def native_pressure_unit(self) -> str | None: """Return the native unit of measurement for pressure.""" + if self._override_pressure: + if hasattr(self, "_attr_pressure_unit"): + return self._attr_pressure_unit + if hasattr(self, "pressure_unit"): + return self.pressure_unit if hasattr(self, "_attr_native_pressure_unit"): return self._attr_native_pressure_unit return None @@ -274,13 +340,13 @@ class WeatherEntity(Entity): @property def pressure_unit(self) -> str | None: """Return the unit of measurement for pressure.""" - if self.native_pressure_unit is None: + if (native_pressure_unit := self.native_pressure_unit) is None: return None - if self._weather_option_pressure_uom: - return self._weather_option_pressure_uom + if weather_option_pressure_uom := self._weather_option_pressure_uom: + return weather_option_pressure_uom - return self.native_pressure_unit + return native_pressure_unit @property def humidity(self) -> float | None: @@ -290,11 +356,21 @@ class WeatherEntity(Entity): @property def native_wind_speed(self) -> float | None: """Return the wind speed in native units.""" + if self._override_wind_speed: + if hasattr(self, "_attr_wind_speed"): + return self._attr_wind_speed # type: ignore[no-any-return, attr-defined] + if hasattr(self, "wind_speed"): + return self.wind_speed # type: ignore[no-any-return, attr-defined] return self._attr_native_wind_speed @property def native_wind_speed_unit(self) -> str | None: """Return the native unit of measurement for wind speed.""" + if self._override_wind_speed: + if hasattr(self, "_attr_wind_speed_unit"): + return self._attr_wind_speed_unit + if hasattr(self, "wind_speed_unit"): + return self.wind_speed_unit if hasattr(self, "_attr_native_wind_speed_unit"): return self._attr_native_wind_speed_unit return None @@ -302,13 +378,13 @@ class WeatherEntity(Entity): @property def wind_speed_unit(self) -> str | None: """Return the unit of measurement for wind speed.""" - if self.native_wind_speed_unit is None: + if (native_wind_speed_unit := self.native_wind_speed_unit) is None: return None - if self._weather_option_wind_speed_uom: - return self._weather_option_wind_speed_uom + if weather_option_wind_speed_uom := self._weather_option_wind_speed_uom: + return weather_option_wind_speed_uom - return self.native_wind_speed_unit + return native_wind_speed_unit @property def wind_bearing(self) -> float | str | None: @@ -323,11 +399,21 @@ class WeatherEntity(Entity): @property def native_visibility(self) -> float | None: """Return the visibility in native units.""" + if self._override_visibility: + if hasattr(self, "_attr_visibility"): + return self._attr_visibility # type: ignore[no-any-return, attr-defined] + if hasattr(self, "visibility"): + return self.visibility # type: ignore[no-any-return, attr-defined] return self._attr_native_visibility @property def native_visibility_unit(self) -> str | None: """Return the native unit of measurement for visibility.""" + if self._override_visibility: + if hasattr(self, "_attr_visibility_unit"): + return self._attr_visibility_unit + if hasattr(self, "visibility_unit"): + return self.visibility_unit if hasattr(self, "_attr_native_visibility_unit"): return self._attr_native_visibility_unit return None @@ -335,27 +421,27 @@ class WeatherEntity(Entity): @property def visibility_unit(self) -> str | None: """Return the unit of measurement for visibility.""" - if self.native_visibility_unit is None: + if (native_visibility_unit := self.native_visibility_unit) is None: return None - if self._weather_option_visibility_uom: - return self._weather_option_visibility_uom + if weather_option_visibility_uom := self._weather_option_visibility_uom: + return weather_option_visibility_uom - return self.native_visibility_unit + return native_visibility_unit @property def forecast(self) -> list[Forecast] | None: """Return the forecast in native units.""" return self._attr_forecast - @property - def native_precipitation(self) -> float | None: - """Return the precipitation in native units.""" - return self._attr_native_precipitation - @property def native_precipitation_unit(self) -> str | None: """Return the native unit of measurement for accumulated precipitation.""" + if self._override_precipitation: + if hasattr(self, "_attr_precipitation_unit"): + return self._attr_precipitation_unit + if hasattr(self, "precipitation_unit"): + return self.precipitation_unit if hasattr(self, "_attr_native_precipitation_unit"): return self._attr_native_precipitation_unit return None @@ -363,13 +449,13 @@ class WeatherEntity(Entity): @property def precipitation_unit(self) -> str | None: """Return the unit of measurement for precipitation.""" - if self.native_precipitation_unit is None: + if (native_precipitation_unit := self.native_precipitation_unit) is None: return None - if self._weather_option_precipitation_uom: - return self._weather_option_precipitation_uom + if weather_option_precipitation_uom := self._weather_option_precipitation_uom: + return weather_option_precipitation_uom - return self.native_precipitation_unit + return native_precipitation_unit @property def precision(self) -> float: @@ -464,31 +550,13 @@ class WeatherEntity(Entity): ) data[ATTR_WEATHER_VISIBILITY_UNIT] = visibility_unit - if ( - (precipitation := self.native_precipitation) is not None - and (precipitation_unit := self.precipitation_unit) is not None - and (native_precipitation_unit := self.native_precipitation_unit) - is not None - ): - with suppress(ValueError): - precipitation_f = float(precipitation) - value_precipitation = UNIT_CONVERSIONS[CONF_PRECIPITATION_UOM]( - precipitation_f, - native_precipitation_unit, - precipitation_unit, - ) - data[ATTR_WEATHER_PRECIPITATION] = round( - value_precipitation, ROUNDING_PRECISION - ) - data[ATTR_WEATHER_PRECIPITATION_UNIT] = precipitation_unit - if self.forecast is not None: forecast = [] for forecast_entry in self.forecast: forecast_entry_new = {} forecast_entry = dict(forecast_entry) temperature = forecast_entry[ATTR_FORECAST_TEMP] - print(f"{self.temperature_unit} and {self.native_temperature_unit}") + if ( self.temperature_unit is not None and self.native_temperature_unit is not None @@ -638,16 +706,3 @@ class WeatherEntity(Entity): and self.native_visibility_unit in VALID_UNITS[CONF_VISIBILITY_UOM] ): self._weather_option_visibility_uom = custom_unit_visibility - - def _report_deprecated_weather_entity(self) -> None: - """Report that the weather entity has not been upgraded.""" - if not self._deprecated_weather_entity_reported: - self._deprecated_weather_entity_reported = True - report_issue = self._suggest_report_issue() - _LOGGER.warning( - "Entity %s (%s) is using deprecated WeatherEntity features which will " - "be unsupported from Home Assistant Core 2022.10, please %s", - self.entity_id, - type(self), - report_issue, - ) diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 08a3a60ee69..bcd0ab34037 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -10,7 +10,6 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_WIND_SPEED, - ATTR_WEATHER_PRECIPITATION, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_VISIBILITY, @@ -150,9 +149,6 @@ async def test_precipitation( forecast = state.attributes[ATTR_FORECAST][0] expected = native_value - assert float(state.attributes[ATTR_WEATHER_PRECIPITATION]) == approx( - expected, rel=1e-2 - ) assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx(expected, rel=1e-2) @@ -183,8 +179,6 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> """Test custom unit.""" wind_speed_value = 5 wind_speed_unit = SPEED_METERS_PER_SECOND - precipitation_value = 2 - precipitation_unit = LENGTH_MILLIMETERS pressure_value = 110 pressure_unit = PRESSURE_HPA temperature_value = 20 @@ -218,8 +212,6 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> native_wind_speed_unit=wind_speed_unit, native_pressure=pressure_value, native_pressure_unit=pressure_unit, - native_precipitation=precipitation_value, - native_precipitation_unit=precipitation_unit, native_visibility=visibility_value, native_visibility_unit=visibility_unit, unique_id="very_unique", @@ -244,10 +236,6 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> convert_pressure(pressure_value, pressure_unit, PRESSURE_INHG), ROUNDING_PRECISION, ) - expected_precipitation = round( - convert_distance(precipitation_value, precipitation_unit, LENGTH_INCHES), - ROUNDING_PRECISION, - ) expected_visibility = round( convert_distance(visibility_value, visibility_unit, LENGTH_MILES), ROUNDING_PRECISION, @@ -260,9 +248,6 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> expected_temperature, rel=0.1 ) assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == approx(expected_pressure) - assert float(state.attributes[ATTR_WEATHER_PRECIPITATION]) == approx( - expected_precipitation - ) assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx( expected_visibility ) diff --git a/tests/testing_config/custom_components/test/weather.py b/tests/testing_config/custom_components/test/weather.py index f57d270980d..fd40472a405 100644 --- a/tests/testing_config/custom_components/test/weather.py +++ b/tests/testing_config/custom_components/test/weather.py @@ -97,11 +97,6 @@ class MockWeather(MockEntity, WeatherEntity): """Return the forecast.""" return self._handle("forecast") - @property - def native_precipitation(self) -> float | None: - """Return the accumulated precipitation.""" - return self._handle("native_precipitation") - @property def native_precipitation_unit(self) -> str | None: """Return the native unit of measurement for accumulated precipitation.""" @@ -126,6 +121,6 @@ class MockWeatherMockForecast(MockWeather): ATTR_FORECAST_PRESSURE: self.native_pressure, ATTR_FORECAST_WIND_SPEED: self.native_wind_speed, ATTR_FORECAST_WIND_BEARING: self.wind_bearing, - ATTR_FORECAST_PRECIPITATION: self.native_precipitation, + ATTR_FORECAST_PRECIPITATION: self._values.get("native_precipitation"), } ] From deabca62f5f5772111b3308cb78a3f68ccb744d5 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 17 Jun 2022 11:56:57 +0000 Subject: [PATCH 08/27] On its way --- homeassistant/components/weather/__init__.py | 195 +++++++++++------- tests/components/weather/test_init.py | 65 ++++++ .../custom_components/test/weather.py | 92 +++++++++ 3 files changed, 281 insertions(+), 71 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 9a6980eed47..ef05cf8354c 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -189,12 +189,16 @@ class WeatherEntity(Entity): _attr_humidity: float | None = None _attr_ozone: float | None = None _attr_precision: float + _attr_pressure: float | None = None # Provide backwards compatibility _attr_pressure_unit: str | None = None _attr_state: None = None + _attr_temperature: float | None = None # Provide backwards compatibility _attr_temperature_unit: str | None = None + _attr_visibility: float | None = None # Provide backwards compatibility _attr_visibility_unit: str | None = None _attr_precipitation_unit: str | None = None _attr_wind_bearing: float | str | None = None + _attr_wind_speed: float | None = None # Provide backwards compatibility _attr_wind_speed_unit: str | None = None _attr_native_pressure: float | None = None @@ -214,49 +218,66 @@ class WeatherEntity(Entity): _weather_option_precipitation_uom: str | None = None _weather_option_wind_speed_uom: str | None = None - _override_temperature: bool = False - _override_pressure: bool = False - _override_visibility: bool = False - _override_precipitation: bool = False - _override_wind_speed: bool = False + _override_temperature: float | None = None + _override_pressure: float | None = None + _override_visibility: float | None = None + _override_precipitation: float | None = None + _override_wind_speed: float | None = None def __init_subclass__(cls, **kwargs: Any) -> None: """Post initialisation processing.""" super().__init_subclass__(**kwargs) _reported = False for method in ( - "temperature", - "temperature_unit", "_attr_temperature", + "temperature", "_attr_temperature_unit", - "pressure", - "pressure_unit", + "temperature_unit", "_attr_pressure", + "pressure", "_attr_pressure_unit", - "wind_speed", - "wind_speed_unit", + "pressure_unit", "_attr_wind_speed", + "wind_speed", "_attr_wind_speed_unit", - "visibility", - "visibility_unit", + "wind_speed_unit", "_attr_visibility", + "visibility", "_attr_visibility_unit", - "precipitation_unit", + "visibility_unit", "_attr_precipitation_unit", + "precipitation_unit", ): if method in cls.__dict__: - if "temperature" in method: - WeatherEntity._override_temperature = True - if "pressure" in method: - WeatherEntity._override_pressure = True - if "wind_speed" in method: - WeatherEntity._override_wind_speed = True - if "visibility" in method: - WeatherEntity._override_visibility = True - if "precipitation" in method: - WeatherEntity._override_precipitation = True - module = inspect.getmodule(cls) + if method in ("temperature", "_attr_temperature"): + value = getattr(cls, method) + setattr(cls, "_override_temperature", value) + if method in ("temperature_unit", "_attr_temperature_unit"): + value = getattr(cls, method) + setattr(cls, "_attr_native_temperature_unit", value) + if method in ("pressure", "_attr_pressure"): + value = getattr(cls, method) + setattr(cls, "_override_pressure", value) + if method in ("pressure_unit", "_attr_pressure_unit"): + value = getattr(cls, method) + setattr(cls, "_attr_native_pressure_unit", value) + if method in ("wind_speed", "_attr_wind_speed"): + value = getattr(cls, method) + setattr(cls, "_override_wind_speed", value) + if method in ("wind_speed_unit", "_attr_wind_speed_unit"): + value = getattr(cls, method) + setattr(cls, "_attr_native_wind_speed_unit", value) + if method in ("visibility", "_attr_visibility"): + value = getattr(cls, method) + setattr(cls, "_override_visibility", value) + if method in ("visibility_unit", "_attr_visibility_unit"): + value = getattr(cls, method) + setattr(cls, "_attr_native_visibility_unit", value) + if method in ("precipitation_unit", "_attr_precipitation_unit"): + value = getattr(cls, method) + setattr(cls, "_attr_native_precipitation_unit", value) if _reported is False: + module = inspect.getmodule(cls) _reported = True if ( module @@ -285,24 +306,24 @@ class WeatherEntity(Entity): return self.async_registry_entry_updated() + @property + def temperature(self) -> float: + """Return the temperature for backward compatibility.""" + if self._override_temperature is not None: + return self._override_temperature + self._override_temperature = self._attr_temperature + return self._attr_temperature # type: ignore[return-value] + @property def native_temperature(self) -> float: """Return the platform temperature in native units (i.e. not converted).""" - if self._override_temperature: - if hasattr(self, "_attr_temperature"): - return self._attr_temperature # type: ignore[no-any-return, attr-defined] - if hasattr(self, "temperature"): - return self.temperature # type: ignore[no-any-return, attr-defined] - return self._attr_native_temperature + if hasattr(self, "_attr_native_temperature"): + return self._attr_native_temperature + return self.temperature @property def native_temperature_unit(self) -> str | None: """Return the native unit of measurement for temperature.""" - if self._override_temperature: - if hasattr(self, "_attr_temperature_unit"): - return self._attr_temperature_unit - if hasattr(self, "temperature"): - return self.temperature_unit if hasattr(self, "_attr_native_temperature_unit"): return self._attr_native_temperature_unit return None @@ -315,24 +336,26 @@ class WeatherEntity(Entity): return self.native_temperature_unit # type: ignore[return-value] + @property + def pressure(self) -> float | None: + """Return the pressure for backward compatibility.""" + if self._override_pressure is not None: + return self._override_pressure + if hasattr(self, "_attr_pressure"): + self._override_pressure = self._attr_pressure + return self._attr_pressure + return None + @property def native_pressure(self) -> float | None: """Return the pressure in native units.""" - if self._override_pressure: - if hasattr(self, "_attr_pressure"): - return self._attr_pressure # type: ignore[no-any-return, attr-defined] - if hasattr(self, "pressure"): - return self.pressure # type: ignore[no-any-return, attr-defined] + if (pressure := self.pressure) is not None: + return pressure return self._attr_native_pressure @property def native_pressure_unit(self) -> str | None: """Return the native unit of measurement for pressure.""" - if self._override_pressure: - if hasattr(self, "_attr_pressure_unit"): - return self._attr_pressure_unit - if hasattr(self, "pressure_unit"): - return self.pressure_unit if hasattr(self, "_attr_native_pressure_unit"): return self._attr_native_pressure_unit return None @@ -353,24 +376,26 @@ class WeatherEntity(Entity): """Return the humidity in native units.""" return self._attr_humidity + @property + def wind_speed(self) -> float | None: + """Return the wind_speed for backward compatibility.""" + if self._override_wind_speed is not None: + return self._override_wind_speed + if hasattr(self, "_attr_wind_speed"): + self._override_wind_speed = self._attr_wind_speed + return self._attr_wind_speed + return None + @property def native_wind_speed(self) -> float | None: """Return the wind speed in native units.""" - if self._override_wind_speed: - if hasattr(self, "_attr_wind_speed"): - return self._attr_wind_speed # type: ignore[no-any-return, attr-defined] - if hasattr(self, "wind_speed"): - return self.wind_speed # type: ignore[no-any-return, attr-defined] + if (wind_speed := self.wind_speed) is not None: + return wind_speed return self._attr_native_wind_speed @property def native_wind_speed_unit(self) -> str | None: """Return the native unit of measurement for wind speed.""" - if self._override_wind_speed: - if hasattr(self, "_attr_wind_speed_unit"): - return self._attr_wind_speed_unit - if hasattr(self, "wind_speed_unit"): - return self.wind_speed_unit if hasattr(self, "_attr_native_wind_speed_unit"): return self._attr_native_wind_speed_unit return None @@ -396,24 +421,26 @@ class WeatherEntity(Entity): """Return the ozone level.""" return self._attr_ozone + @property + def visibility(self) -> float | None: + """Return the visibility for backward compatibility.""" + if self._override_visibility is not None: + return self._override_visibility + if hasattr(self, "_attr_visibility"): + self._override_visibility = self._attr_visibility + return self._attr_visibility + return None + @property def native_visibility(self) -> float | None: """Return the visibility in native units.""" - if self._override_visibility: - if hasattr(self, "_attr_visibility"): - return self._attr_visibility # type: ignore[no-any-return, attr-defined] - if hasattr(self, "visibility"): - return self.visibility # type: ignore[no-any-return, attr-defined] + if (visibility := self.visibility) is not None: + return visibility return self._attr_native_visibility @property def native_visibility_unit(self) -> str | None: """Return the native unit of measurement for visibility.""" - if self._override_visibility: - if hasattr(self, "_attr_visibility_unit"): - return self._attr_visibility_unit - if hasattr(self, "visibility_unit"): - return self.visibility_unit if hasattr(self, "_attr_native_visibility_unit"): return self._attr_native_visibility_unit return None @@ -437,11 +464,6 @@ class WeatherEntity(Entity): @property def native_precipitation_unit(self) -> str | None: """Return the native unit of measurement for accumulated precipitation.""" - if self._override_precipitation: - if hasattr(self, "_attr_precipitation_unit"): - return self._attr_precipitation_unit - if hasattr(self, "precipitation_unit"): - return self.precipitation_unit if hasattr(self, "_attr_native_precipitation_unit"): return self._attr_native_precipitation_unit return None @@ -497,6 +519,16 @@ class WeatherEntity(Entity): else round(value_temp, precision) ) data[ATTR_WEATHER_TEMPERATURE_UNIT] = temp_unit + elif (temperature := self.native_temperature) is not None and ( + self._override_temperature + ): + with suppress(ValueError): + temperature_f = float(temperature) + data[ATTR_WEATHER_TEMPERATURE] = ( + round(temperature_f) + if precision == 0 + else round(temperature_f, precision) + ) if (humidity := self.humidity) is not None: data[ATTR_WEATHER_HUMIDITY] = round(humidity) @@ -516,6 +548,12 @@ class WeatherEntity(Entity): ) data[ATTR_WEATHER_PRESSURE] = round(value_pressure, ROUNDING_PRECISION) data[ATTR_WEATHER_PRESSURE_UNIT] = pressure_unit + elif (pressure := self.native_pressure) is not None and ( + self._override_pressure + ): + with suppress(ValueError): + pressure_f = float(pressure) + data[ATTR_WEATHER_PRESSURE] = round(pressure_f, ROUNDING_PRECISION) if (wind_bearing := self.wind_bearing) is not None: data[ATTR_WEATHER_WIND_BEARING] = wind_bearing @@ -534,6 +572,12 @@ class WeatherEntity(Entity): value_wind_speed, ROUNDING_PRECISION ) data[ATTR_WEATHER_WIND_SPEED_UNIT] = wind_speed_unit + elif (wind_speed := self.native_wind_speed) is not None and ( + self._override_wind_speed + ): + with suppress(ValueError): + wind_speed_f = float(wind_speed) + data[ATTR_WEATHER_WIND_SPEED] = round(wind_speed_f, ROUNDING_PRECISION) if ( (visibility := self.native_visibility) is not None @@ -549,6 +593,15 @@ class WeatherEntity(Entity): value_visibility, ROUNDING_PRECISION ) data[ATTR_WEATHER_VISIBILITY_UNIT] = visibility_unit + elif (visibility := self.native_visibility) is not None and ( + self._override_visibility + ): + with suppress(ValueError): + visibility_f = float(visibility) + data[ATTR_WEATHER_VISIBILITY] = round(visibility_f, ROUNDING_PRECISION) + + if (precipitation_unit := self.precipitation_unit) is not None: + data[ATTR_WEATHER_PRECIPITATION_UNIT] = precipitation_unit if self.forecast is not None: forecast = [] diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index bcd0ab34037..f394d96d73c 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -10,10 +10,15 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_WIND_SPEED, + ATTR_WEATHER_PRECIPITATION_UNIT, ATTR_WEATHER_PRESSURE, + ATTR_WEATHER_PRESSURE_UNIT, ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_TEMPERATURE_UNIT, ATTR_WEATHER_VISIBILITY, + ATTR_WEATHER_VISIBILITY_UNIT, ATTR_WEATHER_WIND_SPEED, + ATTR_WEATHER_WIND_SPEED_UNIT, ROUNDING_PRECISION, ) from homeassistant.const import ( @@ -251,3 +256,63 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx( expected_visibility ) + + +async def test_backwards_compability( + hass: HomeAssistant, enable_custom_integrations +) -> None: + """Test custom unit.""" + wind_speed_value = 5 + wind_speed_unit = SPEED_METERS_PER_SECOND + pressure_value = 110 + pressure_unit = PRESSURE_HPA + temperature_value = 20 + temperature_unit = TEMP_CELSIUS + visibility_value = 11 + visibility_unit = LENGTH_KILOMETERS + precipitation_value = 1 + precipitation_unit = LENGTH_MILLIMETERS + + platform: WeatherPlatform = getattr(hass.components, "test.weather") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockWeatherMockForecastCompat( + name="Test", + condition=ATTR_CONDITION_SUNNY, + temperature=temperature_value, + temperature_unit=temperature_unit, + wind_speed=wind_speed_value, + wind_speed_unit=wind_speed_unit, + pressure=pressure_value, + pressure_unit=pressure_unit, + visibility=visibility_value, + visibility_unit=visibility_unit, + precipitation=precipitation_value, + precipitation_unit=precipitation_unit, + unique_id="very_unique", + ) + ) + + entity0 = platform.ENTITIES[0] + assert await async_setup_component( + hass, "weather", {"weather": {"platform": "test"}} + ) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + forecast = state.attributes[ATTR_FORECAST][0] + + assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx(wind_speed_value) + assert state.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] == SPEED_METERS_PER_SECOND + assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( + temperature_value, rel=0.1 + ) + assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == TEMP_CELSIUS + assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == approx(pressure_value) + assert state.attributes[ATTR_WEATHER_PRESSURE_UNIT] == PRESSURE_HPA + assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx(visibility_value) + assert state.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == LENGTH_KILOMETERS + assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx( + precipitation_value, rel=1e-2 + ) + assert state.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == LENGTH_MILLIMETERS diff --git a/tests/testing_config/custom_components/test/weather.py b/tests/testing_config/custom_components/test/weather.py index fd40472a405..1336620860f 100644 --- a/tests/testing_config/custom_components/test/weather.py +++ b/tests/testing_config/custom_components/test/weather.py @@ -108,6 +108,80 @@ class MockWeather(MockEntity, WeatherEntity): return self._handle("condition") +class MockWeatherCompat(MockEntity, WeatherEntity): + """Mock weather class for backwards compatibility check.""" + + @property + def temperature(self) -> float | None: + """Return the platform temperature.""" + return self._handle("temperature") + + @property + def temperature_unit(self) -> str | None: + """Return the unit of measurement for temperature.""" + return self._handle("temperature_unit") + + @property + def pressure(self) -> float | None: + """Return the pressure.""" + return self._handle("pressure") + + @property + def pressure_unit(self) -> str | None: + """Return the unit of measurement for pressure.""" + return self._handle("pressure_unit") + + @property + def humidity(self) -> float | None: + """Return the humidity.""" + return self._handle("humidity") + + @property + def wind_speed(self) -> float | None: + """Return the wind speed.""" + return self._handle("wind_speed") + + @property + def wind_speed_unit(self) -> str | None: + """Return the unit of measurement for wind speed.""" + return self._handle("wind_speed_unit") + + @property + def wind_bearing(self) -> float | str | None: + """Return the wind bearing.""" + return self._handle("wind_bearing") + + @property + def ozone(self) -> float | None: + """Return the ozone level.""" + return self._handle("ozone") + + @property + def visibility(self) -> float | None: + """Return the visibility.""" + return self._handle("visibility") + + @property + def visibility_unit(self) -> str | None: + """Return the unit of measurement for visibility.""" + return self._handle("visibility_unit") + + @property + def forecast(self) -> list[Forecast] | None: + """Return the forecast.""" + return self._handle("forecast") + + @property + def precipitation_unit(self) -> str | None: + """Return the unit of measurement for accumulated precipitation.""" + return self._handle("precipitation_unit") + + @property + def condition(self) -> str | None: + """Return the current condition.""" + return self._handle("condition") + + class MockWeatherMockForecast(MockWeather): """Mock weather class with mocked forecast.""" @@ -124,3 +198,21 @@ class MockWeatherMockForecast(MockWeather): ATTR_FORECAST_PRECIPITATION: self._values.get("native_precipitation"), } ] + + +class MockWeatherMockForecastCompat(MockWeatherCompat): + """Mock weather class with mocked forecast for compatibility check.""" + + @property + def forecast(self) -> list[Forecast] | None: + """Return the forecast.""" + return [ + { + ATTR_FORECAST_TEMP: self.native_temperature, + ATTR_FORECAST_TEMP_LOW: self.native_temperature, + ATTR_FORECAST_PRESSURE: self.native_pressure, + ATTR_FORECAST_WIND_SPEED: self.native_wind_speed, + ATTR_FORECAST_WIND_BEARING: self.wind_bearing, + ATTR_FORECAST_PRECIPITATION: self._values.get("precipitation"), + } + ] From 986f0001a5945bb8fbce54adfe5ebd6369048d08 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 17 Jun 2022 12:38:30 +0000 Subject: [PATCH 09/27] Fix mypy and add test --- homeassistant/components/weather/__init__.py | 6 ++-- tests/components/weather/test_init.py | 38 ++++++++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index ef05cf8354c..ce3a59caf4d 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -307,15 +307,15 @@ class WeatherEntity(Entity): self.async_registry_entry_updated() @property - def temperature(self) -> float: + def temperature(self) -> float | None: """Return the temperature for backward compatibility.""" if self._override_temperature is not None: return self._override_temperature self._override_temperature = self._attr_temperature - return self._attr_temperature # type: ignore[return-value] + return self._attr_temperature @property - def native_temperature(self) -> float: + def native_temperature(self) -> float | None: """Return the platform temperature in native units (i.e. not converted).""" if hasattr(self, "_attr_native_temperature"): return self._attr_native_temperature diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index f394d96d73c..078eee7a505 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -292,15 +292,38 @@ async def test_backwards_compability( unique_id="very_unique", ) ) + platform.ENTITIES.append( + platform.MockWeatherMockForecastCompat( + name="Test2", + condition=ATTR_CONDITION_SUNNY, + temperature=temperature_value, + temperature_unit=temperature_unit, + wind_speed=wind_speed_value, + wind_speed_unit=None, + pressure=pressure_value, + pressure_unit=None, + visibility=visibility_value, + visibility_unit=None, + precipitation=precipitation_value, + precipitation_unit=None, + unique_id="very_unique2", + ) + ) entity0 = platform.ENTITIES[0] + entity1 = platform.ENTITIES[1] assert await async_setup_component( hass, "weather", {"weather": {"platform": "test"}} ) + assert await async_setup_component( + hass, "weather", {"weather": {"platform": "test2"}} + ) await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] + state1 = hass.states.get(entity1.entity_id) + forecast1 = state1.attributes[ATTR_FORECAST][0] assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx(wind_speed_value) assert state.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] == SPEED_METERS_PER_SECOND @@ -316,3 +339,18 @@ async def test_backwards_compability( precipitation_value, rel=1e-2 ) assert state.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == LENGTH_MILLIMETERS + + assert float(state1.attributes[ATTR_WEATHER_WIND_SPEED]) == approx(wind_speed_value) + assert float(state1.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( + temperature_value, rel=0.1 + ) + assert state1.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == TEMP_CELSIUS + assert float(state1.attributes[ATTR_WEATHER_PRESSURE]) == approx(pressure_value) + assert float(state1.attributes[ATTR_WEATHER_VISIBILITY]) == approx(visibility_value) + assert float(forecast1[ATTR_FORECAST_PRECIPITATION]) == approx( + precipitation_value, rel=1e-2 + ) + assert ATTR_WEATHER_WIND_SPEED_UNIT not in state1.attributes + assert ATTR_WEATHER_PRESSURE_UNIT not in state1.attributes + assert ATTR_WEATHER_VISIBILITY_UNIT not in state1.attributes + assert ATTR_WEATHER_PRECIPITATION_UNIT not in state1.attributes From 9c3d760a14d1ae7c40d093af9fb867649d55dc31 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 17 Jun 2022 13:46:27 +0000 Subject: [PATCH 10/27] cleanup --- homeassistant/components/weather/__init__.py | 54 +++++++------------- 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index ce3a59caf4d..c3a3d357ab5 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -322,11 +322,9 @@ class WeatherEntity(Entity): return self.temperature @property - def native_temperature_unit(self) -> str | None: + def native_temperature_unit(self) -> str: """Return the native unit of measurement for temperature.""" - if hasattr(self, "_attr_native_temperature_unit"): - return self._attr_native_temperature_unit - return None + return self._attr_native_temperature_unit @property def temperature_unit(self) -> str: @@ -334,17 +332,15 @@ class WeatherEntity(Entity): if weather_option_temperature_uom := self._weather_option_temperature_uom: return weather_option_temperature_uom - return self.native_temperature_unit # type: ignore[return-value] + return self.native_temperature_unit @property def pressure(self) -> float | None: """Return the pressure for backward compatibility.""" if self._override_pressure is not None: return self._override_pressure - if hasattr(self, "_attr_pressure"): - self._override_pressure = self._attr_pressure - return self._attr_pressure - return None + self._override_pressure = self._attr_pressure + return self._attr_pressure @property def native_pressure(self) -> float | None: @@ -356,9 +352,7 @@ class WeatherEntity(Entity): @property def native_pressure_unit(self) -> str | None: """Return the native unit of measurement for pressure.""" - if hasattr(self, "_attr_native_pressure_unit"): - return self._attr_native_pressure_unit - return None + return self._attr_native_pressure_unit @property def pressure_unit(self) -> str | None: @@ -381,10 +375,8 @@ class WeatherEntity(Entity): """Return the wind_speed for backward compatibility.""" if self._override_wind_speed is not None: return self._override_wind_speed - if hasattr(self, "_attr_wind_speed"): - self._override_wind_speed = self._attr_wind_speed - return self._attr_wind_speed - return None + self._override_wind_speed = self._attr_wind_speed + return self._attr_wind_speed @property def native_wind_speed(self) -> float | None: @@ -396,9 +388,7 @@ class WeatherEntity(Entity): @property def native_wind_speed_unit(self) -> str | None: """Return the native unit of measurement for wind speed.""" - if hasattr(self, "_attr_native_wind_speed_unit"): - return self._attr_native_wind_speed_unit - return None + return self._attr_native_wind_speed_unit @property def wind_speed_unit(self) -> str | None: @@ -426,10 +416,8 @@ class WeatherEntity(Entity): """Return the visibility for backward compatibility.""" if self._override_visibility is not None: return self._override_visibility - if hasattr(self, "_attr_visibility"): - self._override_visibility = self._attr_visibility - return self._attr_visibility - return None + self._override_visibility = self._attr_visibility + return self._attr_visibility @property def native_visibility(self) -> float | None: @@ -441,9 +429,7 @@ class WeatherEntity(Entity): @property def native_visibility_unit(self) -> str | None: """Return the native unit of measurement for visibility.""" - if hasattr(self, "_attr_native_visibility_unit"): - return self._attr_native_visibility_unit - return None + return self._attr_native_visibility_unit @property def visibility_unit(self) -> str | None: @@ -464,9 +450,7 @@ class WeatherEntity(Entity): @property def native_precipitation_unit(self) -> str | None: """Return the native unit of measurement for accumulated precipitation.""" - if hasattr(self, "_attr_native_precipitation_unit"): - return self._attr_native_precipitation_unit - return None + return self._attr_native_precipitation_unit @property def precipitation_unit(self) -> str | None: @@ -486,7 +470,7 @@ class WeatherEntity(Entity): return self._attr_precision return ( PRECISION_TENTHS - if self.temperature_unit == TEMP_CELSIUS + if self.native_temperature_unit == TEMP_CELSIUS else PRECISION_WHOLE ) @@ -496,12 +480,10 @@ class WeatherEntity(Entity): """Return the state attributes, converted from native units to user-configured units.""" data = {} - precision_str = str(self.precision) - precision = ( - len(precision_str) - precision_str.index(".") - 1 - if "." in precision_str - else 0 - ) + if self.precision == PRECISION_WHOLE: + precision = 0 + else: + precision = 1 if ( (temperature := self.native_temperature) is not None From db441057b5949def035d78e425bc48f6b9d4fa04 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 18 Jun 2022 13:26:35 +0000 Subject: [PATCH 11/27] Trim down --- homeassistant/components/weather/__init__.py | 366 ++++++++++--------- tests/components/weather/test_init.py | 5 +- 2 files changed, 202 insertions(+), 169 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index c3a3d357ab5..eac49495a65 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -15,6 +15,7 @@ from homeassistant.const import ( LENGTH_KILOMETERS, LENGTH_MILES, LENGTH_MILLIMETERS, + PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, PRESSURE_HPA, @@ -34,6 +35,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType from homeassistant.util import ( distance as distance_util, @@ -140,6 +142,23 @@ VALID_UNITS: dict[str, tuple[str, ...]] = { } +def round_temperature(temperature: float | None, precision: float) -> float | None: + """Provide rounding for temperature.""" + if temperature is None: + return None + + # Round in the units appropriate + if precision == PRECISION_HALVES: + temperature = round(temperature * 2) / 2.0 + elif precision == PRECISION_TENTHS: + temperature = round(temperature, 1) + # Integer as a fall back (PRECISION_WHOLE) + else: + temperature = round(temperature) + + return temperature + + class Forecast(TypedDict, total=False): """Typed weather forecast dict.""" @@ -190,21 +209,21 @@ class WeatherEntity(Entity): _attr_ozone: float | None = None _attr_precision: float _attr_pressure: float | None = None # Provide backwards compatibility - _attr_pressure_unit: str | None = None + _attr_pressure_unit: str | None = None # Provide backwards compatibility _attr_state: None = None _attr_temperature: float | None = None # Provide backwards compatibility - _attr_temperature_unit: str | None = None + _attr_temperature_unit: str | None = None # Provide backwards compatibility _attr_visibility: float | None = None # Provide backwards compatibility - _attr_visibility_unit: str | None = None - _attr_precipitation_unit: str | None = None + _attr_visibility_unit: str | None = None # Provide backwards compatibility + _attr_precipitation_unit: str | None = None # Provide backwards compatibility _attr_wind_bearing: float | str | None = None _attr_wind_speed: float | None = None # Provide backwards compatibility - _attr_wind_speed_unit: str | None = None + _attr_wind_speed_unit: str | None = None # Provide backwards compatibility _attr_native_pressure: float | None = None _attr_native_pressure_unit: str | None = None - _attr_native_temperature: float - _attr_native_temperature_unit: str + _attr_native_temperature: float | None = None + _attr_native_temperature_unit: str | None = None _attr_native_visibility: float | None = None _attr_native_visibility_unit: str | None = None _attr_native_precipitation: float | None = None @@ -218,11 +237,7 @@ class WeatherEntity(Entity): _weather_option_precipitation_uom: str | None = None _weather_option_wind_speed_uom: str | None = None - _override_temperature: float | None = None - _override_pressure: float | None = None - _override_visibility: float | None = None - _override_precipitation: float | None = None - _override_wind_speed: float | None = None + _override: bool = False # Override for backward compatibility def __init_subclass__(cls, **kwargs: Any) -> None: """Post initialisation processing.""" @@ -249,33 +264,7 @@ class WeatherEntity(Entity): "precipitation_unit", ): if method in cls.__dict__: - if method in ("temperature", "_attr_temperature"): - value = getattr(cls, method) - setattr(cls, "_override_temperature", value) - if method in ("temperature_unit", "_attr_temperature_unit"): - value = getattr(cls, method) - setattr(cls, "_attr_native_temperature_unit", value) - if method in ("pressure", "_attr_pressure"): - value = getattr(cls, method) - setattr(cls, "_override_pressure", value) - if method in ("pressure_unit", "_attr_pressure_unit"): - value = getattr(cls, method) - setattr(cls, "_attr_native_pressure_unit", value) - if method in ("wind_speed", "_attr_wind_speed"): - value = getattr(cls, method) - setattr(cls, "_override_wind_speed", value) - if method in ("wind_speed_unit", "_attr_wind_speed_unit"): - value = getattr(cls, method) - setattr(cls, "_attr_native_wind_speed_unit", value) - if method in ("visibility", "_attr_visibility"): - value = getattr(cls, method) - setattr(cls, "_override_visibility", value) - if method in ("visibility_unit", "_attr_visibility_unit"): - value = getattr(cls, method) - setattr(cls, "_attr_native_visibility_unit", value) - if method in ("precipitation_unit", "_attr_precipitation_unit"): - value = getattr(cls, method) - setattr(cls, "_attr_native_precipitation_unit", value) + setattr(cls, "_override", True) if _reported is False: module = inspect.getmodule(cls) _reported = True @@ -309,44 +298,40 @@ class WeatherEntity(Entity): @property def temperature(self) -> float | None: """Return the temperature for backward compatibility.""" - if self._override_temperature is not None: - return self._override_temperature - self._override_temperature = self._attr_temperature return self._attr_temperature @property def native_temperature(self) -> float | None: - """Return the platform temperature in native units (i.e. not converted).""" - if hasattr(self, "_attr_native_temperature"): - return self._attr_native_temperature - return self.temperature + """Return the temperature in native units.""" + return self._attr_native_temperature @property - def native_temperature_unit(self) -> str: + def native_temperature_unit(self) -> str | None: """Return the native unit of measurement for temperature.""" return self._attr_native_temperature_unit @property - def temperature_unit(self) -> str: - """Return the unit of measurement for temperature.""" + def temperature_unit(self) -> str | None: + """Return the converted unit of measurement for temperature.""" + if (temperature_unit := self._attr_temperature_unit) is not None: + return temperature_unit + + if (native_temperature_unit := self.native_temperature_unit) is None: + return None + if weather_option_temperature_uom := self._weather_option_temperature_uom: return weather_option_temperature_uom - return self.native_temperature_unit + return native_temperature_unit @property def pressure(self) -> float | None: """Return the pressure for backward compatibility.""" - if self._override_pressure is not None: - return self._override_pressure - self._override_pressure = self._attr_pressure return self._attr_pressure @property def native_pressure(self) -> float | None: """Return the pressure in native units.""" - if (pressure := self.pressure) is not None: - return pressure return self._attr_native_pressure @property @@ -356,7 +341,10 @@ class WeatherEntity(Entity): @property def pressure_unit(self) -> str | None: - """Return the unit of measurement for pressure.""" + """Return the converted unit of measurement for pressure.""" + if (pressure_unit := self._attr_pressure_unit) is not None: + return pressure_unit + if (native_pressure_unit := self.native_pressure_unit) is None: return None @@ -373,16 +361,11 @@ class WeatherEntity(Entity): @property def wind_speed(self) -> float | None: """Return the wind_speed for backward compatibility.""" - if self._override_wind_speed is not None: - return self._override_wind_speed - self._override_wind_speed = self._attr_wind_speed return self._attr_wind_speed @property def native_wind_speed(self) -> float | None: """Return the wind speed in native units.""" - if (wind_speed := self.wind_speed) is not None: - return wind_speed return self._attr_native_wind_speed @property @@ -392,7 +375,10 @@ class WeatherEntity(Entity): @property def wind_speed_unit(self) -> str | None: - """Return the unit of measurement for wind speed.""" + """Return the converted unit of measurement for wind speed.""" + if (wind_speed_unit := self._attr_wind_speed_unit) is not None: + return wind_speed_unit + if (native_wind_speed_unit := self.native_wind_speed_unit) is None: return None @@ -414,16 +400,11 @@ class WeatherEntity(Entity): @property def visibility(self) -> float | None: """Return the visibility for backward compatibility.""" - if self._override_visibility is not None: - return self._override_visibility - self._override_visibility = self._attr_visibility return self._attr_visibility @property def native_visibility(self) -> float | None: """Return the visibility in native units.""" - if (visibility := self.visibility) is not None: - return visibility return self._attr_native_visibility @property @@ -433,7 +414,10 @@ class WeatherEntity(Entity): @property def visibility_unit(self) -> str | None: - """Return the unit of measurement for visibility.""" + """Return the converted unit of measurement for visibility.""" + if (visibility_unit := self._attr_visibility_unit) is not None: + return visibility_unit + if (native_visibility_unit := self.native_visibility_unit) is None: return None @@ -454,12 +438,15 @@ class WeatherEntity(Entity): @property def precipitation_unit(self) -> str | None: - """Return the unit of measurement for precipitation.""" + """Return the converted unit of measurement for precipitation.""" + if (precipitation_unit := self._attr_precipitation_unit) is not None: + return precipitation_unit + if (native_precipitation_unit := self.native_precipitation_unit) is None: return None - if weather_option_precipitation_uom := self._weather_option_precipitation_uom: - return weather_option_precipitation_uom + if _weather_option_precipitation_uom := self._weather_option_precipitation_uom: + return _weather_option_precipitation_uom return native_precipitation_unit @@ -476,16 +463,22 @@ class WeatherEntity(Entity): @final @property - def state_attributes(self): + def state_attributes(self): # noqa: C901 """Return the state attributes, converted from native units to user-configured units.""" data = {} - if self.precision == PRECISION_WHOLE: - precision = 0 - else: - precision = 1 + precision = self.precision - if ( + if (temperature := self.temperature) is not None and ( + temp_unit := self.temperature_unit + ) is not None: + data[ATTR_WEATHER_TEMPERATURE] = show_temp( + self.hass, + temperature, + temp_unit, + precision, + ) + elif ( (temperature := self.native_temperature) is not None and (native_temp_unit := self.native_temperature_unit) is not None and (temp_unit := self.temperature_unit) is not None @@ -495,30 +488,27 @@ class WeatherEntity(Entity): value_temp = UNIT_CONVERSIONS[CONF_TEMPERATURE_UOM]( temperature_f, native_temp_unit, temp_unit ) - data[ATTR_WEATHER_TEMPERATURE] = ( - round(value_temp) - if precision == 0 - else round(value_temp, precision) - ) - data[ATTR_WEATHER_TEMPERATURE_UNIT] = temp_unit - elif (temperature := self.native_temperature) is not None and ( - self._override_temperature - ): - with suppress(ValueError): - temperature_f = float(temperature) - data[ATTR_WEATHER_TEMPERATURE] = ( - round(temperature_f) - if precision == 0 - else round(temperature_f, precision) + data[ATTR_WEATHER_TEMPERATURE] = round_temperature( + value_temp, precision ) + if (temp_unit := self.temperature_unit) is not None: + data[ATTR_WEATHER_TEMPERATURE_UNIT] = temp_unit + if (humidity := self.humidity) is not None: data[ATTR_WEATHER_HUMIDITY] = round(humidity) if (ozone := self.ozone) is not None: data[ATTR_WEATHER_OZONE] = ozone - if ( + if (pressure := self.pressure) is not None: + if (pressure_unit := self.pressure_unit) is not None: + pressure = round( + self.hass.config.units.pressure(pressure, pressure_unit), + ROUNDING_PRECISION, + ) + data[ATTR_WEATHER_PRESSURE] = pressure + elif ( (pressure := self.native_pressure) is not None and (pressure_unit := self.pressure_unit) is not None and (native_pressure_unit := self.native_pressure_unit) is not None @@ -529,18 +519,21 @@ class WeatherEntity(Entity): pressure_f, native_pressure_unit, pressure_unit ) data[ATTR_WEATHER_PRESSURE] = round(value_pressure, ROUNDING_PRECISION) - data[ATTR_WEATHER_PRESSURE_UNIT] = pressure_unit - elif (pressure := self.native_pressure) is not None and ( - self._override_pressure - ): - with suppress(ValueError): - pressure_f = float(pressure) - data[ATTR_WEATHER_PRESSURE] = round(pressure_f, ROUNDING_PRECISION) + + if (pressure_unit := self.pressure_unit) is not None: + data[ATTR_WEATHER_PRESSURE_UNIT] = pressure_unit if (wind_bearing := self.wind_bearing) is not None: data[ATTR_WEATHER_WIND_BEARING] = wind_bearing - if ( + if (wind_speed := self.wind_speed) is not None: + if (wind_speed_unit := self.wind_speed_unit) is not None: + wind_speed = round( + self.hass.config.units.wind_speed(wind_speed, wind_speed_unit), + ROUNDING_PRECISION, + ) + data[ATTR_WEATHER_WIND_SPEED] = wind_speed + elif ( (wind_speed := self.native_wind_speed) is not None and (wind_speed_unit := self.wind_speed_unit) is not None and (native_wind_speed_unit := self.native_wind_speed_unit) is not None @@ -553,15 +546,18 @@ class WeatherEntity(Entity): data[ATTR_WEATHER_WIND_SPEED] = round( value_wind_speed, ROUNDING_PRECISION ) - data[ATTR_WEATHER_WIND_SPEED_UNIT] = wind_speed_unit - elif (wind_speed := self.native_wind_speed) is not None and ( - self._override_wind_speed - ): - with suppress(ValueError): - wind_speed_f = float(wind_speed) - data[ATTR_WEATHER_WIND_SPEED] = round(wind_speed_f, ROUNDING_PRECISION) - if ( + if (wind_speed_unit := self.wind_speed_unit) is not None: + data[ATTR_WEATHER_WIND_SPEED_UNIT] = wind_speed_unit + + if (visibility := self.visibility) is not None: + if (visibility_unit := self.visibility_unit) is not None: + visibility = round( + self.hass.config.units.length(visibility, visibility_unit), + ROUNDING_PRECISION, + ) + data[ATTR_WEATHER_VISIBILITY] = visibility + elif ( (visibility := self.native_visibility) is not None and (visibility_unit := self.visibility_unit) is not None and (native_visibility_unit := self.native_visibility_unit) is not None @@ -574,13 +570,9 @@ class WeatherEntity(Entity): data[ATTR_WEATHER_VISIBILITY] = round( value_visibility, ROUNDING_PRECISION ) - data[ATTR_WEATHER_VISIBILITY_UNIT] = visibility_unit - elif (visibility := self.native_visibility) is not None and ( - self._override_visibility - ): - with suppress(ValueError): - visibility_f = float(visibility) - data[ATTR_WEATHER_VISIBILITY] = round(visibility_f, ROUNDING_PRECISION) + + if (visibility_unit := self.visibility_unit) is not None: + data[ATTR_WEATHER_VISIBILITY_UNIT] = visibility_unit if (precipitation_unit := self.precipitation_unit) is not None: data[ATTR_WEATHER_PRECIPITATION_UNIT] = precipitation_unit @@ -588,97 +580,137 @@ class WeatherEntity(Entity): if self.forecast is not None: forecast = [] for forecast_entry in self.forecast: - forecast_entry_new = {} forecast_entry = dict(forecast_entry) temperature = forecast_entry[ATTR_FORECAST_TEMP] if ( - self.temperature_unit is not None - and self.native_temperature_unit is not None + self._override is True + and (temp_unit := self.temperature_unit) is not None ): + forecast_entry[ATTR_FORECAST_TEMP] = show_temp( + self.hass, + temperature, + temp_unit, + precision, + ) + elif (temp_unit := self.temperature_unit) is not None and ( + native_temp_unit := self.native_temperature_unit + ) is not None: with suppress(ValueError): value_temp = UNIT_CONVERSIONS[CONF_TEMPERATURE_UOM]( temperature, - self.native_temperature_unit, - self.temperature_unit, + native_temp_unit, + temp_unit, ) - forecast_entry_new[ATTR_FORECAST_TEMP] = ( - round(value_temp) - if precision == 0 - else round(value_temp, precision) + forecast_entry[ATTR_FORECAST_TEMP] = round_temperature( + value_temp, precision ) - else: - forecast_entry_new[ATTR_FORECAST_TEMP] = temperature - if temp_low := forecast_entry.get(ATTR_FORECAST_TEMP_LOW): + if forecast_temp_low := forecast_entry.get(ATTR_FORECAST_TEMP_LOW): if ( - self.temperature_unit is not None - and self.native_temperature_unit is not None + self._override is True + and (temp_unit := self.temperature_unit) is not None ): + forecast_entry[ATTR_FORECAST_TEMP_LOW] = show_temp( + self.hass, + forecast_temp_low, + temp_unit, + precision, + ) + elif (temperature_unit := self.temperature_unit) is not None and ( + native_temperature_unit := self.native_temperature_unit + ) is not None: with suppress(ValueError): - forecast_entry_new[ATTR_FORECAST_TEMP_LOW] = round( - UNIT_CONVERSIONS[CONF_TEMPERATURE_UOM]( - temp_low, - self.native_temperature_unit, - self.temperature_unit, - ), - ROUNDING_PRECISION, + value_temp_low = UNIT_CONVERSIONS[CONF_TEMPERATURE_UOM]( + forecast_temp_low, + native_temperature_unit, + temperature_unit, ) - else: - forecast_entry_new[ATTR_FORECAST_TEMP_LOW] = temp_low - if pressure := forecast_entry.get(ATTR_FORECAST_PRESSURE): + forecast_entry[ATTR_FORECAST_TEMP_LOW] = round_temperature( + value_temp_low, precision + ) + + if forecast_pressure := forecast_entry.get(ATTR_FORECAST_PRESSURE): if ( - self.pressure_unit is not None - and self.native_pressure_unit is not None + self._override is True + and (pressure_unit := self.pressure_unit) is not None ): + pressure = round( + self.hass.config.units.pressure( + forecast_pressure, pressure_unit + ), + ROUNDING_PRECISION, + ) + forecast_entry[ATTR_FORECAST_PRESSURE] = pressure + elif (pressure_unit := self.pressure_unit) is not None and ( + native_pressure_unit := self.native_pressure_unit + ) is not None: with suppress(ValueError): - forecast_entry_new[ATTR_FORECAST_PRESSURE] = round( + forecast_entry[ATTR_FORECAST_PRESSURE] = round( UNIT_CONVERSIONS[CONF_PRESSURE_UOM]( - pressure, - self.native_pressure_unit, - self.pressure_unit, + forecast_pressure, + native_pressure_unit, + pressure_unit, ), ROUNDING_PRECISION, ) - else: - forecast_entry_new[ATTR_FORECAST_PRESSURE] = pressure - if wind_speed := forecast_entry.get(ATTR_FORECAST_WIND_SPEED): + if forecast_wind_speed := forecast_entry.get(ATTR_FORECAST_WIND_SPEED): if ( - self.wind_speed_unit is not None - and self.native_wind_speed_unit is not None + self._override is True + and (wind_speed_unit := self.wind_speed_unit) is not None ): + wind_speed = round( + self.hass.config.units.wind_speed( + forecast_wind_speed, wind_speed_unit + ), + ROUNDING_PRECISION, + ) + forecast_entry[ATTR_FORECAST_WIND_SPEED] = wind_speed + elif (wind_speed_unit := self.wind_speed_unit) is not None and ( + native_wind_speed_unit := self.native_wind_speed_unit + ) is not None: with suppress(ValueError): - forecast_entry_new[ATTR_FORECAST_WIND_SPEED] = round( + forecast_entry[ATTR_FORECAST_WIND_SPEED] = round( UNIT_CONVERSIONS[CONF_WIND_SPEED_UOM]( - wind_speed, - self.native_wind_speed_unit, - self.wind_speed_unit, + forecast_wind_speed, + native_wind_speed_unit, + wind_speed_unit, ), ROUNDING_PRECISION, ) - else: - forecast_entry_new[ATTR_FORECAST_WIND_SPEED] = wind_speed - if precipitation := forecast_entry.get(ATTR_FORECAST_PRECIPITATION): + if forecast_precipitation := forecast_entry.get( + ATTR_FORECAST_PRECIPITATION + ): if ( - self.precipitation_unit is not None - and self.native_precipitation_unit is not None + self._override is True + and (precipitation_unit := self.precipitation_unit) is not None ): + precipitation = round( + self.hass.config.units.accumulated_precipitation( + forecast_precipitation, precipitation_unit + ), + ROUNDING_PRECISION, + ) + forecast_entry[ATTR_FORECAST_PRECIPITATION] = precipitation + elif ( + precipitation_unit := self.precipitation_unit + ) is not None and ( + native_precipitation_unit := self.native_precipitation_unit + ) is not None: with suppress(ValueError): - forecast_entry_new[ATTR_FORECAST_PRECIPITATION] = round( + forecast_entry[ATTR_FORECAST_PRECIPITATION] = round( UNIT_CONVERSIONS[CONF_PRECIPITATION_UOM]( - precipitation, - self.native_precipitation_unit, - self.precipitation_unit, + forecast_precipitation, + native_precipitation_unit, + precipitation_unit, ), ROUNDING_PRECISION, ) - else: - forecast_entry_new[ATTR_FORECAST_PRECIPITATION] = precipitation - forecast.append({**forecast_entry, **forecast_entry_new}) + forecast.append(forecast_entry) data[ATTR_FORECAST] = forecast diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 078eee7a505..2b9ac1af2ba 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -28,6 +28,7 @@ from homeassistant.const import ( LENGTH_MILLIMETERS, PRESSURE_HPA, PRESSURE_INHG, + PRESSURE_PA, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR, TEMP_CELSIUS, @@ -265,7 +266,7 @@ async def test_backwards_compability( wind_speed_value = 5 wind_speed_unit = SPEED_METERS_PER_SECOND pressure_value = 110 - pressure_unit = PRESSURE_HPA + pressure_unit = PRESSURE_PA temperature_value = 20 temperature_unit = TEMP_CELSIUS visibility_value = 11 @@ -332,7 +333,7 @@ async def test_backwards_compability( ) assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == TEMP_CELSIUS assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == approx(pressure_value) - assert state.attributes[ATTR_WEATHER_PRESSURE_UNIT] == PRESSURE_HPA + assert state.attributes[ATTR_WEATHER_PRESSURE_UNIT] == PRESSURE_PA assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx(visibility_value) assert state.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == LENGTH_KILOMETERS assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx( From 0e10c3a710da0f8f4166ddbd45025b418abcce88 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 18 Jun 2022 14:12:13 +0000 Subject: [PATCH 12/27] Fix precision --- homeassistant/components/weather/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index eac49495a65..593d810d6ff 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -455,9 +455,15 @@ class WeatherEntity(Entity): """Return the precision of the temperature value, after unit conversion.""" if hasattr(self, "_attr_precision"): return self._attr_precision + if self._override: + return ( + PRECISION_TENTHS + if self.hass.config.units.temperature_unit == TEMP_CELSIUS + else PRECISION_WHOLE + ) return ( PRECISION_TENTHS - if self.native_temperature_unit == TEMP_CELSIUS + if self.temperature_unit == TEMP_CELSIUS else PRECISION_WHOLE ) From b411979e98a984d889ad905b8e676a93e8ac4ca9 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 18 Jun 2022 15:20:18 +0000 Subject: [PATCH 13/27] Fix and add test --- homeassistant/components/weather/__init__.py | 4 +- tests/components/demo/test_weather.py | 17 -------- tests/components/weather/test_init.py | 43 +++++++++++++++++++- 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 593d810d6ff..694a4e15eae 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -499,7 +499,9 @@ class WeatherEntity(Entity): ) if (temp_unit := self.temperature_unit) is not None: - data[ATTR_WEATHER_TEMPERATURE_UNIT] = temp_unit + data[ATTR_WEATHER_TEMPERATURE_UNIT] = ( + self.hass.config.units.temperature_unit if self._override else temp_unit + ) if (humidity := self.humidity) is not None: data[ATTR_WEATHER_HUMIDITY] = round(humidity) diff --git a/tests/components/demo/test_weather.py b/tests/components/demo/test_weather.py index db3f3441df1..0ec6a118334 100644 --- a/tests/components/demo/test_weather.py +++ b/tests/components/demo/test_weather.py @@ -53,20 +53,3 @@ async def test_attributes(hass): data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 100 ) assert len(data.get(ATTR_FORECAST)) == 7 - - -async def test_temperature_convert(hass): - """Test temperature conversion.""" - assert await async_setup_component( - hass, weather.DOMAIN, {"weather": {"platform": "demo"}} - ) - hass.config.units = METRIC_SYSTEM - await hass.async_block_till_done() - - state = hass.states.get("weather.demo_weather_north") - assert state is not None - - assert state.state == "rainy" - - data = state.attributes - assert data.get(ATTR_WEATHER_TEMPERATURE) == -24.4 diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 2b9ac1af2ba..5a606f62b31 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -41,6 +41,7 @@ from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure from homeassistant.util.speed import convert as convert_speed from homeassistant.util.temperature import convert as convert_temperature +from homeassistant.util.unit_system import IMPERIAL_SYSTEM from tests.testing_config.custom_components.test import weather as WeatherPlatform @@ -259,10 +260,10 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> ) -async def test_backwards_compability( +async def test_backwards_compatibility( hass: HomeAssistant, enable_custom_integrations ) -> None: - """Test custom unit.""" + """Test backwards compatibility.""" wind_speed_value = 5 wind_speed_unit = SPEED_METERS_PER_SECOND pressure_value = 110 @@ -355,3 +356,41 @@ async def test_backwards_compability( assert ATTR_WEATHER_PRESSURE_UNIT not in state1.attributes assert ATTR_WEATHER_VISIBILITY_UNIT not in state1.attributes assert ATTR_WEATHER_PRECIPITATION_UNIT not in state1.attributes + + +async def test_backwards_compatibility_convert_temperature( + hass: HomeAssistant, enable_custom_integrations +) -> None: + """Test backward compatibility for converting temperature.""" + temperature_value = 20 + temperature_unit = TEMP_CELSIUS + + platform: WeatherPlatform = getattr(hass.components, "test.weather") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockWeatherMockForecastCompat( + name="Test", + condition=ATTR_CONDITION_SUNNY, + temperature=temperature_value, + temperature_unit=temperature_unit, + unique_id="very_unique", + ) + ) + + hass.config.units = IMPERIAL_SYSTEM + + entity0 = platform.ENTITIES[0] + assert await async_setup_component( + hass, "weather", {"weather": {"platform": "test"}} + ) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + + expected_temperature = convert_temperature( + temperature_value, temperature_unit, TEMP_FAHRENHEIT + ) + assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( + expected_temperature, rel=0.1 + ) + assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == TEMP_FAHRENHEIT From b60983a99fb3ad8bf8455ffb8b8e064381be3a50 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sat, 18 Jun 2022 17:24:05 +0000 Subject: [PATCH 14/27] Minor cleaning --- homeassistant/components/weather/__init__.py | 128 ++++++++++-------- tests/components/weather/test_init.py | 12 ++ .../custom_components/test/weather.py | 8 +- 3 files changed, 90 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 694a4e15eae..be37d4899fc 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -119,7 +119,7 @@ VALID_UNITS_VISIBILITY: tuple[str, ...] = ( LENGTH_KILOMETERS, LENGTH_MILES, ) -VALID_UNITS_SPEED: tuple[str, ...] = ( +VALID_UNITS_WIND_SPEED: tuple[str, ...] = ( SPEED_METERS_PER_SECOND, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR, @@ -138,7 +138,7 @@ VALID_UNITS: dict[str, tuple[str, ...]] = { CONF_TEMPERATURE_UOM: VALID_UNITS_TEMPERATURE, CONF_VISIBILITY_UOM: VALID_UNITS_VISIBILITY, CONF_PRECIPITATION_UOM: VALID_UNITS_PRECIPITATION, - CONF_WIND_SPEED_UOM: VALID_UNITS_SPEED, + CONF_WIND_SPEED_UOM: VALID_UNITS_WIND_SPEED, } @@ -208,17 +208,35 @@ class WeatherEntity(Entity): _attr_humidity: float | None = None _attr_ozone: float | None = None _attr_precision: float - _attr_pressure: float | None = None # Provide backwards compatibility - _attr_pressure_unit: str | None = None # Provide backwards compatibility + _attr_pressure: float | None = ( + None # Provide backwards compatibility. Use _attr_native_pressure + ) + _attr_pressure_unit: str | None = ( + None # Provide backwards compatibility. Use _attr_native_pressure_unit + ) _attr_state: None = None - _attr_temperature: float | None = None # Provide backwards compatibility - _attr_temperature_unit: str | None = None # Provide backwards compatibility - _attr_visibility: float | None = None # Provide backwards compatibility - _attr_visibility_unit: str | None = None # Provide backwards compatibility - _attr_precipitation_unit: str | None = None # Provide backwards compatibility + _attr_temperature: float | None = ( + None # Provide backwards compatibility. Use _attr_native_temperature + ) + _attr_temperature_unit: str | None = ( + None # Provide backwards compatibility. Use _attr_native_temperature_unit + ) + _attr_visibility: float | None = ( + None # Provide backwards compatibility. Use _attr_native_visibility + ) + _attr_visibility_unit: str | None = ( + None # Provide backwards compatibility. Use _attr_native_visibility_unit + ) + _attr_precipitation_unit: str | None = ( + None # Provide backwards compatibility. Use _attr_native_precipitation_unit + ) _attr_wind_bearing: float | str | None = None - _attr_wind_speed: float | None = None # Provide backwards compatibility - _attr_wind_speed_unit: str | None = None # Provide backwards compatibility + _attr_wind_speed: float | None = ( + None # Provide backwards compatibility. Use _attr_native_wind_speed + ) + _attr_wind_speed_unit: str | None = ( + None # Provide backwards compatibility. Use _attr_native_wind_speed_unit + ) _attr_native_pressure: float | None = None _attr_native_pressure_unit: str | None = None @@ -237,56 +255,58 @@ class WeatherEntity(Entity): _weather_option_precipitation_uom: str | None = None _weather_option_wind_speed_uom: str | None = None - _override: bool = False # Override for backward compatibility + _override: bool = False # Override for backward compatibility check def __init_subclass__(cls, **kwargs: Any) -> None: """Post initialisation processing.""" super().__init_subclass__(**kwargs) _reported = False - for method in ( - "_attr_temperature", - "temperature", - "_attr_temperature_unit", - "temperature_unit", - "_attr_pressure", - "pressure", - "_attr_pressure_unit", - "pressure_unit", - "_attr_wind_speed", - "wind_speed", - "_attr_wind_speed_unit", - "wind_speed_unit", - "_attr_visibility", - "visibility", - "_attr_visibility_unit", - "visibility_unit", - "_attr_precipitation_unit", - "precipitation_unit", + if any( + method in cls.__dict__ + for method in ( + "_attr_temperature", + "temperature", + "_attr_temperature_unit", + "temperature_unit", + "_attr_pressure", + "pressure", + "_attr_pressure_unit", + "pressure_unit", + "_attr_wind_speed", + "wind_speed", + "_attr_wind_speed_unit", + "wind_speed_unit", + "_attr_visibility", + "visibility", + "_attr_visibility_unit", + "visibility_unit", + "_attr_precipitation_unit", + "precipitation_unit", + ) ): - if method in cls.__dict__: - setattr(cls, "_override", True) - if _reported is False: - module = inspect.getmodule(cls) - _reported = True - if ( - module - and module.__file__ - and "custom_components" in module.__file__ - ): - report_issue = "report it to the custom component author." - else: - report_issue = ( - "create a bug report at " - "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" - ) - _LOGGER.warning( - "%s::%s is overriding deprecated methods on an instance of " - "WeatherEntity, this is not valid and will be unsupported " - "from Home Assistant 2022.10. Please %s", - cls.__module__, - cls.__name__, - report_issue, + setattr(cls, "_override", True) + if _reported is False: + module = inspect.getmodule(cls) + _reported = True + if ( + module + and module.__file__ + and "custom_components" in module.__file__ + ): + report_issue = "report it to the custom component author." + else: + report_issue = ( + "create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue" ) + _LOGGER.warning( + "%s::%s is overriding deprecated methods on an instance of " + "WeatherEntity, this is not valid and will be unsupported " + "from Home Assistant 2022.10. Please %s", + cls.__module__, + cls.__name__, + report_issue, + ) async def async_internal_added_to_hass(self) -> None: """Call when the sensor entity is added to hass.""" diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 5a606f62b31..9c68eabef37 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -20,12 +20,16 @@ from homeassistant.components.weather import ( ATTR_WEATHER_WIND_SPEED, ATTR_WEATHER_WIND_SPEED_UNIT, ROUNDING_PRECISION, + round_temperature, ) from homeassistant.const import ( LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_MILES, LENGTH_MILLIMETERS, + PRECISION_HALVES, + PRECISION_TENTHS, + PRECISION_WHOLE, PRESSURE_HPA, PRESSURE_INHG, PRESSURE_PA, @@ -394,3 +398,11 @@ async def test_backwards_compatibility_convert_temperature( expected_temperature, rel=0.1 ) assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == TEMP_FAHRENHEIT + + +async def test_backwards_compatibility_round_temperature(hass: HomeAssistant) -> None: + """Test backward compatibility for rounding temperature.""" + + assert round_temperature(20.3, PRECISION_HALVES) == 20.5 + assert round_temperature(20.3, PRECISION_TENTHS) == 20.3 + assert round_temperature(20.3, PRECISION_WHOLE) == 20 diff --git a/tests/testing_config/custom_components/test/weather.py b/tests/testing_config/custom_components/test/weather.py index 1336620860f..9fd52f685ad 100644 --- a/tests/testing_config/custom_components/test/weather.py +++ b/tests/testing_config/custom_components/test/weather.py @@ -208,10 +208,10 @@ class MockWeatherMockForecastCompat(MockWeatherCompat): """Return the forecast.""" return [ { - ATTR_FORECAST_TEMP: self.native_temperature, - ATTR_FORECAST_TEMP_LOW: self.native_temperature, - ATTR_FORECAST_PRESSURE: self.native_pressure, - ATTR_FORECAST_WIND_SPEED: self.native_wind_speed, + ATTR_FORECAST_TEMP: self.temperature, + ATTR_FORECAST_TEMP_LOW: self.temperature, + ATTR_FORECAST_PRESSURE: self.pressure, + ATTR_FORECAST_WIND_SPEED: self.wind_speed, ATTR_FORECAST_WIND_BEARING: self.wind_bearing, ATTR_FORECAST_PRECIPITATION: self._values.get("precipitation"), } From 3eff221fa45469cfcc3d36e774b939c6a936a5ea Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 20 Jun 2022 18:02:59 +0000 Subject: [PATCH 15/27] From review comments and discussions --- homeassistant/components/weather/__init__.py | 479 ++++++++---------- tests/components/weather/test_init.py | 269 +++++++++- .../custom_components/test/weather.py | 17 +- 3 files changed, 478 insertions(+), 287 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index be37d4899fc..0ea0fd55503 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -35,7 +35,6 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.temperature import display_temp as show_temp from homeassistant.helpers.typing import ConfigType from homeassistant.util import ( distance as distance_util, @@ -66,13 +65,18 @@ ATTR_CONDITION_WINDY = "windy" ATTR_CONDITION_WINDY_VARIANT = "windy-variant" ATTR_FORECAST = "forecast" ATTR_FORECAST_CONDITION: Final = "condition" +ATTR_FORECAST_NATIVE_PRECIPITATION: Final = "native_precipitation" 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_TEMP: Final = "native_temperature" ATTR_FORECAST_TEMP: Final = "temperature" +ATTR_FORECAST_NATIVE_TEMP_LOW: Final = "native_templow" ATTR_FORECAST_TEMP_LOW: Final = "templow" ATTR_FORECAST_TIME: Final = "datetime" ATTR_FORECAST_WIND_BEARING: Final = "wind_bearing" +ATTR_FORECAST_NATIVE_WIND_SPEED: Final = "native_wind_speed" ATTR_FORECAST_WIND_SPEED: Final = "wind_speed" ATTR_WEATHER_HUMIDITY = "humidity" ATTR_WEATHER_OZONE = "ozone" @@ -87,11 +91,11 @@ ATTR_WEATHER_WIND_SPEED = "wind_speed" ATTR_WEATHER_WIND_SPEED_UNIT = "wind_speed_unit" ATTR_WEATHER_PRECIPITATION_UNIT = "precipitation_unit" -CONF_PRECIPITATION_UOM = "precipitation_unit_of_measurement" -CONF_PRESSURE_UOM = "pressure_unit_of_measurement" -CONF_TEMPERATURE_UOM = "temperature_unit_of_measurement" -CONF_VISIBILITY_UOM = "visibility_unit_of_measurement" -CONF_WIND_SPEED_UOM = "wind_speed_unit_of_measurement" +ATTR_PRECIPITATION_UOM = "precipitation_unit_of_measurement" +ATTR_PRESSURE_UOM = "pressure_unit_of_measurement" +ATTR_TEMPERATURE_UOM = "temperature_unit_of_measurement" +ATTR_VISIBILITY_UOM = "visibility_unit_of_measurement" +ATTR_WIND_SPEED_UOM = "wind_speed_unit_of_measurement" DOMAIN = "weather" @@ -126,24 +130,24 @@ VALID_UNITS_WIND_SPEED: tuple[str, ...] = ( ) UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { - CONF_PRESSURE_UOM: pressure_util.convert, - CONF_TEMPERATURE_UOM: temperature_util.convert, - CONF_VISIBILITY_UOM: distance_util.convert, - CONF_PRECIPITATION_UOM: distance_util.convert, - CONF_WIND_SPEED_UOM: speed_util.convert, + ATTR_PRESSURE_UOM: pressure_util.convert, + ATTR_TEMPERATURE_UOM: temperature_util.convert, + ATTR_VISIBILITY_UOM: distance_util.convert, + ATTR_PRECIPITATION_UOM: distance_util.convert, + ATTR_WIND_SPEED_UOM: speed_util.convert, } VALID_UNITS: dict[str, tuple[str, ...]] = { - CONF_PRESSURE_UOM: VALID_UNITS_PRESSURE, - CONF_TEMPERATURE_UOM: VALID_UNITS_TEMPERATURE, - CONF_VISIBILITY_UOM: VALID_UNITS_VISIBILITY, - CONF_PRECIPITATION_UOM: VALID_UNITS_PRECIPITATION, - CONF_WIND_SPEED_UOM: VALID_UNITS_WIND_SPEED, + ATTR_PRESSURE_UOM: VALID_UNITS_PRESSURE, + ATTR_TEMPERATURE_UOM: VALID_UNITS_TEMPERATURE, + ATTR_VISIBILITY_UOM: VALID_UNITS_VISIBILITY, + ATTR_PRECIPITATION_UOM: VALID_UNITS_PRECIPITATION, + ATTR_WIND_SPEED_UOM: VALID_UNITS_WIND_SPEED, } def round_temperature(temperature: float | None, precision: float) -> float | None: - """Provide rounding for temperature.""" + """Convert temperature into preferred precision for display.""" if temperature is None: return None @@ -160,16 +164,24 @@ def round_temperature(temperature: float | None, precision: float) -> float | No class Forecast(TypedDict, total=False): - """Typed weather forecast dict.""" + """Typed weather forecast dict. + + All attributes are in native units and old attributes kept for backwards compatibility. + """ condition: str | None datetime: str precipitation_probability: int | None + native_precipitation: float | None precipitation: float | None + native_pressure: float | None pressure: float | None + native_temperature: float | None temperature: float | None + native_templow: float | None templow: float | None wind_bearing: float | str | None + native_wind_speed: float | None wind_speed: float | None @@ -244,7 +256,6 @@ class WeatherEntity(Entity): _attr_native_temperature_unit: str | None = None _attr_native_visibility: float | None = None _attr_native_visibility_unit: str | None = None - _attr_native_precipitation: float | None = None _attr_native_precipitation_unit: str | None = None _attr_native_wind_speed: float | None = None _attr_native_wind_speed_unit: str | None = None @@ -255,8 +266,6 @@ class WeatherEntity(Entity): _weather_option_precipitation_uom: str | None = None _weather_option_wind_speed_uom: str | None = None - _override: bool = False # Override for backward compatibility check - def __init_subclass__(cls, **kwargs: Any) -> None: """Post initialisation processing.""" super().__init_subclass__(**kwargs) @@ -284,7 +293,6 @@ class WeatherEntity(Entity): "precipitation_unit", ) ): - setattr(cls, "_override", True) if _reported is False: module = inspect.getmodule(cls) _reported = True @@ -331,18 +339,20 @@ class WeatherEntity(Entity): return self._attr_native_temperature_unit @property - def temperature_unit(self) -> str | None: + def temperature_unit(self) -> str: """Return the converted unit of measurement for temperature.""" + if ( + weather_option_temperature_uom := self._weather_option_temperature_uom + ) is not None: + return weather_option_temperature_uom + if (temperature_unit := self._attr_temperature_unit) is not None: return temperature_unit - if (native_temperature_unit := self.native_temperature_unit) is None: - return None + if (native_temperature_unit := self.native_temperature_unit) is not None: + return native_temperature_unit - if weather_option_temperature_uom := self._weather_option_temperature_uom: - return weather_option_temperature_uom - - return native_temperature_unit + return self.hass.config.units.temperature_unit @property def pressure(self) -> float | None: @@ -360,18 +370,20 @@ class WeatherEntity(Entity): return self._attr_native_pressure_unit @property - def pressure_unit(self) -> str | None: + def pressure_unit(self) -> str: """Return the converted unit of measurement for pressure.""" + if ( + weather_option_pressure_uom := self._weather_option_pressure_uom + ) is not None: + return weather_option_pressure_uom + if (pressure_unit := self._attr_pressure_unit) is not None: return pressure_unit - if (native_pressure_unit := self.native_pressure_unit) is None: - return None + if (native_pressure_unit := self.native_pressure_unit) is not None: + return native_pressure_unit - if weather_option_pressure_uom := self._weather_option_pressure_uom: - return weather_option_pressure_uom - - return native_pressure_unit + return self.hass.config.units.pressure_unit @property def humidity(self) -> float | None: @@ -394,18 +406,20 @@ class WeatherEntity(Entity): return self._attr_native_wind_speed_unit @property - def wind_speed_unit(self) -> str | None: + def wind_speed_unit(self) -> str: """Return the converted unit of measurement for wind speed.""" + if ( + weather_option_wind_speed_uom := self._weather_option_wind_speed_uom + ) is not None: + return weather_option_wind_speed_uom + if (wind_speed_unit := self._attr_wind_speed_unit) is not None: return wind_speed_unit - if (native_wind_speed_unit := self.native_wind_speed_unit) is None: - return None + if (native_wind_speed_unit := self.native_wind_speed_unit) is not None: + return native_wind_speed_unit - if weather_option_wind_speed_uom := self._weather_option_wind_speed_uom: - return weather_option_wind_speed_uom - - return native_wind_speed_unit + return self.hass.config.units.wind_speed_unit @property def wind_bearing(self) -> float | str | None: @@ -433,18 +447,20 @@ class WeatherEntity(Entity): return self._attr_native_visibility_unit @property - def visibility_unit(self) -> str | None: + def visibility_unit(self) -> str: """Return the converted unit of measurement for visibility.""" + if ( + weather_option_visibility_uom := self._weather_option_visibility_uom + ) is not None: + return weather_option_visibility_uom + if (visibility_unit := self._attr_visibility_unit) is not None: return visibility_unit - if (native_visibility_unit := self.native_visibility_unit) is None: - return None + if (native_visibility_unit := self.native_visibility_unit) is not None: + return native_visibility_unit - if weather_option_visibility_uom := self._weather_option_visibility_uom: - return weather_option_visibility_uom - - return native_visibility_unit + return self.hass.config.units.length_unit @property def forecast(self) -> list[Forecast] | None: @@ -457,30 +473,26 @@ class WeatherEntity(Entity): return self._attr_native_precipitation_unit @property - def precipitation_unit(self) -> str | None: + def precipitation_unit(self) -> str: """Return the converted unit of measurement for precipitation.""" + if ( + weather_option_precipitation_uom := self._weather_option_precipitation_uom + ) is not None: + return weather_option_precipitation_uom + if (precipitation_unit := self._attr_precipitation_unit) is not None: return precipitation_unit - if (native_precipitation_unit := self.native_precipitation_unit) is None: - return None + if (native_precipitation_unit := self.native_precipitation_unit) is not None: + return native_precipitation_unit - if _weather_option_precipitation_uom := self._weather_option_precipitation_uom: - return _weather_option_precipitation_uom - - return native_precipitation_unit + return self.hass.config.units.accumulated_precipitation_unit @property def precision(self) -> float: """Return the precision of the temperature value, after unit conversion.""" if hasattr(self, "_attr_precision"): return self._attr_precision - if self._override: - return ( - PRECISION_TENTHS - if self.hass.config.units.temperature_unit == TEMP_CELSIUS - else PRECISION_WHOLE - ) return ( PRECISION_TENTHS if self.temperature_unit == TEMP_CELSIUS @@ -489,39 +501,28 @@ class WeatherEntity(Entity): @final @property - def state_attributes(self): # noqa: C901 + def state_attributes(self): """Return the state attributes, converted from native units to user-configured units.""" data = {} precision = self.precision - if (temperature := self.temperature) is not None and ( - temp_unit := self.temperature_unit - ) is not None: - data[ATTR_WEATHER_TEMPERATURE] = show_temp( - self.hass, - temperature, - temp_unit, - precision, - ) - elif ( - (temperature := self.native_temperature) is not None - and (native_temp_unit := self.native_temperature_unit) is not None - and (temp_unit := self.temperature_unit) is not None - ): + if (temperature := (self.native_temperature or self.temperature)) is not None: + if (native_temperature_unit := self.native_temperature_unit) is not None: + from_unit = native_temperature_unit + to_unit = self.temperature_unit + else: + from_unit = self.temperature_unit + to_unit = self.hass.config.units.temperature_unit with suppress(ValueError): temperature_f = float(temperature) - value_temp = UNIT_CONVERSIONS[CONF_TEMPERATURE_UOM]( - temperature_f, native_temp_unit, temp_unit + value_temp = UNIT_CONVERSIONS[ATTR_TEMPERATURE_UOM]( + temperature_f, from_unit, to_unit ) data[ATTR_WEATHER_TEMPERATURE] = round_temperature( value_temp, precision ) - - if (temp_unit := self.temperature_unit) is not None: - data[ATTR_WEATHER_TEMPERATURE_UNIT] = ( - self.hass.config.units.temperature_unit if self._override else temp_unit - ) + data[ATTR_WEATHER_TEMPERATURE_UNIT] = to_unit if (humidity := self.humidity) is not None: data[ATTR_WEATHER_HUMIDITY] = round(humidity) @@ -529,216 +530,170 @@ class WeatherEntity(Entity): if (ozone := self.ozone) is not None: data[ATTR_WEATHER_OZONE] = ozone - if (pressure := self.pressure) is not None: - if (pressure_unit := self.pressure_unit) is not None: - pressure = round( - self.hass.config.units.pressure(pressure, pressure_unit), - ROUNDING_PRECISION, - ) - data[ATTR_WEATHER_PRESSURE] = pressure - elif ( - (pressure := self.native_pressure) is not None - and (pressure_unit := self.pressure_unit) is not None - and (native_pressure_unit := self.native_pressure_unit) is not None - ): + if (pressure := (self.native_pressure or self.pressure)) is not None: + if (native_pressure_unit := self.native_pressure_unit) is not None: + from_unit = native_pressure_unit + to_unit = self.pressure_unit + else: + from_unit = self.pressure_unit + to_unit = self.hass.config.units.pressure_unit with suppress(ValueError): pressure_f = float(pressure) - value_pressure = UNIT_CONVERSIONS[CONF_PRESSURE_UOM]( - pressure_f, native_pressure_unit, pressure_unit + value_pressure = UNIT_CONVERSIONS[ATTR_PRESSURE_UOM]( + pressure_f, from_unit, to_unit ) data[ATTR_WEATHER_PRESSURE] = round(value_pressure, ROUNDING_PRECISION) - - if (pressure_unit := self.pressure_unit) is not None: - data[ATTR_WEATHER_PRESSURE_UNIT] = pressure_unit + data[ATTR_WEATHER_PRESSURE_UNIT] = to_unit if (wind_bearing := self.wind_bearing) is not None: data[ATTR_WEATHER_WIND_BEARING] = wind_bearing - if (wind_speed := self.wind_speed) is not None: - if (wind_speed_unit := self.wind_speed_unit) is not None: - wind_speed = round( - self.hass.config.units.wind_speed(wind_speed, wind_speed_unit), - ROUNDING_PRECISION, - ) - data[ATTR_WEATHER_WIND_SPEED] = wind_speed - elif ( - (wind_speed := self.native_wind_speed) is not None - and (wind_speed_unit := self.wind_speed_unit) is not None - and (native_wind_speed_unit := self.native_wind_speed_unit) is not None - ): + if (wind_speed := (self.native_wind_speed or self.wind_speed)) is not None: + if (native_wind_speed_unit := self.native_wind_speed_unit) is not None: + from_unit = native_wind_speed_unit + to_unit = self.wind_speed_unit + else: + from_unit = self.wind_speed_unit + to_unit = self.hass.config.units.wind_speed_unit with suppress(ValueError): wind_speed_f = float(wind_speed) - value_wind_speed = UNIT_CONVERSIONS[CONF_WIND_SPEED_UOM]( - wind_speed_f, native_wind_speed_unit, wind_speed_unit + value_wind_speed = UNIT_CONVERSIONS[ATTR_WIND_SPEED_UOM]( + wind_speed_f, from_unit, to_unit ) data[ATTR_WEATHER_WIND_SPEED] = round( value_wind_speed, ROUNDING_PRECISION ) + data[ATTR_WEATHER_WIND_SPEED_UNIT] = to_unit - if (wind_speed_unit := self.wind_speed_unit) is not None: - data[ATTR_WEATHER_WIND_SPEED_UNIT] = wind_speed_unit - - if (visibility := self.visibility) is not None: - if (visibility_unit := self.visibility_unit) is not None: - visibility = round( - self.hass.config.units.length(visibility, visibility_unit), - ROUNDING_PRECISION, - ) - data[ATTR_WEATHER_VISIBILITY] = visibility - elif ( - (visibility := self.native_visibility) is not None - and (visibility_unit := self.visibility_unit) is not None - and (native_visibility_unit := self.native_visibility_unit) is not None - ): + if (visibility := (self.native_visibility or self.visibility)) is not None: + if (native_visibility_unit := self.native_visibility_unit) is not None: + from_unit = native_visibility_unit + to_unit = self.visibility_unit + else: + from_unit = self.visibility_unit + to_unit = self.hass.config.units.length_unit with suppress(ValueError): visibility_f = float(visibility) - value_visibility = UNIT_CONVERSIONS[CONF_VISIBILITY_UOM]( - visibility_f, native_visibility_unit, visibility_unit + value_visibility = UNIT_CONVERSIONS[ATTR_VISIBILITY_UOM]( + visibility_f, from_unit, to_unit ) data[ATTR_WEATHER_VISIBILITY] = round( value_visibility, ROUNDING_PRECISION ) - - if (visibility_unit := self.visibility_unit) is not None: - data[ATTR_WEATHER_VISIBILITY_UNIT] = visibility_unit - - if (precipitation_unit := self.precipitation_unit) is not None: - data[ATTR_WEATHER_PRECIPITATION_UNIT] = precipitation_unit + data[ATTR_WEATHER_VISIBILITY_UNIT] = to_unit if self.forecast is not None: forecast = [] for forecast_entry in self.forecast: forecast_entry = dict(forecast_entry) - temperature = forecast_entry[ATTR_FORECAST_TEMP] + + temperature = forecast_entry.get( + ATTR_FORECAST_NATIVE_TEMP, forecast_entry.get(ATTR_FORECAST_TEMP) + ) if ( - self._override is True - and (temp_unit := self.temperature_unit) is not None - ): - forecast_entry[ATTR_FORECAST_TEMP] = show_temp( - self.hass, - temperature, - temp_unit, - precision, - ) - elif (temp_unit := self.temperature_unit) is not None and ( - native_temp_unit := self.native_temperature_unit + native_temperature_unit := self.native_temperature_unit ) is not None: + from_temp_unit = native_temperature_unit + to_temp_unit = self.temperature_unit + else: + from_temp_unit = self.temperature_unit + to_temp_unit = self.hass.config.units.temperature_unit + + with suppress(ValueError): + value_temp = UNIT_CONVERSIONS[ATTR_TEMPERATURE_UOM]( + temperature, + from_temp_unit, + to_temp_unit, + ) + forecast_entry[ATTR_FORECAST_TEMP] = round_temperature( + value_temp, precision + ) + + if forecast_temp_low := forecast_entry.get( + ATTR_FORECAST_NATIVE_TEMP_LOW, + forecast_entry.get(ATTR_FORECAST_TEMP_LOW), + ): with suppress(ValueError): - value_temp = UNIT_CONVERSIONS[CONF_TEMPERATURE_UOM]( - temperature, - native_temp_unit, - temp_unit, - ) - forecast_entry[ATTR_FORECAST_TEMP] = round_temperature( - value_temp, precision - ) - - if forecast_temp_low := forecast_entry.get(ATTR_FORECAST_TEMP_LOW): - if ( - self._override is True - and (temp_unit := self.temperature_unit) is not None - ): - forecast_entry[ATTR_FORECAST_TEMP_LOW] = show_temp( - self.hass, + value_temp_low = UNIT_CONVERSIONS[ATTR_TEMPERATURE_UOM]( forecast_temp_low, - temp_unit, - precision, + from_temp_unit, + to_temp_unit, ) - elif (temperature_unit := self.temperature_unit) is not None and ( - native_temperature_unit := self.native_temperature_unit - ) is not None: - with suppress(ValueError): - value_temp_low = UNIT_CONVERSIONS[CONF_TEMPERATURE_UOM]( - forecast_temp_low, - native_temperature_unit, - temperature_unit, - ) - forecast_entry[ATTR_FORECAST_TEMP_LOW] = round_temperature( - value_temp_low, precision - ) + forecast_entry[ATTR_FORECAST_TEMP_LOW] = round_temperature( + value_temp_low, precision + ) - if forecast_pressure := forecast_entry.get(ATTR_FORECAST_PRESSURE): - if ( - self._override is True - and (pressure_unit := self.pressure_unit) is not None - ): - pressure = round( - self.hass.config.units.pressure( - forecast_pressure, pressure_unit + if forecast_pressure := forecast_entry.get( + ATTR_FORECAST_NATIVE_PRESSURE, + forecast_entry.get(ATTR_FORECAST_PRESSURE), + ): + if (native_pressure_unit := self.native_pressure_unit) is not None: + from_pressure_unit = native_pressure_unit + to_pressure_unit = self.pressure_unit + else: + from_pressure_unit = self.pressure_unit + to_pressure_unit = self.hass.config.units.pressure_unit + with suppress(ValueError): + forecast_entry[ATTR_FORECAST_PRESSURE] = round( + UNIT_CONVERSIONS[ATTR_PRESSURE_UOM]( + forecast_pressure, + from_pressure_unit, + to_pressure_unit, ), ROUNDING_PRECISION, ) - forecast_entry[ATTR_FORECAST_PRESSURE] = pressure - elif (pressure_unit := self.pressure_unit) is not None and ( - native_pressure_unit := self.native_pressure_unit - ) is not None: - with suppress(ValueError): - forecast_entry[ATTR_FORECAST_PRESSURE] = round( - UNIT_CONVERSIONS[CONF_PRESSURE_UOM]( - forecast_pressure, - native_pressure_unit, - pressure_unit, - ), - ROUNDING_PRECISION, - ) - if forecast_wind_speed := forecast_entry.get(ATTR_FORECAST_WIND_SPEED): + if forecast_wind_speed := forecast_entry.get( + ATTR_FORECAST_NATIVE_WIND_SPEED, + forecast_entry.get(ATTR_FORECAST_WIND_SPEED), + ): if ( - self._override is True - and (wind_speed_unit := self.wind_speed_unit) is not None - ): - wind_speed = round( - self.hass.config.units.wind_speed( - forecast_wind_speed, wind_speed_unit - ), - ROUNDING_PRECISION, - ) - forecast_entry[ATTR_FORECAST_WIND_SPEED] = wind_speed - elif (wind_speed_unit := self.wind_speed_unit) is not None and ( native_wind_speed_unit := self.native_wind_speed_unit ) is not None: - with suppress(ValueError): - forecast_entry[ATTR_FORECAST_WIND_SPEED] = round( - UNIT_CONVERSIONS[CONF_WIND_SPEED_UOM]( - forecast_wind_speed, - native_wind_speed_unit, - wind_speed_unit, - ), - ROUNDING_PRECISION, - ) - - if forecast_precipitation := forecast_entry.get( - ATTR_FORECAST_PRECIPITATION - ): - if ( - self._override is True - and (precipitation_unit := self.precipitation_unit) is not None - ): - precipitation = round( - self.hass.config.units.accumulated_precipitation( - forecast_precipitation, precipitation_unit + from_wind_speed_unit = native_wind_speed_unit + to_wind_speed_unit = self.wind_speed_unit + else: + from_wind_speed_unit = self.wind_speed_unit + to_wind_speed_unit = self.hass.config.units.wind_speed_unit + with suppress(ValueError): + forecast_entry[ATTR_FORECAST_WIND_SPEED] = round( + UNIT_CONVERSIONS[ATTR_WIND_SPEED_UOM]( + forecast_wind_speed, + from_wind_speed_unit, + to_wind_speed_unit, ), ROUNDING_PRECISION, ) - forecast_entry[ATTR_FORECAST_PRECIPITATION] = precipitation - elif ( - precipitation_unit := self.precipitation_unit - ) is not None and ( + + if forecast_precipitation := forecast_entry.get( + ATTR_FORECAST_NATIVE_PRECIPITATION, + forecast_entry.get(ATTR_FORECAST_PRECIPITATION), + ): + if ( native_precipitation_unit := self.native_precipitation_unit ) is not None: - with suppress(ValueError): - forecast_entry[ATTR_FORECAST_PRECIPITATION] = round( - UNIT_CONVERSIONS[CONF_PRECIPITATION_UOM]( - forecast_precipitation, - native_precipitation_unit, - precipitation_unit, - ), - ROUNDING_PRECISION, - ) + from_precipitation_unit = native_precipitation_unit + to_precipitation_unit = self.precipitation_unit + else: + from_precipitation_unit = self.precipitation_unit + to_precipitation_unit = ( + self.hass.config.units.accumulated_precipitation_unit + ) + with suppress(ValueError): + forecast_entry[ATTR_FORECAST_PRECIPITATION] = round( + UNIT_CONVERSIONS[ATTR_PRECIPITATION_UOM]( + forecast_precipitation, + from_precipitation_unit, + to_precipitation_unit, + ), + ROUNDING_PRECISION, + ) forecast.append(forecast_entry) + if forecast_precipitation is not None: + data[ATTR_WEATHER_PRECIPITATION_UNIT] = to_precipitation_unit data[ATTR_FORECAST] = forecast @@ -767,37 +722,37 @@ class WeatherEntity(Entity): self._weather_option_visibility_uom = None if weather_options := self.registry_entry.options.get(DOMAIN): if ( - (custom_unit_temperature := weather_options.get(CONF_TEMPERATURE_UOM)) - and custom_unit_temperature in VALID_UNITS[CONF_TEMPERATURE_UOM] - and self.native_temperature_unit in VALID_UNITS[CONF_TEMPERATURE_UOM] + (custom_unit_temperature := weather_options.get(ATTR_TEMPERATURE_UOM)) + and custom_unit_temperature in VALID_UNITS[ATTR_TEMPERATURE_UOM] + and self.native_temperature_unit in VALID_UNITS[ATTR_TEMPERATURE_UOM] ): self._weather_option_temperature_uom = custom_unit_temperature if ( - (custom_unit_pressure := weather_options.get(CONF_PRESSURE_UOM)) - and custom_unit_pressure in VALID_UNITS[CONF_PRESSURE_UOM] - and self.native_pressure_unit in VALID_UNITS[CONF_PRESSURE_UOM] + (custom_unit_pressure := weather_options.get(ATTR_PRESSURE_UOM)) + and custom_unit_pressure in VALID_UNITS[ATTR_PRESSURE_UOM] + and self.native_pressure_unit in VALID_UNITS[ATTR_PRESSURE_UOM] ): self._weather_option_pressure_uom = custom_unit_pressure if ( ( custom_unit_precipitation := weather_options.get( - CONF_PRECIPITATION_UOM + ATTR_PRECIPITATION_UOM ) ) - and custom_unit_precipitation in VALID_UNITS[CONF_PRECIPITATION_UOM] + and custom_unit_precipitation in VALID_UNITS[ATTR_PRECIPITATION_UOM] and self.native_precipitation_unit - in VALID_UNITS[CONF_PRECIPITATION_UOM] + in VALID_UNITS[ATTR_PRECIPITATION_UOM] ): self._weather_option_precipitation_uom = custom_unit_precipitation if ( - (custom_unit_wind_speed := weather_options.get(CONF_WIND_SPEED_UOM)) - and custom_unit_wind_speed in VALID_UNITS[CONF_WIND_SPEED_UOM] - and self.native_wind_speed_unit in VALID_UNITS[CONF_WIND_SPEED_UOM] + (custom_unit_wind_speed := weather_options.get(ATTR_WIND_SPEED_UOM)) + and custom_unit_wind_speed in VALID_UNITS[ATTR_WIND_SPEED_UOM] + and self.native_wind_speed_unit in VALID_UNITS[ATTR_WIND_SPEED_UOM] ): self._weather_option_wind_speed_uom = custom_unit_wind_speed if ( - (custom_unit_visibility := weather_options.get(CONF_VISIBILITY_UOM)) - and custom_unit_visibility in VALID_UNITS[CONF_VISIBILITY_UOM] - and self.native_visibility_unit in VALID_UNITS[CONF_VISIBILITY_UOM] + (custom_unit_visibility := weather_options.get(ATTR_VISIBILITY_UOM)) + and custom_unit_visibility in VALID_UNITS[ATTR_VISIBILITY_UOM] + and self.native_visibility_unit in VALID_UNITS[ATTR_VISIBILITY_UOM] ): self._weather_option_visibility_uom = custom_unit_visibility diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 9c68eabef37..d369218181b 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -1,4 +1,6 @@ """The test for weather entity.""" +from datetime import datetime + import pytest from pytest import approx @@ -9,7 +11,9 @@ from homeassistant.components.weather import ( ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, + ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, + ATTR_WEATHER_OZONE, ATTR_WEATHER_PRECIPITATION_UNIT, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_PRESSURE_UNIT, @@ -17,12 +21,16 @@ from homeassistant.components.weather import ( ATTR_WEATHER_TEMPERATURE_UNIT, ATTR_WEATHER_VISIBILITY, ATTR_WEATHER_VISIBILITY_UNIT, + ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, ATTR_WEATHER_WIND_SPEED_UNIT, ROUNDING_PRECISION, + Forecast, + WeatherEntity, round_temperature, ) from homeassistant.const import ( + ATTR_FRIENDLY_NAME, LENGTH_INCHES, LENGTH_KILOMETERS, LENGTH_MILES, @@ -33,6 +41,7 @@ from homeassistant.const import ( PRESSURE_HPA, PRESSURE_INHG, PRESSURE_PA, + PRESSURE_PSI, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR, TEMP_CELSIUS, @@ -45,11 +54,73 @@ from homeassistant.util.distance import convert as convert_distance from homeassistant.util.pressure import convert as convert_pressure from homeassistant.util.speed import convert as convert_speed from homeassistant.util.temperature import convert as convert_temperature -from homeassistant.util.unit_system import IMPERIAL_SYSTEM +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM from tests.testing_config.custom_components.test import weather as WeatherPlatform +class MockWeatherEntity(WeatherEntity): + """Mock a Weather Entity.""" + + def __init__(self) -> None: + """Initiate Entity.""" + super().__init__() + self._attr_condition = ATTR_CONDITION_SUNNY + self._attr_native_precipitation_unit = LENGTH_MILLIMETERS + self._attr_native_pressure = 10 + self._attr_native_pressure_unit = PRESSURE_HPA + self._attr_native_temperature = 20 + self._attr_native_temperature_unit = TEMP_CELSIUS + self._attr_native_visibility = 30 + self._attr_native_visibility_unit = LENGTH_KILOMETERS + self._attr_native_wind_speed = 3 + self._attr_native_wind_speed_unit = SPEED_METERS_PER_SECOND + self._attr_forecast = [ + Forecast( + datetime=datetime(2022, 6, 20, 20, 00, 00), + native_precipitation=1, + native_temperature=20, + ) + ] + + +class MockWeatherEntityPrecision(WeatherEntity): + """Mock a Weather Entity with precision.""" + + def __init__(self) -> None: + """Initiate Entity.""" + super().__init__() + self._attr_condition = ATTR_CONDITION_SUNNY + self._attr_native_temperature = 20.3 + self._attr_native_temperature_unit = TEMP_CELSIUS + self._attr_precision = PRECISION_HALVES + + +class MockWeatherEntityCompat(WeatherEntity): + """Mock a Weather Entity using old attributes.""" + + def __init__(self) -> None: + """Initiate Entity.""" + super().__init__() + self._attr_condition = ATTR_CONDITION_SUNNY + self._attr_precipitation_unit = LENGTH_MILLIMETERS + self._attr_pressure = 10 + self._attr_pressure_unit = PRESSURE_HPA + self._attr_temperature = 20 + self._attr_temperature_unit = TEMP_CELSIUS + self._attr_visibility = 30 + self._attr_visibility_unit = LENGTH_KILOMETERS + self._attr_wind_speed = 3 + self._attr_wind_speed_unit = SPEED_METERS_PER_SECOND + self._attr_forecast = [ + Forecast( + datetime=datetime(2022, 6, 20, 20, 00, 00), + precipitation=1, + temperature=20, + ) + ] + + async def create_entity(hass: HomeAssistant, **kwargs): """Create the weather entity to run tests on.""" kwargs = {"native_temperature": None, "native_temperature_unit": None, **kwargs} @@ -163,6 +234,23 @@ async def test_precipitation( assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx(expected, rel=1e-2) +async def test_wind_bearing_and_ozone( + hass: HomeAssistant, + enable_custom_integrations, +): + """Test wind bearing.""" + wind_bearing_value = 180 + ozone_value = 10 + + entity0 = await create_entity( + hass, wind_bearing=wind_bearing_value, ozone=ozone_value + ) + + state = hass.states.get(entity0.entity_id) + assert float(state.attributes[ATTR_WEATHER_WIND_BEARING]) == 180 + assert float(state.attributes[ATTR_WEATHER_OZONE]) == 10 + + async def test_none_forecast( hass: HomeAssistant, enable_custom_integrations, @@ -181,9 +269,9 @@ async def test_none_forecast( state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - assert forecast[ATTR_FORECAST_PRESSURE] is None - assert forecast[ATTR_FORECAST_WIND_SPEED] is None - assert forecast[ATTR_FORECAST_PRECIPITATION] is None + assert forecast.get(ATTR_FORECAST_PRESSURE) is None + assert forecast.get(ATTR_FORECAST_WIND_SPEED) is None + assert forecast.get(ATTR_FORECAST_PRECIPITATION) is None async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> None: @@ -196,6 +284,8 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> temperature_unit = TEMP_CELSIUS visibility_value = 11 visibility_unit = LENGTH_KILOMETERS + precipitation_value = 1.1 + precipitation_unit = LENGTH_MILLIMETERS set_options = { "wind_speed_unit_of_measurement": SPEED_MILES_PER_HOUR, @@ -214,7 +304,7 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> platform: WeatherPlatform = getattr(hass.components, "test.weather") platform.init(empty=True) platform.ENTITIES.append( - platform.MockWeather( + platform.MockWeatherMockForecast( name="Test", condition=ATTR_CONDITION_SUNNY, native_temperature=temperature_value, @@ -225,6 +315,8 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> native_pressure_unit=pressure_unit, native_visibility=visibility_value, native_visibility_unit=visibility_unit, + native_precipitation=precipitation_value, + native_precipitation_unit=precipitation_unit, unique_id="very_unique", ) ) @@ -236,6 +328,8 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> await hass.async_block_till_done() state = hass.states.get(entity0.entity_id) + forecast = state.attributes[ATTR_FORECAST][0] + expected_wind_speed = round( convert_speed(wind_speed_value, wind_speed_unit, SPEED_MILES_PER_HOUR), ROUNDING_PRECISION, @@ -251,6 +345,10 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> convert_distance(visibility_value, visibility_unit, LENGTH_MILES), ROUNDING_PRECISION, ) + expected_precipitation = round( + convert_distance(precipitation_value, precipitation_unit, LENGTH_INCHES), + ROUNDING_PRECISION, + ) assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx( expected_wind_speed @@ -262,6 +360,9 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx( expected_visibility ) + assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx( + expected_precipitation, rel=1e-2 + ) async def test_backwards_compatibility( @@ -279,6 +380,8 @@ async def test_backwards_compatibility( precipitation_value = 1 precipitation_unit = LENGTH_MILLIMETERS + hass.config.units = METRIC_SYSTEM + platform: WeatherPlatform = getattr(hass.components, "test.weather") platform.init(empty=True) platform.ENTITIES.append( @@ -305,13 +408,9 @@ async def test_backwards_compatibility( temperature=temperature_value, temperature_unit=temperature_unit, wind_speed=wind_speed_value, - wind_speed_unit=None, pressure=pressure_value, - pressure_unit=None, visibility=visibility_value, - visibility_unit=None, precipitation=precipitation_value, - precipitation_unit=None, unique_id="very_unique2", ) ) @@ -347,27 +446,37 @@ async def test_backwards_compatibility( assert state.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == LENGTH_MILLIMETERS assert float(state1.attributes[ATTR_WEATHER_WIND_SPEED]) == approx(wind_speed_value) + assert state1.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] == SPEED_METERS_PER_SECOND assert float(state1.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( temperature_value, rel=0.1 ) assert state1.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == TEMP_CELSIUS assert float(state1.attributes[ATTR_WEATHER_PRESSURE]) == approx(pressure_value) + assert state1.attributes[ATTR_WEATHER_PRESSURE_UNIT] == PRESSURE_PA assert float(state1.attributes[ATTR_WEATHER_VISIBILITY]) == approx(visibility_value) + assert state1.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == LENGTH_KILOMETERS assert float(forecast1[ATTR_FORECAST_PRECIPITATION]) == approx( precipitation_value, rel=1e-2 ) - assert ATTR_WEATHER_WIND_SPEED_UNIT not in state1.attributes - assert ATTR_WEATHER_PRESSURE_UNIT not in state1.attributes - assert ATTR_WEATHER_VISIBILITY_UNIT not in state1.attributes - assert ATTR_WEATHER_PRECIPITATION_UNIT not in state1.attributes + assert state1.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == LENGTH_MILLIMETERS -async def test_backwards_compatibility_convert_temperature( +async def test_backwards_compatibility_convert_values( hass: HomeAssistant, enable_custom_integrations ) -> None: - """Test backward compatibility for converting temperature.""" + """Test backward compatibility for converting values.""" + wind_speed_value = 5 + wind_speed_unit = SPEED_METERS_PER_SECOND + pressure_value = 110 + pressure_unit = PRESSURE_PA temperature_value = 20 temperature_unit = TEMP_CELSIUS + visibility_value = 11 + visibility_unit = LENGTH_KILOMETERS + precipitation_value = 1 + precipitation_unit = LENGTH_MILLIMETERS + + hass.config.units = IMPERIAL_SYSTEM platform: WeatherPlatform = getattr(hass.components, "test.weather") platform.init(empty=True) @@ -377,12 +486,18 @@ async def test_backwards_compatibility_convert_temperature( condition=ATTR_CONDITION_SUNNY, temperature=temperature_value, temperature_unit=temperature_unit, + wind_speed=wind_speed_value, + wind_speed_unit=wind_speed_unit, + pressure=pressure_value, + pressure_unit=pressure_unit, + visibility=visibility_value, + visibility_unit=visibility_unit, + precipitation=precipitation_value, + precipitation_unit=precipitation_unit, unique_id="very_unique", ) ) - hass.config.units = IMPERIAL_SYSTEM - entity0 = platform.ENTITIES[0] assert await async_setup_component( hass, "weather", {"weather": {"platform": "test"}} @@ -391,13 +506,48 @@ async def test_backwards_compatibility_convert_temperature( state = hass.states.get(entity0.entity_id) + expected_wind_speed = round( + convert_speed(wind_speed_value, wind_speed_unit, SPEED_MILES_PER_HOUR), + ROUNDING_PRECISION, + ) expected_temperature = convert_temperature( temperature_value, temperature_unit, TEMP_FAHRENHEIT ) - assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( - expected_temperature, rel=0.1 + expected_pressure = round( + convert_pressure(pressure_value, pressure_unit, PRESSURE_PSI), + ROUNDING_PRECISION, ) - assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == TEMP_FAHRENHEIT + expected_visibility = round( + convert_distance(visibility_value, visibility_unit, LENGTH_MILES), + ROUNDING_PRECISION, + ) + expected_precipitation = round( + convert_distance(precipitation_value, precipitation_unit, LENGTH_INCHES), + ROUNDING_PRECISION, + ) + + assert state.attributes == { + ATTR_FORECAST: [ + { + ATTR_FORECAST_PRECIPITATION: approx(expected_precipitation, rel=0.1), + ATTR_FORECAST_PRESSURE: approx(expected_pressure, rel=0.1), + ATTR_FORECAST_TEMP: approx(expected_temperature, rel=0.1), + ATTR_FORECAST_TEMP_LOW: approx(expected_temperature, rel=0.1), + ATTR_FORECAST_WIND_BEARING: None, + ATTR_FORECAST_WIND_SPEED: approx(expected_wind_speed, rel=0.1), + } + ], + ATTR_FRIENDLY_NAME: "Test", + ATTR_WEATHER_PRECIPITATION_UNIT: LENGTH_INCHES, + ATTR_WEATHER_PRESSURE: approx(expected_pressure, rel=0.1), + ATTR_WEATHER_PRESSURE_UNIT: PRESSURE_PSI, + ATTR_WEATHER_TEMPERATURE: approx(expected_temperature, rel=0.1), + ATTR_WEATHER_TEMPERATURE_UNIT: TEMP_FAHRENHEIT, + ATTR_WEATHER_VISIBILITY: approx(expected_visibility, rel=0.1), + ATTR_WEATHER_VISIBILITY_UNIT: LENGTH_MILES, + ATTR_WEATHER_WIND_SPEED: approx(expected_wind_speed, rel=0.1), + ATTR_WEATHER_WIND_SPEED_UNIT: SPEED_MILES_PER_HOUR, + } async def test_backwards_compatibility_round_temperature(hass: HomeAssistant) -> None: @@ -406,3 +556,82 @@ async def test_backwards_compatibility_round_temperature(hass: HomeAssistant) -> assert round_temperature(20.3, PRECISION_HALVES) == 20.5 assert round_temperature(20.3, PRECISION_TENTHS) == 20.3 assert round_temperature(20.3, PRECISION_WHOLE) == 20 + assert round_temperature(None, PRECISION_WHOLE) is None + + +async def test_attr(hass: HomeAssistant) -> None: + """Test the _attr attributes.""" + + weather = MockWeatherEntity() + weather.hass = hass + + assert weather.condition == ATTR_CONDITION_SUNNY + assert weather.native_precipitation_unit == LENGTH_MILLIMETERS + assert weather.precipitation_unit == LENGTH_MILLIMETERS + assert weather.native_pressure == 10 + assert weather.native_pressure_unit == PRESSURE_HPA + assert weather.pressure_unit == PRESSURE_HPA + assert weather.native_temperature == 20 + assert weather.native_temperature_unit == TEMP_CELSIUS + assert weather.temperature_unit == TEMP_CELSIUS + assert weather.native_visibility == 30 + assert weather.native_visibility_unit == LENGTH_KILOMETERS + assert weather.visibility_unit == LENGTH_KILOMETERS + assert weather.native_wind_speed == 3 + assert weather.native_wind_speed_unit == SPEED_METERS_PER_SECOND + assert weather.wind_speed_unit == SPEED_METERS_PER_SECOND + + +async def test_attr_compatibility(hass: HomeAssistant) -> None: + """Test the _attr attributes in compatibility mode.""" + + weather = MockWeatherEntityCompat() + weather.hass = hass + + assert weather.condition == ATTR_CONDITION_SUNNY + assert weather.precipitation_unit == LENGTH_MILLIMETERS + assert weather.pressure == 10 + assert weather.pressure_unit == PRESSURE_HPA + assert weather.temperature == 20 + assert weather.temperature_unit == TEMP_CELSIUS + assert weather.visibility == 30 + assert weather.visibility_unit == LENGTH_KILOMETERS + assert weather.wind_speed == 3 + assert weather.wind_speed_unit == SPEED_METERS_PER_SECOND + + forecast_entry = [ + Forecast( + datetime=datetime(2022, 6, 20, 20, 00, 00), + precipitation=1, + temperature=20, + ) + ] + + assert weather.forecast == forecast_entry + + assert weather.state_attributes == { + ATTR_FORECAST: forecast_entry, + ATTR_WEATHER_PRESSURE: 1000.0, + ATTR_WEATHER_PRESSURE_UNIT: PRESSURE_PA, + ATTR_WEATHER_TEMPERATURE: 20.0, + ATTR_WEATHER_TEMPERATURE_UNIT: TEMP_CELSIUS, + ATTR_WEATHER_VISIBILITY: 30.0, + ATTR_WEATHER_VISIBILITY_UNIT: LENGTH_KILOMETERS, + ATTR_WEATHER_WIND_SPEED: 3.0, + ATTR_WEATHER_WIND_SPEED_UNIT: SPEED_METERS_PER_SECOND, + ATTR_WEATHER_PRECIPITATION_UNIT: LENGTH_MILLIMETERS, + } + + +async def test_precision_for_temperature(hass: HomeAssistant) -> None: + """Test the precision for temperature.""" + + weather = MockWeatherEntityPrecision() + weather.hass = hass + + assert weather.condition == ATTR_CONDITION_SUNNY + assert weather.native_temperature == 20.3 + assert weather.temperature_unit == TEMP_CELSIUS + assert weather.precision == PRECISION_HALVES + + assert weather.state_attributes[ATTR_WEATHER_TEMPERATURE] == 20.5 diff --git a/tests/testing_config/custom_components/test/weather.py b/tests/testing_config/custom_components/test/weather.py index 9fd52f685ad..23a9569c785 100644 --- a/tests/testing_config/custom_components/test/weather.py +++ b/tests/testing_config/custom_components/test/weather.py @@ -6,6 +6,11 @@ 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_PRECIPITATION, + ATTR_FORECAST_NATIVE_PRESSURE, + ATTR_FORECAST_NATIVE_TEMP, + ATTR_FORECAST_NATIVE_TEMP_LOW, + ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, @@ -190,12 +195,14 @@ class MockWeatherMockForecast(MockWeather): """Return the forecast.""" return [ { - ATTR_FORECAST_TEMP: self.native_temperature, - ATTR_FORECAST_TEMP_LOW: self.native_temperature, - ATTR_FORECAST_PRESSURE: self.native_pressure, - ATTR_FORECAST_WIND_SPEED: self.native_wind_speed, + ATTR_FORECAST_NATIVE_TEMP: self.native_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, ATTR_FORECAST_WIND_BEARING: self.wind_bearing, - ATTR_FORECAST_PRECIPITATION: self._values.get("native_precipitation"), + ATTR_FORECAST_NATIVE_PRECIPITATION: self._values.get( + "native_precipitation" + ), } ] From a4190e1014fb6a5854fec55709de6cc2ac6352fc Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 20 Jun 2022 18:18:15 +0000 Subject: [PATCH 16/27] Add text to docstrings --- homeassistant/components/weather/__init__.py | 45 ++++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 0ea0fd55503..43220ac054d 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -325,7 +325,10 @@ class WeatherEntity(Entity): @property def temperature(self) -> float | None: - """Return the temperature for backward compatibility.""" + """Return the temperature for backward compatibility. + + Should not be set by integrations. + """ return self._attr_temperature @property @@ -340,7 +343,10 @@ class WeatherEntity(Entity): @property def temperature_unit(self) -> str: - """Return the converted unit of measurement for temperature.""" + """Return the converted unit of measurement for temperature. + + Should not be set by integrations. + """ if ( weather_option_temperature_uom := self._weather_option_temperature_uom ) is not None: @@ -356,7 +362,10 @@ class WeatherEntity(Entity): @property def pressure(self) -> float | None: - """Return the pressure for backward compatibility.""" + """Return the pressure for backward compatibility. + + Should not be set by integrations. + """ return self._attr_pressure @property @@ -371,7 +380,10 @@ class WeatherEntity(Entity): @property def pressure_unit(self) -> str: - """Return the converted unit of measurement for pressure.""" + """Return the converted unit of measurement for pressure. + + Should not be set by integrations. + """ if ( weather_option_pressure_uom := self._weather_option_pressure_uom ) is not None: @@ -392,7 +404,10 @@ class WeatherEntity(Entity): @property def wind_speed(self) -> float | None: - """Return the wind_speed for backward compatibility.""" + """Return the wind_speed for backward compatibility. + + Should not be set by integrations. + """ return self._attr_wind_speed @property @@ -407,7 +422,10 @@ class WeatherEntity(Entity): @property def wind_speed_unit(self) -> str: - """Return the converted unit of measurement for wind speed.""" + """Return the converted unit of measurement for wind speed. + + Should not be set by integrations. + """ if ( weather_option_wind_speed_uom := self._weather_option_wind_speed_uom ) is not None: @@ -433,7 +451,10 @@ class WeatherEntity(Entity): @property def visibility(self) -> float | None: - """Return the visibility for backward compatibility.""" + """Return the visibility for backward compatibility. + + Should not be set by integrations. + """ return self._attr_visibility @property @@ -448,7 +469,10 @@ class WeatherEntity(Entity): @property def visibility_unit(self) -> str: - """Return the converted unit of measurement for visibility.""" + """Return the converted unit of measurement for visibility. + + Should not be set by integrations. + """ if ( weather_option_visibility_uom := self._weather_option_visibility_uom ) is not None: @@ -474,7 +498,10 @@ class WeatherEntity(Entity): @property def precipitation_unit(self) -> str: - """Return the converted unit of measurement for precipitation.""" + """Return the converted unit of measurement for precipitation. + + Should not be set by integrations. + """ if ( weather_option_precipitation_uom := self._weather_option_precipitation_uom ) is not None: From cb19657aab2b6a78278d8274a10bc2bc0479c320 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 20 Jun 2022 20:07:44 +0000 Subject: [PATCH 17/27] Fix forecast issues --- homeassistant/components/weather/__init__.py | 24 ++++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 43220ac054d..c7331c85ea4 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -628,8 +628,9 @@ class WeatherEntity(Entity): to_temp_unit = self.hass.config.units.temperature_unit with suppress(ValueError): + temperature_f = float(temperature) value_temp = UNIT_CONVERSIONS[ATTR_TEMPERATURE_UOM]( - temperature, + temperature_f, from_temp_unit, to_temp_unit, ) @@ -642,8 +643,9 @@ class WeatherEntity(Entity): forecast_entry.get(ATTR_FORECAST_TEMP_LOW), ): with suppress(ValueError): + forecast_temp_low_f = float(forecast_temp_low) value_temp_low = UNIT_CONVERSIONS[ATTR_TEMPERATURE_UOM]( - forecast_temp_low, + forecast_temp_low_f, from_temp_unit, to_temp_unit, ) @@ -663,9 +665,10 @@ class WeatherEntity(Entity): from_pressure_unit = self.pressure_unit to_pressure_unit = self.hass.config.units.pressure_unit with suppress(ValueError): + forecast_pressure_f = float(forecast_pressure) forecast_entry[ATTR_FORECAST_PRESSURE] = round( UNIT_CONVERSIONS[ATTR_PRESSURE_UOM]( - forecast_pressure, + forecast_pressure_f, from_pressure_unit, to_pressure_unit, ), @@ -685,15 +688,20 @@ class WeatherEntity(Entity): from_wind_speed_unit = self.wind_speed_unit to_wind_speed_unit = self.hass.config.units.wind_speed_unit with suppress(ValueError): + forecast_wind_speed_f = float(forecast_wind_speed) forecast_entry[ATTR_FORECAST_WIND_SPEED] = round( UNIT_CONVERSIONS[ATTR_WIND_SPEED_UOM]( - forecast_wind_speed, + forecast_wind_speed_f, from_wind_speed_unit, to_wind_speed_unit, ), ROUNDING_PRECISION, ) + from_precipitation_unit = self.precipitation_unit + to_precipitation_unit = ( + self.hass.config.units.accumulated_precipitation_unit + ) if forecast_precipitation := forecast_entry.get( ATTR_FORECAST_NATIVE_PRECIPITATION, forecast_entry.get(ATTR_FORECAST_PRECIPITATION), @@ -703,15 +711,11 @@ class WeatherEntity(Entity): ) is not None: from_precipitation_unit = native_precipitation_unit to_precipitation_unit = self.precipitation_unit - else: - from_precipitation_unit = self.precipitation_unit - to_precipitation_unit = ( - self.hass.config.units.accumulated_precipitation_unit - ) with suppress(ValueError): + forecast_precipitation_f = float(forecast_precipitation) forecast_entry[ATTR_FORECAST_PRECIPITATION] = round( UNIT_CONVERSIONS[ATTR_PRECIPITATION_UOM]( - forecast_precipitation, + forecast_precipitation_f, from_precipitation_unit, to_precipitation_unit, ), From dd8a0a0c63d677fdb44a3b4d132ef50bf695b46e Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 21 Jun 2022 11:54:03 +0200 Subject: [PATCH 18/27] Apply suggested changes --- homeassistant/components/weather/__init__.py | 345 ++++++++++--------- 1 file changed, 190 insertions(+), 155 deletions(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index c7331c85ea4..e7c8cd4a3c0 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -91,12 +91,6 @@ ATTR_WEATHER_WIND_SPEED = "wind_speed" ATTR_WEATHER_WIND_SPEED_UNIT = "wind_speed_unit" ATTR_WEATHER_PRECIPITATION_UNIT = "precipitation_unit" -ATTR_PRECIPITATION_UOM = "precipitation_unit_of_measurement" -ATTR_PRESSURE_UOM = "pressure_unit_of_measurement" -ATTR_TEMPERATURE_UOM = "temperature_unit_of_measurement" -ATTR_VISIBILITY_UOM = "visibility_unit_of_measurement" -ATTR_WIND_SPEED_UOM = "wind_speed_unit_of_measurement" - DOMAIN = "weather" ENTITY_ID_FORMAT = DOMAIN + ".{}" @@ -130,19 +124,19 @@ VALID_UNITS_WIND_SPEED: tuple[str, ...] = ( ) UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { - ATTR_PRESSURE_UOM: pressure_util.convert, - ATTR_TEMPERATURE_UOM: temperature_util.convert, - ATTR_VISIBILITY_UOM: distance_util.convert, - ATTR_PRECIPITATION_UOM: distance_util.convert, - ATTR_WIND_SPEED_UOM: speed_util.convert, + ATTR_WEATHER_PRESSURE_UNIT: pressure_util.convert, + ATTR_WEATHER_TEMPERATURE_UNIT: temperature_util.convert, + ATTR_WEATHER_VISIBILITY_UNIT: distance_util.convert, + ATTR_WEATHER_PRECIPITATION_UNIT: distance_util.convert, + ATTR_WEATHER_WIND_SPEED_UNIT: speed_util.convert, } VALID_UNITS: dict[str, tuple[str, ...]] = { - ATTR_PRESSURE_UOM: VALID_UNITS_PRESSURE, - ATTR_TEMPERATURE_UOM: VALID_UNITS_TEMPERATURE, - ATTR_VISIBILITY_UOM: VALID_UNITS_VISIBILITY, - ATTR_PRECIPITATION_UOM: VALID_UNITS_PRECIPITATION, - ATTR_WIND_SPEED_UOM: VALID_UNITS_WIND_SPEED, + ATTR_WEATHER_PRESSURE_UNIT: VALID_UNITS_PRESSURE, + ATTR_WEATHER_TEMPERATURE_UNIT: VALID_UNITS_TEMPERATURE, + ATTR_WEATHER_VISIBILITY_UNIT: VALID_UNITS_VISIBILITY, + ATTR_WEATHER_PRECIPITATION_UNIT: VALID_UNITS_PRECIPITATION, + ATTR_WEATHER_WIND_SPEED_UNIT: VALID_UNITS_WIND_SPEED, } @@ -334,15 +328,29 @@ class WeatherEntity(Entity): @property def native_temperature(self) -> float | None: """Return the temperature in native units.""" + if (temperature := self.temperature) is not None: + return temperature + return self._attr_native_temperature @property def native_temperature_unit(self) -> str | None: """Return the native unit of measurement for temperature.""" + if (temperature_unit := self.temperature_unit) is not None: + return temperature_unit + return self._attr_native_temperature_unit @property - def temperature_unit(self) -> str: + def temperature_unit(self) -> str | None: + """Return the temperature unit for backward compatibility. + + Should not be set by integrations. + """ + return self._attr_temperature_unit + + @property + def _temperature_unit(self) -> str: """Return the converted unit of measurement for temperature. Should not be set by integrations. @@ -352,12 +360,6 @@ class WeatherEntity(Entity): ) is not None: return weather_option_temperature_uom - if (temperature_unit := self._attr_temperature_unit) is not None: - return temperature_unit - - if (native_temperature_unit := self.native_temperature_unit) is not None: - return native_temperature_unit - return self.hass.config.units.temperature_unit @property @@ -371,15 +373,29 @@ class WeatherEntity(Entity): @property def native_pressure(self) -> float | None: """Return the pressure in native units.""" + if (pressure := self.pressure) is not None: + return pressure + return self._attr_native_pressure @property def native_pressure_unit(self) -> str | None: """Return the native unit of measurement for pressure.""" + if (pressure_unit := self.pressure_unit) is not None: + return pressure_unit + return self._attr_native_pressure_unit @property - def pressure_unit(self) -> str: + def pressure_unit(self) -> str | None: + """Return the pressure unit for backward compatibility. + + Should not be set by integrations. + """ + return self._attr_pressure_unit + + @property + def _pressure_unit(self) -> str: """Return the converted unit of measurement for pressure. Should not be set by integrations. @@ -389,13 +405,7 @@ class WeatherEntity(Entity): ) is not None: return weather_option_pressure_uom - if (pressure_unit := self._attr_pressure_unit) is not None: - return pressure_unit - - if (native_pressure_unit := self.native_pressure_unit) is not None: - return native_pressure_unit - - return self.hass.config.units.pressure_unit + return PRESSURE_HPA if self.hass.config.units.is_metric else PRESSURE_INHG @property def humidity(self) -> float | None: @@ -413,15 +423,29 @@ class WeatherEntity(Entity): @property def native_wind_speed(self) -> float | None: """Return the wind speed in native units.""" + if (wind_speed := self.wind_speed) is not None: + return wind_speed + return self._attr_native_wind_speed @property def native_wind_speed_unit(self) -> str | None: """Return the native unit of measurement for wind speed.""" + if (wind_speed_unit := self.wind_speed_unit) is not None: + return wind_speed_unit + return self._attr_native_wind_speed_unit @property - def wind_speed_unit(self) -> str: + def wind_speed_unit(self) -> str | None: + """Return the wind_speed unit for backward compatibility. + + Should not be set by integrations. + """ + return self._attr_wind_speed_unit + + @property + def _wind_speed_unit(self) -> str: """Return the converted unit of measurement for wind speed. Should not be set by integrations. @@ -431,13 +455,11 @@ class WeatherEntity(Entity): ) is not None: return weather_option_wind_speed_uom - if (wind_speed_unit := self._attr_wind_speed_unit) is not None: - return wind_speed_unit - - if (native_wind_speed_unit := self.native_wind_speed_unit) is not None: - return native_wind_speed_unit - - return self.hass.config.units.wind_speed_unit + return ( + SPEED_KILOMETERS_PER_HOUR + if self.hass.config.units.is_metric + else SPEED_MILES_PER_HOUR + ) @property def wind_bearing(self) -> float | str | None: @@ -460,15 +482,29 @@ class WeatherEntity(Entity): @property def native_visibility(self) -> float | None: """Return the visibility in native units.""" + if (visibility := self.visibility) is not None: + return visibility + return self._attr_native_visibility @property def native_visibility_unit(self) -> str | None: """Return the native unit of measurement for visibility.""" + if (visibility_unit := self.visibility_unit) is not None: + return visibility_unit + return self._attr_native_visibility_unit @property - def visibility_unit(self) -> str: + def visibility_unit(self) -> str | None: + """Return the visibility unit for backward compatibility. + + Should not be set by integrations. + """ + return self._attr_visibility_unit + + @property + def _visibility_unit(self) -> str: """Return the converted unit of measurement for visibility. Should not be set by integrations. @@ -478,12 +514,6 @@ class WeatherEntity(Entity): ) is not None: return weather_option_visibility_uom - if (visibility_unit := self._attr_visibility_unit) is not None: - return visibility_unit - - if (native_visibility_unit := self.native_visibility_unit) is not None: - return native_visibility_unit - return self.hass.config.units.length_unit @property @@ -494,10 +524,21 @@ class WeatherEntity(Entity): @property def native_precipitation_unit(self) -> str | None: """Return the native unit of measurement for accumulated precipitation.""" + if (precipitation_unit := self.precipitation_unit) is not None: + return precipitation_unit + return self._attr_native_precipitation_unit @property - def precipitation_unit(self) -> str: + def precipitation_unit(self) -> str | None: + """Return the precipitation unit for backward compatibility. + + Should not be set by integrations. + """ + return self._attr_precipitation_unit + + @property + def _precipitation_unit(self) -> str: """Return the converted unit of measurement for precipitation. Should not be set by integrations. @@ -507,12 +548,6 @@ class WeatherEntity(Entity): ) is not None: return weather_option_precipitation_uom - if (precipitation_unit := self._attr_precipitation_unit) is not None: - return precipitation_unit - - if (native_precipitation_unit := self.native_precipitation_unit) is not None: - return native_precipitation_unit - return self.hass.config.units.accumulated_precipitation_unit @property @@ -534,22 +569,20 @@ class WeatherEntity(Entity): precision = self.precision - if (temperature := (self.native_temperature or self.temperature)) is not None: - if (native_temperature_unit := self.native_temperature_unit) is not None: - from_unit = native_temperature_unit - to_unit = self.temperature_unit - else: - from_unit = self.temperature_unit - to_unit = self.hass.config.units.temperature_unit - with suppress(ValueError): + if (temperature := self.native_temperature) is not None: + from_unit = self.native_temperature_unit or self._temperature_unit + to_unit = self._temperature_unit + try: temperature_f = float(temperature) - value_temp = UNIT_CONVERSIONS[ATTR_TEMPERATURE_UOM]( + value_temp = UNIT_CONVERSIONS[ATTR_WEATHER_TEMPERATURE_UNIT]( temperature_f, from_unit, to_unit ) data[ATTR_WEATHER_TEMPERATURE] = round_temperature( value_temp, precision ) data[ATTR_WEATHER_TEMPERATURE_UNIT] = to_unit + except (TypeError, ValueError): + data[ATTR_WEATHER_TEMPERATURE] = temperature if (humidity := self.humidity) is not None: data[ATTR_WEATHER_HUMIDITY] = round(humidity) @@ -557,57 +590,51 @@ class WeatherEntity(Entity): if (ozone := self.ozone) is not None: data[ATTR_WEATHER_OZONE] = ozone - if (pressure := (self.native_pressure or self.pressure)) is not None: - if (native_pressure_unit := self.native_pressure_unit) is not None: - from_unit = native_pressure_unit - to_unit = self.pressure_unit - else: - from_unit = self.pressure_unit - to_unit = self.hass.config.units.pressure_unit - with suppress(ValueError): + if (pressure := self.native_pressure) is not None: + from_unit = self.native_pressure_unit or self._pressure_unit + to_unit = self._pressure_unit + try: pressure_f = float(pressure) - value_pressure = UNIT_CONVERSIONS[ATTR_PRESSURE_UOM]( + value_pressure = UNIT_CONVERSIONS[ATTR_WEATHER_PRESSURE_UNIT]( pressure_f, from_unit, to_unit ) data[ATTR_WEATHER_PRESSURE] = round(value_pressure, ROUNDING_PRECISION) data[ATTR_WEATHER_PRESSURE_UNIT] = to_unit + except (TypeError, ValueError): + data[ATTR_WEATHER_PRESSURE] = pressure if (wind_bearing := self.wind_bearing) is not None: data[ATTR_WEATHER_WIND_BEARING] = wind_bearing - if (wind_speed := (self.native_wind_speed or self.wind_speed)) is not None: - if (native_wind_speed_unit := self.native_wind_speed_unit) is not None: - from_unit = native_wind_speed_unit - to_unit = self.wind_speed_unit - else: - from_unit = self.wind_speed_unit - to_unit = self.hass.config.units.wind_speed_unit - with suppress(ValueError): + if (wind_speed := self.native_wind_speed) is not None: + from_unit = self.native_wind_speed_unit or self._wind_speed_unit + to_unit = self._wind_speed_unit + try: wind_speed_f = float(wind_speed) - value_wind_speed = UNIT_CONVERSIONS[ATTR_WIND_SPEED_UOM]( + value_wind_speed = UNIT_CONVERSIONS[ATTR_WEATHER_WIND_SPEED_UNIT]( wind_speed_f, from_unit, to_unit ) data[ATTR_WEATHER_WIND_SPEED] = round( value_wind_speed, ROUNDING_PRECISION ) data[ATTR_WEATHER_WIND_SPEED_UNIT] = to_unit + except (TypeError, ValueError): + data[ATTR_WEATHER_WIND_SPEED] = wind_speed - if (visibility := (self.native_visibility or self.visibility)) is not None: - if (native_visibility_unit := self.native_visibility_unit) is not None: - from_unit = native_visibility_unit - to_unit = self.visibility_unit - else: - from_unit = self.visibility_unit - to_unit = self.hass.config.units.length_unit - with suppress(ValueError): + if (visibility := self.native_visibility) is not None: + from_unit = self.native_visibility_unit or self._visibility_unit + to_unit = self._visibility_unit + try: visibility_f = float(visibility) - value_visibility = UNIT_CONVERSIONS[ATTR_VISIBILITY_UOM]( + value_visibility = UNIT_CONVERSIONS[ATTR_WEATHER_VISIBILITY_UNIT]( visibility_f, from_unit, to_unit ) data[ATTR_WEATHER_VISIBILITY] = round( value_visibility, ROUNDING_PRECISION ) data[ATTR_WEATHER_VISIBILITY_UNIT] = to_unit + except (TypeError, ValueError): + data[ATTR_WEATHER_VISIBILITY] = visibility if self.forecast is not None: forecast = [] @@ -618,33 +645,32 @@ class WeatherEntity(Entity): ATTR_FORECAST_NATIVE_TEMP, forecast_entry.get(ATTR_FORECAST_TEMP) ) - if ( - native_temperature_unit := self.native_temperature_unit - ) is not None: - from_temp_unit = native_temperature_unit - to_temp_unit = self.temperature_unit - else: - from_temp_unit = self.temperature_unit - to_temp_unit = self.hass.config.units.temperature_unit + from_temp_unit = self.native_temperature_unit or self._temperature_unit + to_temp_unit = self._temperature_unit - with suppress(ValueError): - temperature_f = float(temperature) - value_temp = UNIT_CONVERSIONS[ATTR_TEMPERATURE_UOM]( - temperature_f, - from_temp_unit, - to_temp_unit, - ) - forecast_entry[ATTR_FORECAST_TEMP] = round_temperature( - value_temp, precision - ) + if temperature is None: + forecast_entry[ATTR_FORECAST_TEMP] = None + else: + with suppress(TypeError, ValueError): + temperature_f = float(temperature) + value_temp = UNIT_CONVERSIONS[ATTR_WEATHER_TEMPERATURE_UNIT]( + temperature_f, + from_temp_unit, + to_temp_unit, + ) + forecast_entry[ATTR_FORECAST_TEMP] = round_temperature( + value_temp, precision + ) if forecast_temp_low := forecast_entry.get( ATTR_FORECAST_NATIVE_TEMP_LOW, forecast_entry.get(ATTR_FORECAST_TEMP_LOW), ): - with suppress(ValueError): + with suppress(TypeError, ValueError): forecast_temp_low_f = float(forecast_temp_low) - value_temp_low = UNIT_CONVERSIONS[ATTR_TEMPERATURE_UOM]( + value_temp_low = UNIT_CONVERSIONS[ + ATTR_WEATHER_TEMPERATURE_UNIT + ]( forecast_temp_low_f, from_temp_unit, to_temp_unit, @@ -658,16 +684,14 @@ class WeatherEntity(Entity): ATTR_FORECAST_NATIVE_PRESSURE, forecast_entry.get(ATTR_FORECAST_PRESSURE), ): - if (native_pressure_unit := self.native_pressure_unit) is not None: - from_pressure_unit = native_pressure_unit - to_pressure_unit = self.pressure_unit - else: - from_pressure_unit = self.pressure_unit - to_pressure_unit = self.hass.config.units.pressure_unit - with suppress(ValueError): + from_pressure_unit = ( + self.native_pressure_unit or self._pressure_unit + ) + to_pressure_unit = self._pressure_unit + with suppress(TypeError, ValueError): forecast_pressure_f = float(forecast_pressure) forecast_entry[ATTR_FORECAST_PRESSURE] = round( - UNIT_CONVERSIONS[ATTR_PRESSURE_UOM]( + UNIT_CONVERSIONS[ATTR_WEATHER_PRESSURE_UNIT]( forecast_pressure_f, from_pressure_unit, to_pressure_unit, @@ -679,18 +703,14 @@ class WeatherEntity(Entity): ATTR_FORECAST_NATIVE_WIND_SPEED, forecast_entry.get(ATTR_FORECAST_WIND_SPEED), ): - if ( - native_wind_speed_unit := self.native_wind_speed_unit - ) is not None: - from_wind_speed_unit = native_wind_speed_unit - to_wind_speed_unit = self.wind_speed_unit - else: - from_wind_speed_unit = self.wind_speed_unit - to_wind_speed_unit = self.hass.config.units.wind_speed_unit - with suppress(ValueError): + from_wind_speed_unit = ( + self.native_wind_speed_unit or self._wind_speed_unit + ) + to_wind_speed_unit = self._wind_speed_unit + with suppress(TypeError, ValueError): forecast_wind_speed_f = float(forecast_wind_speed) forecast_entry[ATTR_FORECAST_WIND_SPEED] = round( - UNIT_CONVERSIONS[ATTR_WIND_SPEED_UOM]( + UNIT_CONVERSIONS[ATTR_WEATHER_WIND_SPEED_UNIT]( forecast_wind_speed_f, from_wind_speed_unit, to_wind_speed_unit, @@ -698,23 +718,19 @@ class WeatherEntity(Entity): ROUNDING_PRECISION, ) - from_precipitation_unit = self.precipitation_unit - to_precipitation_unit = ( - self.hass.config.units.accumulated_precipitation_unit - ) if forecast_precipitation := forecast_entry.get( ATTR_FORECAST_NATIVE_PRECIPITATION, forecast_entry.get(ATTR_FORECAST_PRECIPITATION), ): - if ( - native_precipitation_unit := self.native_precipitation_unit - ) is not None: - from_precipitation_unit = native_precipitation_unit - to_precipitation_unit = self.precipitation_unit - with suppress(ValueError): + from_precipitation_unit = ( + self.native_precipitation_unit or self._precipitation_unit + ) + to_precipitation_unit = self._precipitation_unit + data[ATTR_WEATHER_PRECIPITATION_UNIT] = to_precipitation_unit + with suppress(TypeError, ValueError): forecast_precipitation_f = float(forecast_precipitation) forecast_entry[ATTR_FORECAST_PRECIPITATION] = round( - UNIT_CONVERSIONS[ATTR_PRECIPITATION_UOM]( + UNIT_CONVERSIONS[ATTR_WEATHER_PRECIPITATION_UNIT]( forecast_precipitation_f, from_precipitation_unit, to_precipitation_unit, @@ -723,8 +739,6 @@ class WeatherEntity(Entity): ) forecast.append(forecast_entry) - if forecast_precipitation is not None: - data[ATTR_WEATHER_PRECIPITATION_UNIT] = to_precipitation_unit data[ATTR_FORECAST] = forecast @@ -753,37 +767,58 @@ class WeatherEntity(Entity): self._weather_option_visibility_uom = None if weather_options := self.registry_entry.options.get(DOMAIN): if ( - (custom_unit_temperature := weather_options.get(ATTR_TEMPERATURE_UOM)) - and custom_unit_temperature in VALID_UNITS[ATTR_TEMPERATURE_UOM] - and self.native_temperature_unit in VALID_UNITS[ATTR_TEMPERATURE_UOM] + ( + custom_unit_temperature := weather_options.get( + ATTR_WEATHER_TEMPERATURE_UNIT + ) + ) + and custom_unit_temperature + in VALID_UNITS[ATTR_WEATHER_TEMPERATURE_UNIT] + and self.native_temperature_unit + in VALID_UNITS[ATTR_WEATHER_TEMPERATURE_UNIT] ): self._weather_option_temperature_uom = custom_unit_temperature if ( - (custom_unit_pressure := weather_options.get(ATTR_PRESSURE_UOM)) - and custom_unit_pressure in VALID_UNITS[ATTR_PRESSURE_UOM] - and self.native_pressure_unit in VALID_UNITS[ATTR_PRESSURE_UOM] + ( + custom_unit_pressure := weather_options.get( + ATTR_WEATHER_PRESSURE_UNIT + ) + ) + and custom_unit_pressure in VALID_UNITS[ATTR_WEATHER_PRESSURE_UNIT] + and self.native_pressure_unit in VALID_UNITS[ATTR_WEATHER_PRESSURE_UNIT] ): self._weather_option_pressure_uom = custom_unit_pressure if ( ( custom_unit_precipitation := weather_options.get( - ATTR_PRECIPITATION_UOM + ATTR_WEATHER_PRECIPITATION_UNIT ) ) - and custom_unit_precipitation in VALID_UNITS[ATTR_PRECIPITATION_UOM] + and custom_unit_precipitation + in VALID_UNITS[ATTR_WEATHER_PRECIPITATION_UNIT] and self.native_precipitation_unit - in VALID_UNITS[ATTR_PRECIPITATION_UOM] + in VALID_UNITS[ATTR_WEATHER_PRECIPITATION_UNIT] ): self._weather_option_precipitation_uom = custom_unit_precipitation if ( - (custom_unit_wind_speed := weather_options.get(ATTR_WIND_SPEED_UOM)) - and custom_unit_wind_speed in VALID_UNITS[ATTR_WIND_SPEED_UOM] - and self.native_wind_speed_unit in VALID_UNITS[ATTR_WIND_SPEED_UOM] + ( + custom_unit_wind_speed := weather_options.get( + ATTR_WEATHER_WIND_SPEED_UNIT + ) + ) + and custom_unit_wind_speed in VALID_UNITS[ATTR_WEATHER_WIND_SPEED_UNIT] + and self.native_wind_speed_unit + in VALID_UNITS[ATTR_WEATHER_WIND_SPEED_UNIT] ): self._weather_option_wind_speed_uom = custom_unit_wind_speed if ( - (custom_unit_visibility := weather_options.get(ATTR_VISIBILITY_UOM)) - and custom_unit_visibility in VALID_UNITS[ATTR_VISIBILITY_UOM] - and self.native_visibility_unit in VALID_UNITS[ATTR_VISIBILITY_UOM] + ( + custom_unit_visibility := weather_options.get( + ATTR_WEATHER_VISIBILITY_UNIT + ) + ) + and custom_unit_visibility in VALID_UNITS[ATTR_WEATHER_VISIBILITY_UNIT] + and self.native_visibility_unit + in VALID_UNITS[ATTR_WEATHER_VISIBILITY_UNIT] ): self._weather_option_visibility_uom = custom_unit_visibility From 95624b9974a0a39dba8f107664a911a28a3ee736 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 21 Jun 2022 11:56:31 +0200 Subject: [PATCH 19/27] Adjust weather tests --- tests/components/weather/test_init.py | 329 ++++++++++++++++++++++---- 1 file changed, 278 insertions(+), 51 deletions(-) diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index d369218181b..be687e3c6a9 100644 --- a/tests/components/weather/test_init.py +++ b/tests/components/weather/test_init.py @@ -41,7 +41,7 @@ from homeassistant.const import ( PRESSURE_HPA, PRESSURE_INHG, PRESSURE_PA, - PRESSURE_PSI, + SPEED_KILOMETERS_PER_HOUR, SPEED_METERS_PER_SECOND, SPEED_MILES_PER_HOUR, TEMP_CELSIUS, @@ -140,11 +140,22 @@ async def create_entity(hass: HomeAssistant, **kwargs): return entity0 -@pytest.mark.parametrize("unit", [TEMP_FAHRENHEIT, TEMP_CELSIUS]) -async def test_temperature(hass: HomeAssistant, enable_custom_integrations, unit: str): +@pytest.mark.parametrize("native_unit", (TEMP_FAHRENHEIT, TEMP_CELSIUS)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ((TEMP_CELSIUS, METRIC_SYSTEM), (TEMP_FAHRENHEIT, IMPERIAL_SYSTEM)), +) +async def test_temperature( + hass: HomeAssistant, + enable_custom_integrations, + native_unit: str, + state_unit: str, + unit_system, +): """Test temperature.""" + hass.config.units = unit_system native_value = 38 - native_unit = unit + state_value = convert_temperature(native_value, native_unit, state_unit) entity0 = await create_entity( hass, native_temperature=native_value, native_temperature_unit=native_unit @@ -153,19 +164,64 @@ async def test_temperature(hass: HomeAssistant, enable_custom_integrations, unit state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - expected = native_value + expected = state_value assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( expected, rel=0.1 ) + assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == state_unit assert float(forecast[ATTR_FORECAST_TEMP]) == approx(expected, rel=0.1) assert float(forecast[ATTR_FORECAST_TEMP_LOW]) == approx(expected, rel=0.1) -@pytest.mark.parametrize("unit", [PRESSURE_INHG, PRESSURE_HPA]) -async def test_pressure(hass: HomeAssistant, enable_custom_integrations, unit: str): +@pytest.mark.parametrize("native_unit", (None,)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ((TEMP_CELSIUS, METRIC_SYSTEM), (TEMP_FAHRENHEIT, IMPERIAL_SYSTEM)), +) +async def test_temperature_no_unit( + hass: HomeAssistant, + enable_custom_integrations, + native_unit: str, + state_unit: str, + unit_system, +): + """Test temperature when the entity does not declare a native unit.""" + hass.config.units = unit_system + native_value = 38 + state_value = native_value + + entity0 = await create_entity( + hass, native_temperature=native_value, native_temperature_unit=native_unit + ) + + state = hass.states.get(entity0.entity_id) + forecast = state.attributes[ATTR_FORECAST][0] + + expected = state_value + assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( + expected, rel=0.1 + ) + assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == state_unit + assert float(forecast[ATTR_FORECAST_TEMP]) == approx(expected, rel=0.1) + assert float(forecast[ATTR_FORECAST_TEMP_LOW]) == approx(expected, rel=0.1) + + +@pytest.mark.parametrize("native_unit", (PRESSURE_INHG, PRESSURE_INHG)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ((PRESSURE_HPA, METRIC_SYSTEM), (PRESSURE_INHG, IMPERIAL_SYSTEM)), +) +async def test_pressure( + hass: HomeAssistant, + enable_custom_integrations, + native_unit: str, + state_unit: str, + unit_system, +): """Test pressure.""" + hass.config.units = unit_system native_value = 30 - native_unit = unit + state_value = convert_pressure(native_value, native_unit, state_unit) entity0 = await create_entity( hass, native_pressure=native_value, native_pressure_unit=native_unit @@ -173,16 +229,61 @@ async def test_pressure(hass: HomeAssistant, enable_custom_integrations, unit: s state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - expected = native_value + expected = state_value assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == approx(expected, rel=1e-2) assert float(forecast[ATTR_FORECAST_PRESSURE]) == approx(expected, rel=1e-2) -@pytest.mark.parametrize("unit", [SPEED_MILES_PER_HOUR, SPEED_METERS_PER_SECOND]) -async def test_wind_speed(hass: HomeAssistant, enable_custom_integrations, unit: str): +@pytest.mark.parametrize("native_unit", (None,)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ((PRESSURE_HPA, METRIC_SYSTEM), (PRESSURE_INHG, IMPERIAL_SYSTEM)), +) +async def test_pressure_no_unit( + hass: HomeAssistant, + enable_custom_integrations, + native_unit: str, + state_unit: str, + unit_system, +): + """Test pressure when the entity does not declare a native unit.""" + hass.config.units = unit_system + native_value = 30 + state_value = native_value + + entity0 = await create_entity( + hass, native_pressure=native_value, native_pressure_unit=native_unit + ) + state = hass.states.get(entity0.entity_id) + forecast = state.attributes[ATTR_FORECAST][0] + + expected = state_value + assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == approx(expected, rel=1e-2) + assert float(forecast[ATTR_FORECAST_PRESSURE]) == approx(expected, rel=1e-2) + + +@pytest.mark.parametrize( + "native_unit", + (SPEED_MILES_PER_HOUR, SPEED_KILOMETERS_PER_HOUR, SPEED_METERS_PER_SECOND), +) +@pytest.mark.parametrize( + "state_unit, unit_system", + ( + (SPEED_KILOMETERS_PER_HOUR, METRIC_SYSTEM), + (SPEED_MILES_PER_HOUR, IMPERIAL_SYSTEM), + ), +) +async def test_wind_speed( + hass: HomeAssistant, + enable_custom_integrations, + native_unit: str, + state_unit: str, + unit_system, +): """Test wind speed.""" + hass.config.units = unit_system native_value = 10 - native_unit = unit + state_value = convert_speed(native_value, native_unit, state_unit) entity0 = await create_entity( hass, native_wind_speed=native_value, native_wind_speed_unit=native_unit @@ -191,37 +292,128 @@ async def test_wind_speed(hass: HomeAssistant, enable_custom_integrations, unit: state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - expected = native_value + expected = state_value assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx( expected, rel=1e-2 ) assert float(forecast[ATTR_FORECAST_WIND_SPEED]) == approx(expected, rel=1e-2) -@pytest.mark.parametrize("unit", [LENGTH_KILOMETERS, LENGTH_MILES]) -async def test_visibility(hass: HomeAssistant, enable_custom_integrations, unit: str): - """Test visibility.""" +@pytest.mark.parametrize("native_unit", (None,)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ( + (SPEED_KILOMETERS_PER_HOUR, METRIC_SYSTEM), + (SPEED_MILES_PER_HOUR, IMPERIAL_SYSTEM), + ), +) +async def test_wind_speed_no_unit( + hass: HomeAssistant, + enable_custom_integrations, + native_unit: str, + state_unit: str, + unit_system, +): + """Test wind speed when the entity does not declare a native unit.""" + hass.config.units = unit_system native_value = 10 - native_unit = unit + state_value = native_value + + entity0 = await create_entity( + hass, native_wind_speed=native_value, native_wind_speed_unit=native_unit + ) + + state = hass.states.get(entity0.entity_id) + forecast = state.attributes[ATTR_FORECAST][0] + + expected = state_value + assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx( + expected, rel=1e-2 + ) + assert float(forecast[ATTR_FORECAST_WIND_SPEED]) == approx(expected, rel=1e-2) + + +@pytest.mark.parametrize("native_unit", (LENGTH_MILES, LENGTH_KILOMETERS)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ( + (LENGTH_KILOMETERS, METRIC_SYSTEM), + (LENGTH_MILES, IMPERIAL_SYSTEM), + ), +) +async def test_visibility( + hass: HomeAssistant, + enable_custom_integrations, + native_unit: str, + state_unit: str, + unit_system, +): + """Test visibility.""" + hass.config.units = unit_system + native_value = 10 + state_value = convert_distance(native_value, native_unit, state_unit) entity0 = await create_entity( hass, native_visibility=native_value, native_visibility_unit=native_unit ) state = hass.states.get(entity0.entity_id) - expected = native_value + expected = state_value assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx( expected, rel=1e-2 ) -@pytest.mark.parametrize("unit", [LENGTH_INCHES, LENGTH_MILLIMETERS]) +@pytest.mark.parametrize("native_unit", (None,)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ( + (LENGTH_KILOMETERS, METRIC_SYSTEM), + (LENGTH_MILES, IMPERIAL_SYSTEM), + ), +) +async def test_visibility_no_unit( + hass: HomeAssistant, + enable_custom_integrations, + native_unit: str, + state_unit: str, + unit_system, +): + """Test visibility when the entity does not declare a native unit.""" + hass.config.units = unit_system + native_value = 10 + state_value = native_value + + entity0 = await create_entity( + hass, native_visibility=native_value, native_visibility_unit=native_unit + ) + + state = hass.states.get(entity0.entity_id) + expected = state_value + assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx( + expected, rel=1e-2 + ) + + +@pytest.mark.parametrize("native_unit", (LENGTH_INCHES, LENGTH_MILLIMETERS)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ( + (LENGTH_MILLIMETERS, METRIC_SYSTEM), + (LENGTH_INCHES, IMPERIAL_SYSTEM), + ), +) async def test_precipitation( - hass: HomeAssistant, enable_custom_integrations, unit: str + hass: HomeAssistant, + enable_custom_integrations, + native_unit: str, + state_unit: str, + unit_system, ): """Test precipitation.""" + hass.config.units = unit_system native_value = 30 - native_unit = unit + state_value = convert_distance(native_value, native_unit, state_unit) entity0 = await create_entity( hass, native_precipitation=native_value, native_precipitation_unit=native_unit @@ -230,7 +422,38 @@ async def test_precipitation( state = hass.states.get(entity0.entity_id) forecast = state.attributes[ATTR_FORECAST][0] - expected = native_value + expected = state_value + assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx(expected, rel=1e-2) + + +@pytest.mark.parametrize("native_unit", (None,)) +@pytest.mark.parametrize( + "state_unit, unit_system", + ( + (LENGTH_MILLIMETERS, METRIC_SYSTEM), + (LENGTH_INCHES, IMPERIAL_SYSTEM), + ), +) +async def test_precipitation_no_unit( + hass: HomeAssistant, + enable_custom_integrations, + native_unit: str, + state_unit: str, + unit_system, +): + """Test precipitation when the entity does not declare a native unit.""" + hass.config.units = unit_system + native_value = 30 + state_value = native_value + + entity0 = await create_entity( + hass, native_precipitation=native_value, native_precipitation_unit=native_unit + ) + + state = hass.states.get(entity0.entity_id) + forecast = state.attributes[ATTR_FORECAST][0] + + expected = state_value assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx(expected, rel=1e-2) @@ -288,11 +511,11 @@ async def test_custom_units(hass: HomeAssistant, enable_custom_integrations) -> precipitation_unit = LENGTH_MILLIMETERS set_options = { - "wind_speed_unit_of_measurement": SPEED_MILES_PER_HOUR, - "precipitation_unit_of_measurement": LENGTH_INCHES, - "pressure_unit_of_measurement": PRESSURE_INHG, - "temperature_unit_of_measurement": TEMP_FAHRENHEIT, - "visibility_unit_of_measurement": LENGTH_MILES, + "wind_speed_unit": SPEED_MILES_PER_HOUR, + "precipitation_unit": LENGTH_INCHES, + "pressure_unit": PRESSURE_INHG, + "temperature_unit": TEMP_FAHRENHEIT, + "visibility_unit": LENGTH_MILES, } entity_registry = er.async_get(hass) @@ -371,7 +594,7 @@ async def test_backwards_compatibility( """Test backwards compatibility.""" wind_speed_value = 5 wind_speed_unit = SPEED_METERS_PER_SECOND - pressure_value = 110 + pressure_value = 110000 pressure_unit = PRESSURE_PA temperature_value = 20 temperature_unit = TEMP_CELSIUS @@ -430,14 +653,18 @@ async def test_backwards_compatibility( state1 = hass.states.get(entity1.entity_id) forecast1 = state1.attributes[ATTR_FORECAST][0] - assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx(wind_speed_value) - assert state.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] == SPEED_METERS_PER_SECOND + assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx( + wind_speed_value * 3.6 + ) + assert state.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] == SPEED_KILOMETERS_PER_HOUR assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( temperature_value, rel=0.1 ) assert state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == TEMP_CELSIUS - assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == approx(pressure_value) - assert state.attributes[ATTR_WEATHER_PRESSURE_UNIT] == PRESSURE_PA + assert float(state.attributes[ATTR_WEATHER_PRESSURE]) == approx( + pressure_value / 100 + ) + assert state.attributes[ATTR_WEATHER_PRESSURE_UNIT] == PRESSURE_HPA assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx(visibility_value) assert state.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == LENGTH_KILOMETERS assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx( @@ -446,13 +673,13 @@ async def test_backwards_compatibility( assert state.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == LENGTH_MILLIMETERS assert float(state1.attributes[ATTR_WEATHER_WIND_SPEED]) == approx(wind_speed_value) - assert state1.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] == SPEED_METERS_PER_SECOND + assert state1.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] == SPEED_KILOMETERS_PER_HOUR assert float(state1.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( temperature_value, rel=0.1 ) assert state1.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] == TEMP_CELSIUS assert float(state1.attributes[ATTR_WEATHER_PRESSURE]) == approx(pressure_value) - assert state1.attributes[ATTR_WEATHER_PRESSURE_UNIT] == PRESSURE_PA + assert state1.attributes[ATTR_WEATHER_PRESSURE_UNIT] == PRESSURE_HPA assert float(state1.attributes[ATTR_WEATHER_VISIBILITY]) == approx(visibility_value) assert state1.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == LENGTH_KILOMETERS assert float(forecast1[ATTR_FORECAST_PRECIPITATION]) == approx( @@ -467,7 +694,7 @@ async def test_backwards_compatibility_convert_values( """Test backward compatibility for converting values.""" wind_speed_value = 5 wind_speed_unit = SPEED_METERS_PER_SECOND - pressure_value = 110 + pressure_value = 110000 pressure_unit = PRESSURE_PA temperature_value = 20 temperature_unit = TEMP_CELSIUS @@ -514,7 +741,7 @@ async def test_backwards_compatibility_convert_values( temperature_value, temperature_unit, TEMP_FAHRENHEIT ) expected_pressure = round( - convert_pressure(pressure_value, pressure_unit, PRESSURE_PSI), + convert_pressure(pressure_value, pressure_unit, PRESSURE_INHG), ROUNDING_PRECISION, ) expected_visibility = round( @@ -540,7 +767,7 @@ async def test_backwards_compatibility_convert_values( ATTR_FRIENDLY_NAME: "Test", ATTR_WEATHER_PRECIPITATION_UNIT: LENGTH_INCHES, ATTR_WEATHER_PRESSURE: approx(expected_pressure, rel=0.1), - ATTR_WEATHER_PRESSURE_UNIT: PRESSURE_PSI, + ATTR_WEATHER_PRESSURE_UNIT: PRESSURE_INHG, ATTR_WEATHER_TEMPERATURE: approx(expected_temperature, rel=0.1), ATTR_WEATHER_TEMPERATURE_UNIT: TEMP_FAHRENHEIT, ATTR_WEATHER_VISIBILITY: approx(expected_visibility, rel=0.1), @@ -567,19 +794,19 @@ async def test_attr(hass: HomeAssistant) -> None: assert weather.condition == ATTR_CONDITION_SUNNY assert weather.native_precipitation_unit == LENGTH_MILLIMETERS - assert weather.precipitation_unit == LENGTH_MILLIMETERS + assert weather._precipitation_unit == LENGTH_MILLIMETERS assert weather.native_pressure == 10 assert weather.native_pressure_unit == PRESSURE_HPA - assert weather.pressure_unit == PRESSURE_HPA + assert weather._pressure_unit == PRESSURE_HPA assert weather.native_temperature == 20 assert weather.native_temperature_unit == TEMP_CELSIUS - assert weather.temperature_unit == TEMP_CELSIUS + assert weather._temperature_unit == TEMP_CELSIUS assert weather.native_visibility == 30 assert weather.native_visibility_unit == LENGTH_KILOMETERS - assert weather.visibility_unit == LENGTH_KILOMETERS + assert weather._visibility_unit == LENGTH_KILOMETERS assert weather.native_wind_speed == 3 assert weather.native_wind_speed_unit == SPEED_METERS_PER_SECOND - assert weather.wind_speed_unit == SPEED_METERS_PER_SECOND + assert weather._wind_speed_unit == SPEED_KILOMETERS_PER_HOUR async def test_attr_compatibility(hass: HomeAssistant) -> None: @@ -589,15 +816,15 @@ async def test_attr_compatibility(hass: HomeAssistant) -> None: weather.hass = hass assert weather.condition == ATTR_CONDITION_SUNNY - assert weather.precipitation_unit == LENGTH_MILLIMETERS + assert weather._precipitation_unit == LENGTH_MILLIMETERS assert weather.pressure == 10 - assert weather.pressure_unit == PRESSURE_HPA + assert weather._pressure_unit == PRESSURE_HPA assert weather.temperature == 20 - assert weather.temperature_unit == TEMP_CELSIUS + assert weather._temperature_unit == TEMP_CELSIUS assert weather.visibility == 30 assert weather.visibility_unit == LENGTH_KILOMETERS assert weather.wind_speed == 3 - assert weather.wind_speed_unit == SPEED_METERS_PER_SECOND + assert weather._wind_speed_unit == SPEED_KILOMETERS_PER_HOUR forecast_entry = [ Forecast( @@ -611,14 +838,14 @@ async def test_attr_compatibility(hass: HomeAssistant) -> None: assert weather.state_attributes == { ATTR_FORECAST: forecast_entry, - ATTR_WEATHER_PRESSURE: 1000.0, - ATTR_WEATHER_PRESSURE_UNIT: PRESSURE_PA, + ATTR_WEATHER_PRESSURE: 10.0, + ATTR_WEATHER_PRESSURE_UNIT: PRESSURE_HPA, ATTR_WEATHER_TEMPERATURE: 20.0, ATTR_WEATHER_TEMPERATURE_UNIT: TEMP_CELSIUS, ATTR_WEATHER_VISIBILITY: 30.0, ATTR_WEATHER_VISIBILITY_UNIT: LENGTH_KILOMETERS, - ATTR_WEATHER_WIND_SPEED: 3.0, - ATTR_WEATHER_WIND_SPEED_UNIT: SPEED_METERS_PER_SECOND, + ATTR_WEATHER_WIND_SPEED: 3.0 * 3.6, + ATTR_WEATHER_WIND_SPEED_UNIT: SPEED_KILOMETERS_PER_HOUR, ATTR_WEATHER_PRECIPITATION_UNIT: LENGTH_MILLIMETERS, } @@ -631,7 +858,7 @@ async def test_precision_for_temperature(hass: HomeAssistant) -> None: assert weather.condition == ATTR_CONDITION_SUNNY assert weather.native_temperature == 20.3 - assert weather.temperature_unit == TEMP_CELSIUS + assert weather._temperature_unit == TEMP_CELSIUS assert weather.precision == PRECISION_HALVES assert weather.state_attributes[ATTR_WEATHER_TEMPERATURE] == 20.5 From dbb771a90227fc85ee43f6da74c4fb4275cf4151 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 21 Jun 2022 12:23:06 +0200 Subject: [PATCH 20/27] Correct precision attribute --- homeassistant/components/weather/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index e7c8cd4a3c0..4c678fc5c91 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -557,7 +557,7 @@ class WeatherEntity(Entity): return self._attr_precision return ( PRECISION_TENTHS - if self.temperature_unit == TEMP_CELSIUS + if self._temperature_unit == TEMP_CELSIUS else PRECISION_WHOLE ) From de705f4fd75cd4b007a77a4de212df0eba4cd276 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 21 Jun 2022 12:23:40 +0200 Subject: [PATCH 21/27] Adjust accuweather tests --- tests/components/accuweather/test_weather.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py index 02ace5d3f1d..97f588cb477 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -46,7 +46,7 @@ async def test_weather_without_forecast(hass): assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == 22.6 assert state.attributes.get(ATTR_WEATHER_VISIBILITY) == 16.1 assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 180 - assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 4.03 + assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 14.5 # 4.03 m/s -> km/h assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION entry = registry.async_get("weather.home") @@ -68,7 +68,7 @@ async def test_weather_with_forecast(hass): assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == 22.6 assert state.attributes.get(ATTR_WEATHER_VISIBILITY) == 16.1 assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 180 - assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 4.03 + assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 14.5 # 4.03 m/s -> km/h assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION forecast = state.attributes.get(ATTR_FORECAST)[0] assert forecast.get(ATTR_FORECAST_CONDITION) == "lightning-rainy" @@ -78,7 +78,7 @@ async def test_weather_with_forecast(hass): assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 15.4 assert forecast.get(ATTR_FORECAST_TIME) == "2020-07-26T05:00:00+00:00" assert forecast.get(ATTR_FORECAST_WIND_BEARING) == 166 - assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 3.61 + assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 13.0 # 3.61 m/s -> km/h entry = registry.async_get("weather.home") assert entry From a56f60b4ce6da1e573179a37f28e66e52f8bd976 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 21 Jun 2022 12:23:59 +0200 Subject: [PATCH 22/27] Adjust aemet tests --- tests/components/aemet/test_weather.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/aemet/test_weather.py b/tests/components/aemet/test_weather.py index 809b61e0bda..ee021cc7f6d 100644 --- a/tests/components/aemet/test_weather.py +++ b/tests/components/aemet/test_weather.py @@ -42,10 +42,10 @@ async def test_aemet_weather(hass): assert state.state == ATTR_CONDITION_SNOWY assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_WEATHER_HUMIDITY) == 99.0 - assert state.attributes.get(ATTR_WEATHER_PRESSURE) == 100440.0 + assert state.attributes.get(ATTR_WEATHER_PRESSURE) == 1004.4 # 100440.0 Pa -> hPa assert state.attributes.get(ATTR_WEATHER_TEMPERATURE) == -0.7 assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 90.0 - assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 4.17 + assert state.attributes.get(ATTR_WEATHER_WIND_SPEED) == 15.0 # 4.17 m/s -> km/h forecast = state.attributes.get(ATTR_FORECAST)[0] assert forecast.get(ATTR_FORECAST_CONDITION) == ATTR_CONDITION_PARTLYCLOUDY assert forecast.get(ATTR_FORECAST_PRECIPITATION) is None @@ -57,7 +57,7 @@ async def test_aemet_weather(hass): == dt_util.parse_datetime("2021-01-10 00:00:00+00:00").isoformat() ) assert forecast.get(ATTR_FORECAST_WIND_BEARING) == 45.0 - assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 5.56 + assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 20.0 # 5.56 m/s -> km/h state = hass.states.get("weather.aemet_hourly") assert state is None From 93e0f2465344a0d7718b8c3a65f3b5f7106bafc0 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 21 Jun 2022 12:24:18 +0200 Subject: [PATCH 23/27] Adjust climacell tests --- tests/components/climacell/test_weather.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py index 3c02f6b9b1f..593caa7755f 100644 --- a/tests/components/climacell/test_weather.py +++ b/tests/components/climacell/test_weather.py @@ -132,7 +132,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-12T00:00:00-08:00", - ATTR_FORECAST_PRECIPITATION: 0.0457, + ATTR_FORECAST_PRECIPITATION: 0.05, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 25, ATTR_FORECAST_TEMP: 19.9, ATTR_FORECAST_TEMP_LOW: 12.1, @@ -148,7 +148,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, ATTR_FORECAST_TIME: "2021-03-14T00:00:00-08:00", - ATTR_FORECAST_PRECIPITATION: 1.0744, + ATTR_FORECAST_PRECIPITATION: 1.07, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 75, ATTR_FORECAST_TEMP: 6.4, ATTR_FORECAST_TEMP_LOW: 3.2, @@ -156,7 +156,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY, ATTR_FORECAST_TIME: "2021-03-15T00:00:00-07:00", # DST starts - ATTR_FORECAST_PRECIPITATION: 7.3050, + ATTR_FORECAST_PRECIPITATION: 7.3, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 95, ATTR_FORECAST_TEMP: 1.2, ATTR_FORECAST_TEMP_LOW: 0.2, @@ -164,7 +164,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-16T00:00:00-07:00", - ATTR_FORECAST_PRECIPITATION: 0.0051, + ATTR_FORECAST_PRECIPITATION: 0.01, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 5, ATTR_FORECAST_TEMP: 6.1, ATTR_FORECAST_TEMP_LOW: -1.6, @@ -188,7 +188,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-19T00:00:00-07:00", - ATTR_FORECAST_PRECIPITATION: 0.1778, + ATTR_FORECAST_PRECIPITATION: 0.18, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 45, ATTR_FORECAST_TEMP: 9.4, ATTR_FORECAST_TEMP_LOW: 4.7, @@ -196,7 +196,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, ATTR_FORECAST_TIME: "2021-03-20T00:00:00-07:00", - ATTR_FORECAST_PRECIPITATION: 1.2319, + ATTR_FORECAST_PRECIPITATION: 1.23, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 55, ATTR_FORECAST_TEMP: 5.0, ATTR_FORECAST_TEMP_LOW: 3.1, @@ -204,7 +204,7 @@ async def test_v3_weather( { ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_TIME: "2021-03-21T00:00:00-07:00", - ATTR_FORECAST_PRECIPITATION: 0.0432, + ATTR_FORECAST_PRECIPITATION: 0.04, ATTR_FORECAST_PRECIPITATION_PROBABILITY: 20, ATTR_FORECAST_TEMP: 6.8, ATTR_FORECAST_TEMP_LOW: 0.9, @@ -213,11 +213,11 @@ async def test_v3_weather( assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "ClimaCell - Daily" assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 24 assert weather_state.attributes[ATTR_WEATHER_OZONE] == 52.625 - assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1028.1246 + assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 1028.12 assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 6.6 - assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.9940 + assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 9.99 assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 320.31 - assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.6289 + assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 14.63 assert weather_state.attributes[ATTR_CLOUD_COVER] == 100 assert weather_state.attributes[ATTR_WIND_GUST] == 24.0758 assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain" From 54142fe29eb495874a2f8b2265cec9b6f15577f3 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 21 Jun 2022 12:24:57 +0200 Subject: [PATCH 24/27] Adjust demo tests --- tests/components/demo/test_weather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/demo/test_weather.py b/tests/components/demo/test_weather.py index 0ec6a118334..8c93219f8e6 100644 --- a/tests/components/demo/test_weather.py +++ b/tests/components/demo/test_weather.py @@ -36,7 +36,7 @@ async def test_attributes(hass): assert data.get(ATTR_WEATHER_TEMPERATURE) == 21.6 assert data.get(ATTR_WEATHER_HUMIDITY) == 92 assert data.get(ATTR_WEATHER_PRESSURE) == 1099 - assert data.get(ATTR_WEATHER_WIND_SPEED) == 0.5 + assert data.get(ATTR_WEATHER_WIND_SPEED) == 1.8 # 0.5 m/s -> km/h assert data.get(ATTR_WEATHER_WIND_BEARING) is None assert data.get(ATTR_WEATHER_OZONE) is None assert data.get(ATTR_ATTRIBUTION) == "Powered by Home Assistant" From 65f8b3b52c797975ce670de81eebcd6b6f3e7deb Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 21 Jun 2022 12:25:11 +0200 Subject: [PATCH 25/27] Adjust ipma tests --- tests/components/ipma/test_weather.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index 7ed1c4d3723..e6469043474 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -198,7 +198,7 @@ async def test_daily_forecast(hass): assert forecast.get(ATTR_FORECAST_TEMP) == 16.2 assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 10.6 assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == "100.0" - assert forecast.get(ATTR_FORECAST_WIND_SPEED) == "10" + assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 10.0 assert forecast.get(ATTR_FORECAST_WIND_BEARING) == "S" @@ -222,5 +222,5 @@ async def test_hourly_forecast(hass): assert forecast.get(ATTR_FORECAST_CONDITION) == "rainy" assert forecast.get(ATTR_FORECAST_TEMP) == 7.7 assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 80.0 - assert forecast.get(ATTR_FORECAST_WIND_SPEED) == "32.7" + assert forecast.get(ATTR_FORECAST_WIND_SPEED) == 32.7 assert forecast.get(ATTR_FORECAST_WIND_BEARING) == "S" From 843b6665bb8f2b4dd7e730d96114da0dba9dc654 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 21 Jun 2022 12:25:23 +0200 Subject: [PATCH 26/27] Adjust knx tests --- tests/components/knx/test_weather.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/knx/test_weather.py b/tests/components/knx/test_weather.py index 21d80248b97..c4a7c5de7a4 100644 --- a/tests/components/knx/test_weather.py +++ b/tests/components/knx/test_weather.py @@ -85,8 +85,8 @@ async def test_weather(hass: HomeAssistant, knx: KNXTestKit): state = hass.states.get("weather.test") assert state.attributes["temperature"] == 0.4 assert state.attributes["wind_bearing"] == 270 - assert state.attributes["wind_speed"] == 1.4400000000000002 - assert state.attributes["pressure"] == 980.5824 + assert state.attributes["wind_speed"] == 1.44 + assert state.attributes["pressure"] == 980.58 assert state.state is ATTR_CONDITION_SUNNY # update from KNX - set rain alarm From cb245b75aeb6c845743bc59ea39c8fc019a4c6c7 Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 21 Jun 2022 12:26:14 +0200 Subject: [PATCH 27/27] Adjust tomorrowio tests --- tests/components/tomorrowio/test_weather.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/components/tomorrowio/test_weather.py b/tests/components/tomorrowio/test_weather.py index f9c7e00b7cd..52c29161452 100644 --- a/tests/components/tomorrowio/test_weather.py +++ b/tests/components/tomorrowio/test_weather.py @@ -99,13 +99,13 @@ async def test_v4_weather(hass: HomeAssistant) -> None: ATTR_FORECAST_TEMP: 45.9, ATTR_FORECAST_TEMP_LOW: 26.1, ATTR_FORECAST_WIND_BEARING: 239.6, - ATTR_FORECAST_WIND_SPEED: 9.49, + ATTR_FORECAST_WIND_SPEED: 34.16, # 9.49 m/s -> km/h } assert weather_state.attributes[ATTR_FRIENDLY_NAME] == "Tomorrow.io - Daily" assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23 assert weather_state.attributes[ATTR_WEATHER_OZONE] == 46.53 - assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 3035.0 + assert weather_state.attributes[ATTR_WEATHER_PRESSURE] == 30.35 assert weather_state.attributes[ATTR_WEATHER_TEMPERATURE] == 44.1 assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 8.15 assert weather_state.attributes[ATTR_WEATHER_WIND_BEARING] == 315.14 - assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 9.33 + assert weather_state.attributes[ATTR_WEATHER_WIND_SPEED] == 33.59 # 9.33 m/s ->km/h