From 90e1fb6ce2faadb9a35fdbe1774fce7b4456364f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 23 Jun 2022 10:48:30 +0200 Subject: [PATCH] Weather unit conversion (#73441) Co-authored-by: Erik --- homeassistant/components/demo/weather.py | 51 +- homeassistant/components/weather/__init__.py | 711 ++++++++++++++-- tests/components/accuweather/test_weather.py | 6 +- tests/components/aemet/test_weather.py | 6 +- tests/components/climacell/test_weather.py | 20 +- tests/components/demo/test_weather.py | 19 +- tests/components/ipma/test_weather.py | 4 +- tests/components/knx/test_weather.py | 4 +- tests/components/tomorrowio/test_weather.py | 6 +- tests/components/weather/test_init.py | 797 ++++++++++++++++-- .../custom_components/test/weather.py | 101 ++- 11 files changed, 1531 insertions(+), 194 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 2e0f8912867..3e72c7ad931 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -1,22 +1,47 @@ """Weather component that handles meteorological data for your location.""" from __future__ import annotations +from collections.abc import Callable +from contextlib import suppress from dataclasses import dataclass from datetime import timedelta +import inspect 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 PRECISION_TENTHS, PRECISION_WHOLE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant +from homeassistant.const import ( + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_MILES, + LENGTH_MILLIMETERS, + PRECISION_HALVES, + 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, ) 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 @@ -40,21 +65,31 @@ 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" 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" DOMAIN = "weather" @@ -64,18 +99,83 @@ 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_WIND_SPEED: tuple[str, ...] = ( + SPEED_METERS_PER_SECOND, + SPEED_KILOMETERS_PER_HOUR, + SPEED_MILES_PER_HOUR, +) + +UNIT_CONVERSIONS: dict[str, Callable[[float, str, str], float]] = { + 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_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, +} + + +def round_temperature(temperature: float | None, precision: float) -> float | None: + """Convert temperature into preferred precision for display.""" + 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.""" + """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 @@ -114,38 +214,219 @@ class WeatherEntity(Entity): _attr_humidity: float | None = None _attr_ozone: float | None = None _attr_precision: float - _attr_pressure: float | None = None - _attr_pressure_unit: str | None = None + _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_unit: str - _attr_temperature: float | None - _attr_visibility: float | None = None - _attr_visibility_unit: str | None = None - _attr_precipitation_unit: str | None = None + _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 - _attr_wind_speed_unit: str | None = None + _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 + _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_unit: str | None = None + _attr_native_wind_speed: float | None = None + _attr_native_wind_speed_unit: str | None = None + + _weather_option_temperature_unit: str | None = None + _weather_option_pressure_unit: str | None = None + _weather_option_visibility_unit: str | None = None + _weather_option_precipitation_unit: str | None = None + _weather_option_wind_speed_unit: str | None = None + + def __init_subclass__(cls, **kwargs: Any) -> None: + """Post initialisation processing.""" + super().__init_subclass__(**kwargs) + _reported = False + 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 _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.""" + 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).""" + """Return the temperature for backward compatibility. + + Should not be set by integrations. + """ return self._attr_temperature @property - def temperature_unit(self) -> str: + 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 | None: + """Return the temperature unit for backward compatibility. + + Should not be set by integrations. + """ return self._attr_temperature_unit + @final + @property + def _default_temperature_unit(self) -> str: + """Return the default unit of measurement for temperature. + + Should not be set by integrations. + """ + return self.hass.config.units.temperature_unit + + @final + @property + def _temperature_unit(self) -> str: + """Return the converted unit of measurement for temperature. + + Should not be set by integrations. + """ + if ( + weather_option_temperature_unit := self._weather_option_temperature_unit + ) is not None: + return weather_option_temperature_unit + + return self._default_temperature_unit + @property def pressure(self) -> float | None: - """Return the pressure in native units.""" + """Return the pressure for backward compatibility. + + Should not be set by integrations. + """ return self._attr_pressure @property - def pressure_unit(self) -> str | None: + 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 | None: + """Return the pressure unit for backward compatibility. + + Should not be set by integrations. + """ return self._attr_pressure_unit + @final + @property + def _default_pressure_unit(self) -> str: + """Return the default unit of measurement for pressure. + + Should not be set by integrations. + """ + return PRESSURE_HPA if self.hass.config.units.is_metric else PRESSURE_INHG + + @final + @property + def _pressure_unit(self) -> str: + """Return the converted unit of measurement for pressure. + + Should not be set by integrations. + """ + if ( + weather_option_pressure_unit := self._weather_option_pressure_unit + ) is not None: + return weather_option_pressure_unit + + return self._default_pressure_unit + @property def humidity(self) -> float | None: """Return the humidity in native units.""" @@ -153,14 +434,63 @@ class WeatherEntity(Entity): @property def wind_speed(self) -> float | None: - """Return the wind speed in native units.""" + """Return the wind_speed for backward compatibility. + + Should not be set by integrations. + """ return self._attr_wind_speed @property - def wind_speed_unit(self) -> str | None: + 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 | None: + """Return the wind_speed unit for backward compatibility. + + Should not be set by integrations. + """ return self._attr_wind_speed_unit + @final + @property + def _default_wind_speed_unit(self) -> str: + """Return the default unit of measurement for wind speed. + + Should not be set by integrations. + """ + return ( + SPEED_KILOMETERS_PER_HOUR + if self.hass.config.units.is_metric + else SPEED_MILES_PER_HOUR + ) + + @final + @property + def _wind_speed_unit(self) -> str: + """Return the converted unit of measurement for wind speed. + + Should not be set by integrations. + """ + if ( + weather_option_wind_speed_unit := self._weather_option_wind_speed_unit + ) is not None: + return weather_option_wind_speed_unit + + return self._default_wind_speed_unit + @property def wind_bearing(self) -> float | str | None: """Return the wind bearing.""" @@ -173,24 +503,103 @@ class WeatherEntity(Entity): @property def visibility(self) -> float | None: - """Return the visibility in native units.""" + """Return the visibility for backward compatibility. + + Should not be set by integrations. + """ return self._attr_visibility @property - def visibility_unit(self) -> str | None: + 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 | None: + """Return the visibility unit for backward compatibility. + + Should not be set by integrations. + """ return self._attr_visibility_unit + @final + @property + def _default_visibility_unit(self) -> str: + """Return the default unit of measurement for visibility. + + Should not be set by integrations. + """ + return self.hass.config.units.length_unit + + @final + @property + def _visibility_unit(self) -> str: + """Return the converted unit of measurement for visibility. + + Should not be set by integrations. + """ + if ( + weather_option_visibility_unit := self._weather_option_visibility_unit + ) is not None: + return weather_option_visibility_unit + + return self._default_visibility_unit + @property def forecast(self) -> list[Forecast] | None: """Return the forecast in native units.""" 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.""" + if (precipitation_unit := self.precipitation_unit) is not None: + return precipitation_unit + + return self._attr_native_precipitation_unit + + @property + def precipitation_unit(self) -> str | None: + """Return the precipitation unit for backward compatibility. + + Should not be set by integrations. + """ return self._attr_precipitation_unit + @final + @property + def _default_precipitation_unit(self) -> str: + """Return the default unit of measurement for precipitation. + + Should not be set by integrations. + """ + return self.hass.config.units.accumulated_precipitation_unit + + @final + @property + def _precipitation_unit(self) -> str: + """Return the converted unit of measurement for precipitation. + + Should not be set by integrations. + """ + if ( + weather_option_precipitation_unit := self._weather_option_precipitation_unit + ) is not None: + return weather_option_precipitation_unit + + return self._default_precipitation_unit + @property def precision(self) -> float: """Return the precision of the temperature value, after unit conversion.""" @@ -198,7 +607,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 ) @@ -207,13 +616,24 @@ 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 = self.precision + + if (temperature := self.native_temperature) is not None: + from_unit = self.native_temperature_unit or self._default_temperature_unit + to_unit = self._temperature_unit + try: + temperature_f = float(temperature) + value_temp = UNIT_CONVERSIONS[ATTR_WEATHER_TEMPERATURE_UNIT]( + temperature_f, from_unit, to_unit + ) + data[ATTR_WEATHER_TEMPERATURE] = round_temperature( + value_temp, precision + ) + except (TypeError, ValueError): + data[ATTR_WEATHER_TEMPERATURE] = temperature + + data[ATTR_WEATHER_TEMPERATURE_UNIT] = self._temperature_unit if (humidity := self.humidity) is not None: data[ATTR_WEATHER_HUMIDITY] = round(humidity) @@ -221,77 +641,159 @@ class WeatherEntity(Entity): if (ozone := self.ozone) is not None: 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 + if (pressure := self.native_pressure) is not None: + from_unit = self.native_pressure_unit or self._default_pressure_unit + to_unit = self._pressure_unit + try: + pressure_f = float(pressure) + value_pressure = UNIT_CONVERSIONS[ATTR_WEATHER_PRESSURE_UNIT]( + pressure_f, from_unit, to_unit ) - data[ATTR_WEATHER_PRESSURE] = pressure + data[ATTR_WEATHER_PRESSURE] = round(value_pressure, ROUNDING_PRECISION) + except (TypeError, ValueError): + data[ATTR_WEATHER_PRESSURE] = pressure + + 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, + if (wind_speed := self.native_wind_speed) is not None: + from_unit = self.native_wind_speed_unit or self._default_wind_speed_unit + to_unit = self._wind_speed_unit + try: + wind_speed_f = float(wind_speed) + value_wind_speed = UNIT_CONVERSIONS[ATTR_WEATHER_WIND_SPEED_UNIT]( + wind_speed_f, from_unit, to_unit ) - data[ATTR_WEATHER_WIND_SPEED] = wind_speed + data[ATTR_WEATHER_WIND_SPEED] = round( + value_wind_speed, ROUNDING_PRECISION + ) + except (TypeError, ValueError): + data[ATTR_WEATHER_WIND_SPEED] = wind_speed - 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 + data[ATTR_WEATHER_WIND_SPEED_UNIT] = self._wind_speed_unit + + if (visibility := self.native_visibility) is not None: + from_unit = self.native_visibility_unit or self._default_visibility_unit + to_unit = self._visibility_unit + try: + visibility_f = float(visibility) + value_visibility = UNIT_CONVERSIONS[ATTR_WEATHER_VISIBILITY_UNIT]( + visibility_f, from_unit, to_unit ) - data[ATTR_WEATHER_VISIBILITY] = visibility + data[ATTR_WEATHER_VISIBILITY] = round( + value_visibility, ROUNDING_PRECISION + ) + except (TypeError, ValueError): + data[ATTR_WEATHER_VISIBILITY] = visibility + + data[ATTR_WEATHER_VISIBILITY_UNIT] = self._visibility_unit + data[ATTR_WEATHER_PRECIPITATION_UNIT] = self._precipitation_unit if self.forecast is not None: forecast = [] for forecast_entry in self.forecast: forecast_entry = dict(forecast_entry) - forecast_entry[ATTR_FORECAST_TEMP] = show_temp( - self.hass, - forecast_entry[ATTR_FORECAST_TEMP], - self.temperature_unit, - self.precision, + + temperature = forecast_entry.pop( + ATTR_FORECAST_NATIVE_TEMP, forecast_entry.get(ATTR_FORECAST_TEMP) ) - if ATTR_FORECAST_TEMP_LOW in forecast_entry: - forecast_entry[ATTR_FORECAST_TEMP_LOW] = show_temp( - self.hass, - forecast_entry[ATTR_FORECAST_TEMP_LOW], - self.temperature_unit, - self.precision, + + from_temp_unit = ( + self.native_temperature_unit or self._default_temperature_unit + ) + to_temp_unit = self._temperature_unit + + 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.pop( + ATTR_FORECAST_NATIVE_TEMP_LOW, + forecast_entry.get(ATTR_FORECAST_TEMP_LOW), + ): + with suppress(TypeError, ValueError): + forecast_temp_low_f = float(forecast_temp_low) + value_temp_low = UNIT_CONVERSIONS[ + ATTR_WEATHER_TEMPERATURE_UNIT + ]( + forecast_temp_low_f, + from_temp_unit, + to_temp_unit, + ) + + forecast_entry[ATTR_FORECAST_TEMP_LOW] = round_temperature( + value_temp_low, precision + ) + + if forecast_pressure := forecast_entry.pop( + ATTR_FORECAST_NATIVE_PRESSURE, + forecast_entry.get(ATTR_FORECAST_PRESSURE), + ): + from_pressure_unit = ( + self.native_pressure_unit or self._default_pressure_unit ) - 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 + 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_WEATHER_PRESSURE_UNIT]( + forecast_pressure_f, + from_pressure_unit, + to_pressure_unit, + ), + ROUNDING_PRECISION, + ) + + if forecast_wind_speed := forecast_entry.pop( + ATTR_FORECAST_NATIVE_WIND_SPEED, + forecast_entry.get(ATTR_FORECAST_WIND_SPEED), + ): + from_wind_speed_unit = ( + self.native_wind_speed_unit or self._default_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_WEATHER_WIND_SPEED_UNIT]( + forecast_wind_speed_f, + from_wind_speed_unit, + to_wind_speed_unit, + ), + ROUNDING_PRECISION, + ) + + if forecast_precipitation := forecast_entry.pop( + ATTR_FORECAST_NATIVE_PRECIPITATION, + forecast_entry.get(ATTR_FORECAST_PRECIPITATION), + ): + from_precipitation_unit = ( + self.native_precipitation_unit + or self._default_precipitation_unit + ) + to_precipitation_unit = self._precipitation_unit + with suppress(TypeError, ValueError): + forecast_precipitation_f = float(forecast_precipitation) + forecast_entry[ATTR_FORECAST_PRECIPITATION] = round( + UNIT_CONVERSIONS[ATTR_WEATHER_PRECIPITATION_UNIT]( + forecast_precipitation_f, + from_precipitation_unit, + to_precipitation_unit, ), ROUNDING_PRECISION, ) - forecast_entry[ATTR_FORECAST_PRECIPITATION] = precipitation forecast.append(forecast_entry) @@ -309,3 +811,44 @@ 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 + self._weather_option_temperature_unit = None + self._weather_option_pressure_unit = None + self._weather_option_precipitation_unit = None + self._weather_option_wind_speed_unit = None + self._weather_option_visibility_unit = None + if weather_options := self.registry_entry.options.get(DOMAIN): + if ( + custom_unit_temperature := weather_options.get( + ATTR_WEATHER_TEMPERATURE_UNIT + ) + ) and custom_unit_temperature in VALID_UNITS[ATTR_WEATHER_TEMPERATURE_UNIT]: + self._weather_option_temperature_unit = custom_unit_temperature + if ( + custom_unit_pressure := weather_options.get(ATTR_WEATHER_PRESSURE_UNIT) + ) and custom_unit_pressure in VALID_UNITS[ATTR_WEATHER_PRESSURE_UNIT]: + self._weather_option_pressure_unit = custom_unit_pressure + if ( + custom_unit_precipitation := weather_options.get( + ATTR_WEATHER_PRECIPITATION_UNIT + ) + ) and custom_unit_precipitation in VALID_UNITS[ + ATTR_WEATHER_PRECIPITATION_UNIT + ]: + self._weather_option_precipitation_unit = custom_unit_precipitation + if ( + 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]: + self._weather_option_wind_speed_unit = custom_unit_wind_speed + if ( + custom_unit_visibility := weather_options.get( + ATTR_WEATHER_VISIBILITY_UNIT + ) + ) and custom_unit_visibility in VALID_UNITS[ATTR_WEATHER_VISIBILITY_UNIT]: + self._weather_option_visibility_unit = custom_unit_visibility 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 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 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" diff --git a/tests/components/demo/test_weather.py b/tests/components/demo/test_weather.py index db3f3441df1..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" @@ -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/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" 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 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 diff --git a/tests/components/weather/test_init.py b/tests/components/weather/test_init.py index 9849a6abe18..814d3b7857c 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,19 +11,44 @@ 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, ATTR_WEATHER_TEMPERATURE, + 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, LENGTH_MILLIMETERS, + PRECISION_HALVES, + PRECISION_TENTHS, + PRECISION_WHOLE, + PRESSURE_HPA, PRESSURE_INHG, + PRESSURE_PA, + SPEED_KILOMETERS_PER_HOUR, 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 @@ -29,11 +56,75 @@ 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 tests.testing_config.custom_components.test import weather as WeatherPlatform -async def create_entity(hass, **kwargs): + +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 = {"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( @@ -49,145 +140,741 @@ async def create_entity(hass, **kwargs): return entity0 -@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) -async def test_temperature_conversion( - hass, +@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 conversion.""" + """Test temperature.""" hass.config.units = unit_system native_value = 38 - native_unit = TEMP_FAHRENHEIT + state_value = convert_temperature(native_value, native_unit, state_unit) 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 = 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_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) -async def test_pressure_conversion( - hass, +@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 pressure conversion.""" + """Test temperature when the entity does not declare a native unit.""" hass.config.units = unit_system - native_value = 30 - native_unit = PRESSURE_INHG + native_value = 38 + state_value = native_value entity0 = await create_entity( - hass, pressure=native_value, pressure_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 = 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 + state_value = convert_pressure(native_value, native_unit, state_unit) + + 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 = convert_pressure(native_value, native_unit, unit_system.pressure_unit) + 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_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) -async def test_wind_speed_conversion( - hass, +@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 wind speed conversion.""" + """Test pressure when the entity does not declare a native unit.""" hass.config.units = unit_system - native_value = 10 - native_unit = SPEED_METERS_PER_SECOND + native_value = 30 + state_value = native_value entity0 = await create_entity( - hass, wind_speed=native_value, wind_speed_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 = 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 + 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 ) 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 = 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_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) -async def test_visibility_conversion( - hass, +@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 visibility conversion.""" + """Test wind speed when the entity does not declare a native unit.""" hass.config.units = unit_system native_value = 10 - native_unit = LENGTH_MILES + state_value = native_value entity0 = await create_entity( - hass, visibility=native_value, visibility_unit=native_unit + hass, native_wind_speed=native_value, native_wind_speed_unit=native_unit ) state = hass.states.get(entity0.entity_id) - expected = convert_distance(native_value, native_unit, unit_system.length_unit) + 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 = state_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, +@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 precipitation conversion.""" + """Test visibility when the entity does not declare a native unit.""" hass.config.units = unit_system - native_value = 30 - native_unit = LENGTH_MILLIMETERS + native_value = 10 + state_value = native_value entity0 = await create_entity( - hass, precipitation=native_value, precipitation_unit=native_unit + 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, + native_unit: str, + state_unit: str, + unit_system, +): + """Test precipitation.""" + hass.config.units = unit_system + native_value = 30 + state_value = convert_distance(native_value, native_unit, state_unit) + + 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 = convert_distance( - native_value, native_unit, unit_system.accumulated_precipitation_unit - ) + 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) + + +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, + hass: HomeAssistant, enable_custom_integrations, ): """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) 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: + """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.1 + precipitation_unit = LENGTH_MILLIMETERS + + set_options = { + "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) + + entry = entity_registry.async_get_or_create("weather", "test", "very_unique") + 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") + platform.init(empty=True) + platform.ENTITIES.append( + platform.MockWeatherMockForecast( + name="Test", + condition=ATTR_CONDITION_SUNNY, + 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_visibility=visibility_value, + native_visibility_unit=visibility_unit, + native_precipitation=precipitation_value, + native_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] + + 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_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 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_VISIBILITY]) == approx( + expected_visibility + ) + assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx( + expected_precipitation, rel=1e-2 + ) + + assert ( + state.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] + == set_options["precipitation_unit"] + ) + assert state.attributes[ATTR_WEATHER_PRESSURE_UNIT] == set_options["pressure_unit"] + assert ( + state.attributes[ATTR_WEATHER_TEMPERATURE_UNIT] + == set_options["temperature_unit"] + ) + assert ( + state.attributes[ATTR_WEATHER_VISIBILITY_UNIT] == set_options["visibility_unit"] + ) + assert ( + state.attributes[ATTR_WEATHER_WIND_SPEED_UNIT] == set_options["wind_speed_unit"] + ) + + +async def test_backwards_compatibility( + hass: HomeAssistant, enable_custom_integrations +) -> None: + """Test backwards compatibility.""" + wind_speed_value = 5 + wind_speed_unit = SPEED_METERS_PER_SECOND + pressure_value = 110000 + 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 = METRIC_SYSTEM + + 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", + ) + ) + platform.ENTITIES.append( + platform.MockWeatherMockForecastCompat( + name="Test2", + condition=ATTR_CONDITION_SUNNY, + temperature=temperature_value, + temperature_unit=temperature_unit, + wind_speed=wind_speed_value, + pressure=pressure_value, + visibility=visibility_value, + precipitation=precipitation_value, + 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 * 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 / 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( + 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 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_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( + precipitation_value, rel=1e-2 + ) + assert state1.attributes[ATTR_WEATHER_PRECIPITATION_UNIT] == LENGTH_MILLIMETERS + + +async def test_backwards_compatibility_convert_values( + hass: HomeAssistant, enable_custom_integrations +) -> None: + """Test backward compatibility for converting values.""" + wind_speed_value = 5 + wind_speed_unit = SPEED_METERS_PER_SECOND + pressure_value = 110000 + 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) + 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) + + 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_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_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), + 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: + """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 + 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_KILOMETERS_PER_HOUR + + +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_KILOMETERS_PER_HOUR + + 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: 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 * 3.6, + ATTR_WEATHER_WIND_SPEED_UNIT: SPEED_KILOMETERS_PER_HOUR, + 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 224d6495548..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, @@ -37,6 +42,80 @@ async def async_setup_platform( class MockWeather(MockEntity, WeatherEntity): """Mock weather class.""" + @property + def native_temperature(self) -> float | None: + """Return the platform temperature.""" + return self._handle("native_temperature") + + @property + def native_temperature_unit(self) -> str | None: + """Return the unit of measurement for temperature.""" + return self._handle("native_temperature_unit") + + @property + def native_pressure(self) -> float | None: + """Return the pressure.""" + return self._handle("native_pressure") + + @property + def native_pressure_unit(self) -> str | None: + """Return the unit of measurement for pressure.""" + return self._handle("native_pressure_unit") + + @property + def humidity(self) -> float | None: + """Return the humidity.""" + return self._handle("humidity") + + @property + def native_wind_speed(self) -> float | None: + """Return the wind speed.""" + return self._handle("native_wind_speed") + + @property + def native_wind_speed_unit(self) -> str | None: + """Return the unit of measurement for wind speed.""" + return self._handle("native_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 native_visibility(self) -> float | None: + """Return the visibility.""" + return self._handle("native_visibility") + + @property + def native_visibility_unit(self) -> str | None: + """Return the unit of measurement for visibility.""" + return self._handle("native_visibility_unit") + + @property + def forecast(self) -> list[Forecast] | None: + """Return the forecast.""" + return self._handle("forecast") + + @property + def native_precipitation_unit(self) -> str | None: + """Return the native unit of measurement for accumulated precipitation.""" + return self._handle("native_precipitation_unit") + + @property + def condition(self) -> str | None: + """Return the current condition.""" + 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.""" @@ -99,7 +178,7 @@ class MockWeather(MockEntity, WeatherEntity): @property def precipitation_unit(self) -> str | None: - """Return the native unit of measurement for accumulated precipitation.""" + """Return the unit of measurement for accumulated precipitation.""" return self._handle("precipitation_unit") @property @@ -111,6 +190,26 @@ class MockWeather(MockEntity, WeatherEntity): class MockWeatherMockForecast(MockWeather): """Mock weather class with mocked forecast.""" + @property + def forecast(self) -> list[Forecast] | None: + """Return the forecast.""" + return [ + { + 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_NATIVE_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."""