Weather unit conversion (#73441)

Co-authored-by: Erik <erik@montnemery.com>
This commit is contained in:
G Johansson 2022-06-23 10:48:30 +02:00 committed by GitHub
parent bccec77e19
commit 90e1fb6ce2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1531 additions and 194 deletions

View File

@ -27,7 +27,14 @@ from homeassistant.components.weather import (
WeatherEntity, WeatherEntity,
) )
from homeassistant.config_entries import ConfigEntry 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.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@ -77,6 +84,8 @@ def setup_platform(
1099, 1099,
0.5, 0.5,
TEMP_CELSIUS, TEMP_CELSIUS,
PRESSURE_HPA,
SPEED_METERS_PER_SECOND,
[ [
[ATTR_CONDITION_RAINY, 1, 22, 15, 60], [ATTR_CONDITION_RAINY, 1, 22, 15, 60],
[ATTR_CONDITION_RAINY, 5, 19, 8, 30], [ATTR_CONDITION_RAINY, 5, 19, 8, 30],
@ -95,6 +104,8 @@ def setup_platform(
987, 987,
4.8, 4.8,
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
PRESSURE_INHG,
SPEED_MILES_PER_HOUR,
[ [
[ATTR_CONDITION_SNOWY, 2, -10, -15, 60], [ATTR_CONDITION_SNOWY, 2, -10, -15, 60],
[ATTR_CONDITION_PARTLYCLOUDY, 1, -13, -14, 25], [ATTR_CONDITION_PARTLYCLOUDY, 1, -13, -14, 25],
@ -121,16 +132,20 @@ class DemoWeather(WeatherEntity):
pressure, pressure,
wind_speed, wind_speed,
temperature_unit, temperature_unit,
pressure_unit,
wind_speed_unit,
forecast, forecast,
): ):
"""Initialize the Demo weather.""" """Initialize the Demo weather."""
self._name = name self._name = name
self._condition = condition self._condition = condition
self._temperature = temperature self._native_temperature = temperature
self._temperature_unit = temperature_unit self._native_temperature_unit = temperature_unit
self._humidity = humidity self._humidity = humidity
self._pressure = pressure self._native_pressure = pressure
self._wind_speed = wind_speed self._native_pressure_unit = pressure_unit
self._native_wind_speed = wind_speed
self._native_wind_speed_unit = wind_speed_unit
self._forecast = forecast self._forecast = forecast
@property @property
@ -144,14 +159,14 @@ class DemoWeather(WeatherEntity):
return False return False
@property @property
def temperature(self): def native_temperature(self):
"""Return the temperature.""" """Return the temperature."""
return self._temperature return self._native_temperature
@property @property
def temperature_unit(self): def native_temperature_unit(self):
"""Return the unit of measurement.""" """Return the unit of measurement."""
return self._temperature_unit return self._native_temperature_unit
@property @property
def humidity(self): def humidity(self):
@ -159,14 +174,24 @@ class DemoWeather(WeatherEntity):
return self._humidity return self._humidity
@property @property
def wind_speed(self): def native_wind_speed(self):
"""Return the wind speed.""" """Return the wind speed."""
return self._wind_speed return self._native_wind_speed
@property @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 the pressure."""
return self._pressure return self._native_pressure
@property
def native_pressure_unit(self):
"""Return the pressure."""
return self._native_pressure_unit
@property @property
def condition(self): def condition(self):

View File

@ -1,22 +1,47 @@
"""Weather component that handles meteorological data for your location.""" """Weather component that handles meteorological data for your location."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from contextlib import suppress
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
import inspect
import logging import logging
from typing import Final, TypedDict, final from typing import Any, Final, TypedDict, final
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, TEMP_CELSIUS from homeassistant.const import (
from homeassistant.core import HomeAssistant 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 from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE, PLATFORM_SCHEMA_BASE,
) )
from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent 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.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 # mypy: allow-untyped-defs, no-check-untyped-defs
@ -40,21 +65,31 @@ ATTR_CONDITION_WINDY = "windy"
ATTR_CONDITION_WINDY_VARIANT = "windy-variant" ATTR_CONDITION_WINDY_VARIANT = "windy-variant"
ATTR_FORECAST = "forecast" ATTR_FORECAST = "forecast"
ATTR_FORECAST_CONDITION: Final = "condition" ATTR_FORECAST_CONDITION: Final = "condition"
ATTR_FORECAST_NATIVE_PRECIPITATION: Final = "native_precipitation"
ATTR_FORECAST_PRECIPITATION: Final = "precipitation" ATTR_FORECAST_PRECIPITATION: Final = "precipitation"
ATTR_FORECAST_PRECIPITATION_PROBABILITY: Final = "precipitation_probability" ATTR_FORECAST_PRECIPITATION_PROBABILITY: Final = "precipitation_probability"
ATTR_FORECAST_NATIVE_PRESSURE: Final = "native_pressure"
ATTR_FORECAST_PRESSURE: Final = "pressure" ATTR_FORECAST_PRESSURE: Final = "pressure"
ATTR_FORECAST_NATIVE_TEMP: Final = "native_temperature"
ATTR_FORECAST_TEMP: Final = "temperature" ATTR_FORECAST_TEMP: Final = "temperature"
ATTR_FORECAST_NATIVE_TEMP_LOW: Final = "native_templow"
ATTR_FORECAST_TEMP_LOW: Final = "templow" ATTR_FORECAST_TEMP_LOW: Final = "templow"
ATTR_FORECAST_TIME: Final = "datetime" ATTR_FORECAST_TIME: Final = "datetime"
ATTR_FORECAST_WIND_BEARING: Final = "wind_bearing" ATTR_FORECAST_WIND_BEARING: Final = "wind_bearing"
ATTR_FORECAST_NATIVE_WIND_SPEED: Final = "native_wind_speed"
ATTR_FORECAST_WIND_SPEED: Final = "wind_speed" ATTR_FORECAST_WIND_SPEED: Final = "wind_speed"
ATTR_WEATHER_HUMIDITY = "humidity" ATTR_WEATHER_HUMIDITY = "humidity"
ATTR_WEATHER_OZONE = "ozone" ATTR_WEATHER_OZONE = "ozone"
ATTR_WEATHER_PRESSURE = "pressure" ATTR_WEATHER_PRESSURE = "pressure"
ATTR_WEATHER_PRESSURE_UNIT = "pressure_unit"
ATTR_WEATHER_TEMPERATURE = "temperature" ATTR_WEATHER_TEMPERATURE = "temperature"
ATTR_WEATHER_TEMPERATURE_UNIT = "temperature_unit"
ATTR_WEATHER_VISIBILITY = "visibility" ATTR_WEATHER_VISIBILITY = "visibility"
ATTR_WEATHER_VISIBILITY_UNIT = "visibility_unit"
ATTR_WEATHER_WIND_BEARING = "wind_bearing" ATTR_WEATHER_WIND_BEARING = "wind_bearing"
ATTR_WEATHER_WIND_SPEED = "wind_speed" ATTR_WEATHER_WIND_SPEED = "wind_speed"
ATTR_WEATHER_WIND_SPEED_UNIT = "wind_speed_unit"
ATTR_WEATHER_PRECIPITATION_UNIT = "precipitation_unit"
DOMAIN = "weather" DOMAIN = "weather"
@ -64,18 +99,83 @@ SCAN_INTERVAL = timedelta(seconds=30)
ROUNDING_PRECISION = 2 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): 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 condition: str | None
datetime: str datetime: str
precipitation_probability: int | None precipitation_probability: int | None
native_precipitation: float | None
precipitation: float | None precipitation: float | None
native_pressure: float | None
pressure: float | None pressure: float | None
native_temperature: float | None
temperature: float | None temperature: float | None
native_templow: float | None
templow: float | None templow: float | None
wind_bearing: float | str | None wind_bearing: float | str | None
native_wind_speed: float | None
wind_speed: float | None wind_speed: float | None
@ -114,38 +214,219 @@ class WeatherEntity(Entity):
_attr_humidity: float | None = None _attr_humidity: float | None = None
_attr_ozone: float | None = None _attr_ozone: float | None = None
_attr_precision: float _attr_precision: float
_attr_pressure: float | None = None _attr_pressure: float | None = (
_attr_pressure_unit: str | None = 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_state: None = None
_attr_temperature_unit: str _attr_temperature: float | None = (
_attr_temperature: float | None None # Provide backwards compatibility. Use _attr_native_temperature
_attr_visibility: float | None = None )
_attr_visibility_unit: str | None = None _attr_temperature_unit: str | None = (
_attr_precipitation_unit: str | None = 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_bearing: float | str | None = None
_attr_wind_speed: float | None = None _attr_wind_speed: float | None = (
_attr_wind_speed_unit: str | None = 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 @property
def temperature(self) -> float | None: 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 return self._attr_temperature
@property @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.""" """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 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 @property
def pressure(self) -> float | None: 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 return self._attr_pressure
@property @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.""" """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 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 @property
def humidity(self) -> float | None: def humidity(self) -> float | None:
"""Return the humidity in native units.""" """Return the humidity in native units."""
@ -153,14 +434,63 @@ class WeatherEntity(Entity):
@property @property
def wind_speed(self) -> float | None: 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 return self._attr_wind_speed
@property @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.""" """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 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 @property
def wind_bearing(self) -> float | str | None: def wind_bearing(self) -> float | str | None:
"""Return the wind bearing.""" """Return the wind bearing."""
@ -173,24 +503,103 @@ class WeatherEntity(Entity):
@property @property
def visibility(self) -> float | None: 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 return self._attr_visibility
@property @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.""" """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 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 @property
def forecast(self) -> list[Forecast] | None: def forecast(self) -> list[Forecast] | None:
"""Return the forecast in native units.""" """Return the forecast in native units."""
return self._attr_forecast return self._attr_forecast
@property @property
def precipitation_unit(self) -> str | None: def native_precipitation_unit(self) -> str | None:
"""Return the native unit of measurement for accumulated precipitation.""" """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 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 @property
def precision(self) -> float: def precision(self) -> float:
"""Return the precision of the temperature value, after unit conversion.""" """Return the precision of the temperature value, after unit conversion."""
@ -198,7 +607,7 @@ class WeatherEntity(Entity):
return self._attr_precision return self._attr_precision
return ( return (
PRECISION_TENTHS PRECISION_TENTHS
if self.hass.config.units.temperature_unit == TEMP_CELSIUS if self._temperature_unit == TEMP_CELSIUS
else PRECISION_WHOLE else PRECISION_WHOLE
) )
@ -207,13 +616,24 @@ class WeatherEntity(Entity):
def state_attributes(self): def state_attributes(self):
"""Return the state attributes, converted from native units to user-configured units.""" """Return the state attributes, converted from native units to user-configured units."""
data = {} data = {}
if self.temperature is not None:
data[ATTR_WEATHER_TEMPERATURE] = show_temp( precision = self.precision
self.hass,
self.temperature, if (temperature := self.native_temperature) is not None:
self.temperature_unit, from_unit = self.native_temperature_unit or self._default_temperature_unit
self.precision, 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: if (humidity := self.humidity) is not None:
data[ATTR_WEATHER_HUMIDITY] = round(humidity) data[ATTR_WEATHER_HUMIDITY] = round(humidity)
@ -221,77 +641,159 @@ class WeatherEntity(Entity):
if (ozone := self.ozone) is not None: if (ozone := self.ozone) is not None:
data[ATTR_WEATHER_OZONE] = ozone data[ATTR_WEATHER_OZONE] = ozone
if (pressure := self.pressure) is not None: if (pressure := self.native_pressure) is not None:
if (unit := self.pressure_unit) is not None: from_unit = self.native_pressure_unit or self._default_pressure_unit
pressure = round( to_unit = self._pressure_unit
self.hass.config.units.pressure(pressure, unit), ROUNDING_PRECISION 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: if (wind_bearing := self.wind_bearing) is not None:
data[ATTR_WEATHER_WIND_BEARING] = wind_bearing 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:
if (unit := self.wind_speed_unit) is not None: from_unit = self.native_wind_speed_unit or self._default_wind_speed_unit
wind_speed = round( to_unit = self._wind_speed_unit
self.hass.config.units.wind_speed(wind_speed, unit), try:
ROUNDING_PRECISION, 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: data[ATTR_WEATHER_WIND_SPEED_UNIT] = self._wind_speed_unit
if (unit := self.visibility_unit) is not None:
visibility = round( if (visibility := self.native_visibility) is not None:
self.hass.config.units.length(visibility, unit), ROUNDING_PRECISION 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: if self.forecast is not None:
forecast = [] forecast = []
for forecast_entry in self.forecast: for forecast_entry in self.forecast:
forecast_entry = dict(forecast_entry) forecast_entry = dict(forecast_entry)
forecast_entry[ATTR_FORECAST_TEMP] = show_temp(
self.hass, temperature = forecast_entry.pop(
forecast_entry[ATTR_FORECAST_TEMP], ATTR_FORECAST_NATIVE_TEMP, forecast_entry.get(ATTR_FORECAST_TEMP)
self.temperature_unit,
self.precision,
) )
if ATTR_FORECAST_TEMP_LOW in forecast_entry:
forecast_entry[ATTR_FORECAST_TEMP_LOW] = show_temp( from_temp_unit = (
self.hass, self.native_temperature_unit or self._default_temperature_unit
forecast_entry[ATTR_FORECAST_TEMP_LOW], )
self.temperature_unit, to_temp_unit = self._temperature_unit
self.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.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 ( to_pressure_unit = self._pressure_unit
native_pressure := forecast_entry.get(ATTR_FORECAST_PRESSURE) with suppress(TypeError, ValueError):
) is not None: forecast_pressure_f = float(forecast_pressure)
if (unit := self.pressure_unit) is not None: forecast_entry[ATTR_FORECAST_PRESSURE] = round(
pressure = round( UNIT_CONVERSIONS[ATTR_WEATHER_PRESSURE_UNIT](
self.hass.config.units.pressure(native_pressure, unit), forecast_pressure_f,
ROUNDING_PRECISION, from_pressure_unit,
) to_pressure_unit,
forecast_entry[ATTR_FORECAST_PRESSURE] = pressure ),
if ( ROUNDING_PRECISION,
native_wind_speed := forecast_entry.get(ATTR_FORECAST_WIND_SPEED) )
) is not None:
if (unit := self.wind_speed_unit) is not None: if forecast_wind_speed := forecast_entry.pop(
wind_speed = round( ATTR_FORECAST_NATIVE_WIND_SPEED,
self.hass.config.units.wind_speed(native_wind_speed, unit), forecast_entry.get(ATTR_FORECAST_WIND_SPEED),
ROUNDING_PRECISION, ):
) from_wind_speed_unit = (
forecast_entry[ATTR_FORECAST_WIND_SPEED] = wind_speed self.native_wind_speed_unit or self._default_wind_speed_unit
if ( )
native_precip := forecast_entry.get(ATTR_FORECAST_PRECIPITATION) to_wind_speed_unit = self._wind_speed_unit
) is not None: with suppress(TypeError, ValueError):
if (unit := self.precipitation_unit) is not None: forecast_wind_speed_f = float(forecast_wind_speed)
precipitation = round( forecast_entry[ATTR_FORECAST_WIND_SPEED] = round(
self.hass.config.units.accumulated_precipitation( UNIT_CONVERSIONS[ATTR_WEATHER_WIND_SPEED_UNIT](
native_precip, 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, ROUNDING_PRECISION,
) )
forecast_entry[ATTR_FORECAST_PRECIPITATION] = precipitation
forecast.append(forecast_entry) forecast.append(forecast_entry)
@ -309,3 +811,44 @@ class WeatherEntity(Entity):
def condition(self) -> str | None: def condition(self) -> str | None:
"""Return the current condition.""" """Return the current condition."""
return self._attr_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

View File

@ -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_TEMPERATURE) == 22.6
assert state.attributes.get(ATTR_WEATHER_VISIBILITY) == 16.1 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_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 assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
entry = registry.async_get("weather.home") 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_TEMPERATURE) == 22.6
assert state.attributes.get(ATTR_WEATHER_VISIBILITY) == 16.1 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_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 assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
forecast = state.attributes.get(ATTR_FORECAST)[0] forecast = state.attributes.get(ATTR_FORECAST)[0]
assert forecast.get(ATTR_FORECAST_CONDITION) == "lightning-rainy" 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_TEMP_LOW) == 15.4
assert forecast.get(ATTR_FORECAST_TIME) == "2020-07-26T05:00:00+00:00" 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_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") entry = registry.async_get("weather.home")
assert entry assert entry

View File

@ -42,10 +42,10 @@ async def test_aemet_weather(hass):
assert state.state == ATTR_CONDITION_SNOWY assert state.state == ATTR_CONDITION_SNOWY
assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION
assert state.attributes.get(ATTR_WEATHER_HUMIDITY) == 99.0 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_TEMPERATURE) == -0.7
assert state.attributes.get(ATTR_WEATHER_WIND_BEARING) == 90.0 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] forecast = state.attributes.get(ATTR_FORECAST)[0]
assert forecast.get(ATTR_FORECAST_CONDITION) == ATTR_CONDITION_PARTLYCLOUDY assert forecast.get(ATTR_FORECAST_CONDITION) == ATTR_CONDITION_PARTLYCLOUDY
assert forecast.get(ATTR_FORECAST_PRECIPITATION) is None 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() == 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_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") state = hass.states.get("weather.aemet_hourly")
assert state is None assert state is None

View File

@ -132,7 +132,7 @@ async def test_v3_weather(
{ {
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-12T00:00:00-08:00", 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_PRECIPITATION_PROBABILITY: 25,
ATTR_FORECAST_TEMP: 19.9, ATTR_FORECAST_TEMP: 19.9,
ATTR_FORECAST_TEMP_LOW: 12.1, ATTR_FORECAST_TEMP_LOW: 12.1,
@ -148,7 +148,7 @@ async def test_v3_weather(
{ {
ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY,
ATTR_FORECAST_TIME: "2021-03-14T00:00:00-08:00", 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_PRECIPITATION_PROBABILITY: 75,
ATTR_FORECAST_TEMP: 6.4, ATTR_FORECAST_TEMP: 6.4,
ATTR_FORECAST_TEMP_LOW: 3.2, ATTR_FORECAST_TEMP_LOW: 3.2,
@ -156,7 +156,7 @@ async def test_v3_weather(
{ {
ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY, ATTR_FORECAST_CONDITION: ATTR_CONDITION_SNOWY,
ATTR_FORECAST_TIME: "2021-03-15T00:00:00-07:00", # DST starts 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_PRECIPITATION_PROBABILITY: 95,
ATTR_FORECAST_TEMP: 1.2, ATTR_FORECAST_TEMP: 1.2,
ATTR_FORECAST_TEMP_LOW: 0.2, ATTR_FORECAST_TEMP_LOW: 0.2,
@ -164,7 +164,7 @@ async def test_v3_weather(
{ {
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-16T00:00:00-07:00", 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_PRECIPITATION_PROBABILITY: 5,
ATTR_FORECAST_TEMP: 6.1, ATTR_FORECAST_TEMP: 6.1,
ATTR_FORECAST_TEMP_LOW: -1.6, ATTR_FORECAST_TEMP_LOW: -1.6,
@ -188,7 +188,7 @@ async def test_v3_weather(
{ {
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-19T00:00:00-07:00", 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_PRECIPITATION_PROBABILITY: 45,
ATTR_FORECAST_TEMP: 9.4, ATTR_FORECAST_TEMP: 9.4,
ATTR_FORECAST_TEMP_LOW: 4.7, ATTR_FORECAST_TEMP_LOW: 4.7,
@ -196,7 +196,7 @@ async def test_v3_weather(
{ {
ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY, ATTR_FORECAST_CONDITION: ATTR_CONDITION_RAINY,
ATTR_FORECAST_TIME: "2021-03-20T00:00:00-07:00", 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_PRECIPITATION_PROBABILITY: 55,
ATTR_FORECAST_TEMP: 5.0, ATTR_FORECAST_TEMP: 5.0,
ATTR_FORECAST_TEMP_LOW: 3.1, ATTR_FORECAST_TEMP_LOW: 3.1,
@ -204,7 +204,7 @@ async def test_v3_weather(
{ {
ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY, ATTR_FORECAST_CONDITION: ATTR_CONDITION_CLOUDY,
ATTR_FORECAST_TIME: "2021-03-21T00:00:00-07:00", 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_PRECIPITATION_PROBABILITY: 20,
ATTR_FORECAST_TEMP: 6.8, ATTR_FORECAST_TEMP: 6.8,
ATTR_FORECAST_TEMP_LOW: 0.9, 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_FRIENDLY_NAME] == "ClimaCell - Daily"
assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 24 assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 24
assert weather_state.attributes[ATTR_WEATHER_OZONE] == 52.625 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_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_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_CLOUD_COVER] == 100
assert weather_state.attributes[ATTR_WIND_GUST] == 24.0758 assert weather_state.attributes[ATTR_WIND_GUST] == 24.0758
assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain" assert weather_state.attributes[ATTR_PRECIPITATION_TYPE] == "rain"

View File

@ -36,7 +36,7 @@ async def test_attributes(hass):
assert data.get(ATTR_WEATHER_TEMPERATURE) == 21.6 assert data.get(ATTR_WEATHER_TEMPERATURE) == 21.6
assert data.get(ATTR_WEATHER_HUMIDITY) == 92 assert data.get(ATTR_WEATHER_HUMIDITY) == 92
assert data.get(ATTR_WEATHER_PRESSURE) == 1099 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_WIND_BEARING) is None
assert data.get(ATTR_WEATHER_OZONE) is None assert data.get(ATTR_WEATHER_OZONE) is None
assert data.get(ATTR_ATTRIBUTION) == "Powered by Home Assistant" 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 data.get(ATTR_FORECAST)[6].get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 100
) )
assert len(data.get(ATTR_FORECAST)) == 7 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

View File

@ -198,7 +198,7 @@ async def test_daily_forecast(hass):
assert forecast.get(ATTR_FORECAST_TEMP) == 16.2 assert forecast.get(ATTR_FORECAST_TEMP) == 16.2
assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 10.6 assert forecast.get(ATTR_FORECAST_TEMP_LOW) == 10.6
assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == "100.0" 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" 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_CONDITION) == "rainy"
assert forecast.get(ATTR_FORECAST_TEMP) == 7.7 assert forecast.get(ATTR_FORECAST_TEMP) == 7.7
assert forecast.get(ATTR_FORECAST_PRECIPITATION_PROBABILITY) == 80.0 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" assert forecast.get(ATTR_FORECAST_WIND_BEARING) == "S"

View File

@ -85,8 +85,8 @@ async def test_weather(hass: HomeAssistant, knx: KNXTestKit):
state = hass.states.get("weather.test") state = hass.states.get("weather.test")
assert state.attributes["temperature"] == 0.4 assert state.attributes["temperature"] == 0.4
assert state.attributes["wind_bearing"] == 270 assert state.attributes["wind_bearing"] == 270
assert state.attributes["wind_speed"] == 1.4400000000000002 assert state.attributes["wind_speed"] == 1.44
assert state.attributes["pressure"] == 980.5824 assert state.attributes["pressure"] == 980.58
assert state.state is ATTR_CONDITION_SUNNY assert state.state is ATTR_CONDITION_SUNNY
# update from KNX - set rain alarm # update from KNX - set rain alarm

View File

@ -99,13 +99,13 @@ async def test_v4_weather(hass: HomeAssistant) -> None:
ATTR_FORECAST_TEMP: 45.9, ATTR_FORECAST_TEMP: 45.9,
ATTR_FORECAST_TEMP_LOW: 26.1, ATTR_FORECAST_TEMP_LOW: 26.1,
ATTR_FORECAST_WIND_BEARING: 239.6, 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_FRIENDLY_NAME] == "Tomorrow.io - Daily"
assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23 assert weather_state.attributes[ATTR_WEATHER_HUMIDITY] == 23
assert weather_state.attributes[ATTR_WEATHER_OZONE] == 46.53 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_TEMPERATURE] == 44.1
assert weather_state.attributes[ATTR_WEATHER_VISIBILITY] == 8.15 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_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

View File

@ -1,4 +1,6 @@
"""The test for weather entity.""" """The test for weather entity."""
from datetime import datetime
import pytest import pytest
from pytest import approx from pytest import approx
@ -9,19 +11,44 @@ from homeassistant.components.weather import (
ATTR_FORECAST_PRESSURE, ATTR_FORECAST_PRESSURE,
ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TEMP_LOW,
ATTR_FORECAST_WIND_BEARING,
ATTR_FORECAST_WIND_SPEED, ATTR_FORECAST_WIND_SPEED,
ATTR_WEATHER_OZONE,
ATTR_WEATHER_PRECIPITATION_UNIT,
ATTR_WEATHER_PRESSURE, ATTR_WEATHER_PRESSURE,
ATTR_WEATHER_PRESSURE_UNIT,
ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_TEMPERATURE,
ATTR_WEATHER_TEMPERATURE_UNIT,
ATTR_WEATHER_VISIBILITY, ATTR_WEATHER_VISIBILITY,
ATTR_WEATHER_VISIBILITY_UNIT,
ATTR_WEATHER_WIND_BEARING,
ATTR_WEATHER_WIND_SPEED, ATTR_WEATHER_WIND_SPEED,
ATTR_WEATHER_WIND_SPEED_UNIT,
ROUNDING_PRECISION,
Forecast,
WeatherEntity,
round_temperature,
) )
from homeassistant.const import ( from homeassistant.const import (
ATTR_FRIENDLY_NAME,
LENGTH_INCHES,
LENGTH_KILOMETERS,
LENGTH_MILES, LENGTH_MILES,
LENGTH_MILLIMETERS, LENGTH_MILLIMETERS,
PRECISION_HALVES,
PRECISION_TENTHS,
PRECISION_WHOLE,
PRESSURE_HPA,
PRESSURE_INHG, PRESSURE_INHG,
PRESSURE_PA,
SPEED_KILOMETERS_PER_HOUR,
SPEED_METERS_PER_SECOND, SPEED_METERS_PER_SECOND,
SPEED_MILES_PER_HOUR,
TEMP_CELSIUS,
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
) )
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.distance import convert as convert_distance from homeassistant.util.distance import convert as convert_distance
from homeassistant.util.pressure import convert as convert_pressure 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.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
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.""" """Create the weather entity to run tests on."""
kwargs = {"temperature": None, "temperature_unit": None, **kwargs} kwargs = {"native_temperature": None, "native_temperature_unit": None, **kwargs}
platform = getattr(hass.components, "test.weather") platform: WeatherPlatform = getattr(hass.components, "test.weather")
platform.init(empty=True) platform.init(empty=True)
platform.ENTITIES.append( platform.ENTITIES.append(
platform.MockWeatherMockForecast( platform.MockWeatherMockForecast(
@ -49,145 +140,741 @@ async def create_entity(hass, **kwargs):
return entity0 return entity0
@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) @pytest.mark.parametrize("native_unit", (TEMP_FAHRENHEIT, TEMP_CELSIUS))
async def test_temperature_conversion( @pytest.mark.parametrize(
hass, "state_unit, unit_system",
((TEMP_CELSIUS, METRIC_SYSTEM), (TEMP_FAHRENHEIT, IMPERIAL_SYSTEM)),
)
async def test_temperature(
hass: HomeAssistant,
enable_custom_integrations, enable_custom_integrations,
native_unit: str,
state_unit: str,
unit_system, unit_system,
): ):
"""Test temperature conversion.""" """Test temperature."""
hass.config.units = unit_system hass.config.units = unit_system
native_value = 38 native_value = 38
native_unit = TEMP_FAHRENHEIT state_value = convert_temperature(native_value, native_unit, state_unit)
entity0 = await create_entity( 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) state = hass.states.get(entity0.entity_id)
forecast = state.attributes[ATTR_FORECAST][0] forecast = state.attributes[ATTR_FORECAST][0]
expected = convert_temperature( expected = state_value
native_value, native_unit, unit_system.temperature_unit
)
assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == approx( assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == approx(
expected, rel=0.1 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]) == approx(expected, rel=0.1)
assert float(forecast[ATTR_FORECAST_TEMP_LOW]) == 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]) @pytest.mark.parametrize("native_unit", (None,))
async def test_pressure_conversion( @pytest.mark.parametrize(
hass, "state_unit, unit_system",
((TEMP_CELSIUS, METRIC_SYSTEM), (TEMP_FAHRENHEIT, IMPERIAL_SYSTEM)),
)
async def test_temperature_no_unit(
hass: HomeAssistant,
enable_custom_integrations, enable_custom_integrations,
native_unit: str,
state_unit: str,
unit_system, unit_system,
): ):
"""Test pressure conversion.""" """Test temperature when the entity does not declare a native unit."""
hass.config.units = unit_system hass.config.units = unit_system
native_value = 30 native_value = 38
native_unit = PRESSURE_INHG state_value = native_value
entity0 = await create_entity( 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) state = hass.states.get(entity0.entity_id)
forecast = state.attributes[ATTR_FORECAST][0] 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(state.attributes[ATTR_WEATHER_PRESSURE]) == approx(expected, rel=1e-2)
assert float(forecast[ATTR_FORECAST_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]) @pytest.mark.parametrize("native_unit", (None,))
async def test_wind_speed_conversion( @pytest.mark.parametrize(
hass, "state_unit, unit_system",
((PRESSURE_HPA, METRIC_SYSTEM), (PRESSURE_INHG, IMPERIAL_SYSTEM)),
)
async def test_pressure_no_unit(
hass: HomeAssistant,
enable_custom_integrations, enable_custom_integrations,
native_unit: str,
state_unit: str,
unit_system, unit_system,
): ):
"""Test wind speed conversion.""" """Test pressure when the entity does not declare a native unit."""
hass.config.units = unit_system hass.config.units = unit_system
native_value = 10 native_value = 30
native_unit = SPEED_METERS_PER_SECOND state_value = native_value
entity0 = await create_entity( 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) state = hass.states.get(entity0.entity_id)
forecast = state.attributes[ATTR_FORECAST][0] 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( assert float(state.attributes[ATTR_WEATHER_WIND_SPEED]) == approx(
expected, rel=1e-2 expected, rel=1e-2
) )
assert float(forecast[ATTR_FORECAST_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]) @pytest.mark.parametrize("native_unit", (None,))
async def test_visibility_conversion( @pytest.mark.parametrize(
hass, "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, enable_custom_integrations,
native_unit: str,
state_unit: str,
unit_system, unit_system,
): ):
"""Test visibility conversion.""" """Test wind speed when the entity does not declare a native unit."""
hass.config.units = unit_system hass.config.units = unit_system
native_value = 10 native_value = 10
native_unit = LENGTH_MILES state_value = native_value
entity0 = await create_entity( 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) 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( assert float(state.attributes[ATTR_WEATHER_VISIBILITY]) == approx(
expected, rel=1e-2 expected, rel=1e-2
) )
@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM]) @pytest.mark.parametrize("native_unit", (None,))
async def test_precipitation_conversion( @pytest.mark.parametrize(
hass, "state_unit, unit_system",
(
(LENGTH_KILOMETERS, METRIC_SYSTEM),
(LENGTH_MILES, IMPERIAL_SYSTEM),
),
)
async def test_visibility_no_unit(
hass: HomeAssistant,
enable_custom_integrations, enable_custom_integrations,
native_unit: str,
state_unit: str,
unit_system, unit_system,
): ):
"""Test precipitation conversion.""" """Test visibility when the entity does not declare a native unit."""
hass.config.units = unit_system hass.config.units = unit_system
native_value = 30 native_value = 10
native_unit = LENGTH_MILLIMETERS state_value = native_value
entity0 = await create_entity( 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) state = hass.states.get(entity0.entity_id)
forecast = state.attributes[ATTR_FORECAST][0] forecast = state.attributes[ATTR_FORECAST][0]
expected = convert_distance( expected = state_value
native_value, native_unit, unit_system.accumulated_precipitation_unit
)
assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx(expected, rel=1e-2) 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( async def test_none_forecast(
hass, hass: HomeAssistant,
enable_custom_integrations, enable_custom_integrations,
): ):
"""Test that conversion with None values succeeds.""" """Test that conversion with None values succeeds."""
entity0 = await create_entity( entity0 = await create_entity(
hass, hass,
pressure=None, native_pressure=None,
pressure_unit=PRESSURE_INHG, native_pressure_unit=PRESSURE_INHG,
wind_speed=None, native_wind_speed=None,
wind_speed_unit=SPEED_METERS_PER_SECOND, native_wind_speed_unit=SPEED_METERS_PER_SECOND,
precipitation=None, native_precipitation=None,
precipitation_unit=LENGTH_MILLIMETERS, native_precipitation_unit=LENGTH_MILLIMETERS,
) )
state = hass.states.get(entity0.entity_id) state = hass.states.get(entity0.entity_id)
forecast = state.attributes[ATTR_FORECAST][0] forecast = state.attributes[ATTR_FORECAST][0]
assert forecast[ATTR_FORECAST_PRESSURE] is None assert forecast.get(ATTR_FORECAST_PRESSURE) is None
assert forecast[ATTR_FORECAST_WIND_SPEED] is None assert forecast.get(ATTR_FORECAST_WIND_SPEED) is None
assert forecast[ATTR_FORECAST_PRECIPITATION] 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

View File

@ -6,6 +6,11 @@ Call init before using it in your tests to ensure clean test data.
from __future__ import annotations from __future__ import annotations
from homeassistant.components.weather import ( 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_PRECIPITATION,
ATTR_FORECAST_PRESSURE, ATTR_FORECAST_PRESSURE,
ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP,
@ -37,6 +42,80 @@ async def async_setup_platform(
class MockWeather(MockEntity, WeatherEntity): class MockWeather(MockEntity, WeatherEntity):
"""Mock weather class.""" """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 @property
def temperature(self) -> float | None: def temperature(self) -> float | None:
"""Return the platform temperature.""" """Return the platform temperature."""
@ -99,7 +178,7 @@ class MockWeather(MockEntity, WeatherEntity):
@property @property
def precipitation_unit(self) -> str | None: 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") return self._handle("precipitation_unit")
@property @property
@ -111,6 +190,26 @@ class MockWeather(MockEntity, WeatherEntity):
class MockWeatherMockForecast(MockWeather): class MockWeatherMockForecast(MockWeather):
"""Mock weather class with mocked forecast.""" """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 @property
def forecast(self) -> list[Forecast] | None: def forecast(self) -> list[Forecast] | None:
"""Return the forecast.""" """Return the forecast."""