mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Add native unit types for weather entities (#59533)
* Add native unit types for weather entities * Update weatherentity and change precision in climacell test * Move weather test to demo tests * Add weather test for temperature conversion * Add more unit conversion tests * Remove extra native_ methods * Remove extra properties and save precision change for another PR * Remove visibility_unit from metoffice component The vibility values given by metoffice are formatted into strings, which means they can't automatically be converted. * Improve docstrings and convert pressures in forecast * Add precipitation and wind speed units * Clean up tests * Round converted weather values * Round weather values to 2 decimal places * Move number of rounding decimal places to constant * Docstring and styles
This commit is contained in:
parent
5a97db6685
commit
09af85c6a4
@ -8,7 +8,7 @@ from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_WIND_SPEED,
|
||||
WeatherEntity,
|
||||
)
|
||||
from homeassistant.const import LENGTH_KILOMETERS, TEMP_CELSIUS
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@ -117,11 +117,6 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity):
|
||||
_visibility = f"{visibility_class} - {visibility_distance}"
|
||||
return _visibility
|
||||
|
||||
@property
|
||||
def visibility_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return LENGTH_KILOMETERS
|
||||
|
||||
@property
|
||||
def pressure(self):
|
||||
"""Return the mean sea-level pressure."""
|
||||
|
@ -61,6 +61,8 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
ROUNDING_PRECISION = 2
|
||||
|
||||
|
||||
class Forecast(TypedDict, total=False):
|
||||
"""Typed weather forecast dict."""
|
||||
@ -112,38 +114,52 @@ class WeatherEntity(Entity):
|
||||
_attr_ozone: float | None = None
|
||||
_attr_precision: float
|
||||
_attr_pressure: float | None = None
|
||||
_attr_pressure_unit: str | None = None
|
||||
_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_wind_bearing: float | str | None = None
|
||||
_attr_wind_speed: float | None = None
|
||||
_attr_wind_speed_unit: str | None = None
|
||||
|
||||
@property
|
||||
def temperature(self) -> float | None:
|
||||
"""Return the platform temperature."""
|
||||
"""Return the platform temperature in native units (i.e. not converted)."""
|
||||
return self._attr_temperature
|
||||
|
||||
@property
|
||||
def temperature_unit(self) -> str:
|
||||
"""Return the unit of measurement."""
|
||||
"""Return the native unit of measurement for temperature."""
|
||||
return self._attr_temperature_unit
|
||||
|
||||
@property
|
||||
def pressure(self) -> float | None:
|
||||
"""Return the pressure."""
|
||||
"""Return the pressure in native units."""
|
||||
return self._attr_pressure
|
||||
|
||||
@property
|
||||
def pressure_unit(self) -> str | None:
|
||||
"""Return the native unit of measurement for pressure."""
|
||||
return self._attr_pressure_unit
|
||||
|
||||
@property
|
||||
def humidity(self) -> float | None:
|
||||
"""Return the humidity."""
|
||||
"""Return the humidity in native units."""
|
||||
return self._attr_humidity
|
||||
|
||||
@property
|
||||
def wind_speed(self) -> float | None:
|
||||
"""Return the wind speed."""
|
||||
"""Return the wind speed in native units."""
|
||||
return self._attr_wind_speed
|
||||
|
||||
@property
|
||||
def wind_speed_unit(self) -> str | None:
|
||||
"""Return the native unit of measurement for wind speed."""
|
||||
return self._attr_wind_speed_unit
|
||||
|
||||
@property
|
||||
def wind_bearing(self) -> float | str | None:
|
||||
"""Return the wind bearing."""
|
||||
@ -156,17 +172,27 @@ class WeatherEntity(Entity):
|
||||
|
||||
@property
|
||||
def visibility(self) -> float | None:
|
||||
"""Return the visibility."""
|
||||
"""Return the visibility in native units."""
|
||||
return self._attr_visibility
|
||||
|
||||
@property
|
||||
def visibility_unit(self) -> str | None:
|
||||
"""Return the native unit of measurement for visibility."""
|
||||
return self._attr_visibility_unit
|
||||
|
||||
@property
|
||||
def forecast(self) -> list[Forecast] | None:
|
||||
"""Return the forecast."""
|
||||
"""Return the forecast in native units."""
|
||||
return self._attr_forecast
|
||||
|
||||
@property
|
||||
def precipitation_unit(self) -> str | None:
|
||||
"""Return the native unit of measurement for accumulated precipitation."""
|
||||
return self._attr_precipitation_unit
|
||||
|
||||
@property
|
||||
def precision(self) -> float:
|
||||
"""Return the precision of the temperature value."""
|
||||
"""Return the precision of the temperature value, after unit conversion."""
|
||||
if hasattr(self, "_attr_precision"):
|
||||
return self._attr_precision
|
||||
return (
|
||||
@ -178,11 +204,14 @@ class WeatherEntity(Entity):
|
||||
@final
|
||||
@property
|
||||
def state_attributes(self):
|
||||
"""Return the state attributes."""
|
||||
"""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
|
||||
self.hass,
|
||||
self.temperature,
|
||||
self.temperature_unit,
|
||||
self.precision,
|
||||
)
|
||||
|
||||
if (humidity := self.humidity) is not None:
|
||||
@ -192,15 +221,28 @@ class WeatherEntity(Entity):
|
||||
data[ATTR_WEATHER_OZONE] = ozone
|
||||
|
||||
if (pressure := self.pressure) is not None:
|
||||
if (unit := self.pressure_unit) is not None:
|
||||
pressure = round(
|
||||
self.hass.config.units.pressure(pressure, unit), ROUNDING_PRECISION
|
||||
)
|
||||
data[ATTR_WEATHER_PRESSURE] = pressure
|
||||
|
||||
if (wind_bearing := self.wind_bearing) is not None:
|
||||
data[ATTR_WEATHER_WIND_BEARING] = wind_bearing
|
||||
|
||||
if (wind_speed := self.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,
|
||||
)
|
||||
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_VISIBILITY] = visibility
|
||||
|
||||
if self.forecast is not None:
|
||||
@ -220,6 +262,34 @@ class WeatherEntity(Entity):
|
||||
self.temperature_unit,
|
||||
self.precision,
|
||||
)
|
||||
if ATTR_FORECAST_PRESSURE in forecast_entry:
|
||||
if (unit := self.pressure_unit) is not None:
|
||||
pressure = round(
|
||||
self.hass.config.units.pressure(
|
||||
forecast_entry[ATTR_FORECAST_PRESSURE], unit
|
||||
),
|
||||
ROUNDING_PRECISION,
|
||||
)
|
||||
forecast_entry[ATTR_FORECAST_PRESSURE] = pressure
|
||||
if ATTR_FORECAST_WIND_SPEED in forecast_entry:
|
||||
if (unit := self.wind_speed_unit) is not None:
|
||||
wind_speed = round(
|
||||
self.hass.config.units.wind_speed(
|
||||
forecast_entry[ATTR_FORECAST_WIND_SPEED], unit
|
||||
),
|
||||
ROUNDING_PRECISION,
|
||||
)
|
||||
forecast_entry[ATTR_FORECAST_WIND_SPEED] = wind_speed
|
||||
if ATTR_FORECAST_PRECIPITATION in forecast_entry:
|
||||
if (unit := self.precipitation_unit) is not None:
|
||||
precipitation = round(
|
||||
self.hass.config.units.accumulated_precipitation(
|
||||
forecast_entry[ATTR_FORECAST_PRECIPITATION], unit
|
||||
),
|
||||
ROUNDING_PRECISION,
|
||||
)
|
||||
forecast_entry[ATTR_FORECAST_PRECIPITATION] = precipitation
|
||||
|
||||
forecast.append(forecast_entry)
|
||||
|
||||
data[ATTR_FORECAST] = forecast
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""The tests for the Weather component."""
|
||||
"""The tests for the demo weather component."""
|
||||
from homeassistant.components import weather
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_FORECAST,
|
170
tests/components/weather/test_init.py
Normal file
170
tests/components/weather/test_init.py
Normal file
@ -0,0 +1,170 @@
|
||||
"""The test for weather entity."""
|
||||
import pytest
|
||||
from pytest import approx
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_SUNNY,
|
||||
ATTR_FORECAST,
|
||||
ATTR_FORECAST_PRECIPITATION,
|
||||
ATTR_FORECAST_PRESSURE,
|
||||
ATTR_FORECAST_TEMP,
|
||||
ATTR_FORECAST_TEMP_LOW,
|
||||
ATTR_FORECAST_WIND_SPEED,
|
||||
ATTR_WEATHER_PRESSURE,
|
||||
ATTR_WEATHER_TEMPERATURE,
|
||||
ATTR_WEATHER_VISIBILITY,
|
||||
ATTR_WEATHER_WIND_SPEED,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
LENGTH_MILES,
|
||||
LENGTH_MILLIMETERS,
|
||||
PRESSURE_INHG,
|
||||
SPEED_METERS_PER_SECOND,
|
||||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.distance import convert as convert_distance
|
||||
from homeassistant.util.pressure import convert as convert_pressure
|
||||
from homeassistant.util.speed import convert as convert_speed
|
||||
from homeassistant.util.temperature import convert as convert_temperature
|
||||
from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM
|
||||
|
||||
|
||||
async def create_entity(hass, **kwargs):
|
||||
"""Create the weather entity to run tests on."""
|
||||
kwargs = {"temperature": None, "temperature_unit": None, **kwargs}
|
||||
platform = getattr(hass.components, "test.weather")
|
||||
platform.init(empty=True)
|
||||
platform.ENTITIES.append(
|
||||
platform.MockWeatherMockForecast(
|
||||
name="Test", condition=ATTR_CONDITION_SUNNY, **kwargs
|
||||
)
|
||||
)
|
||||
|
||||
entity0 = platform.ENTITIES[0]
|
||||
assert await async_setup_component(
|
||||
hass, "weather", {"weather": {"platform": "test"}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
return entity0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM])
|
||||
async def test_temperature_conversion(
|
||||
hass,
|
||||
enable_custom_integrations,
|
||||
unit_system,
|
||||
):
|
||||
"""Test temperature conversion."""
|
||||
hass.config.units = unit_system
|
||||
native_value = 38
|
||||
native_unit = TEMP_FAHRENHEIT
|
||||
|
||||
entity0 = await create_entity(
|
||||
hass, temperature=native_value, 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
|
||||
)
|
||||
assert float(state.attributes[ATTR_WEATHER_TEMPERATURE]) == 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)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unit_system", [IMPERIAL_SYSTEM, METRIC_SYSTEM])
|
||||
async def test_pressure_conversion(
|
||||
hass,
|
||||
enable_custom_integrations,
|
||||
unit_system,
|
||||
):
|
||||
"""Test pressure conversion."""
|
||||
hass.config.units = unit_system
|
||||
native_value = 30
|
||||
native_unit = PRESSURE_INHG
|
||||
|
||||
entity0 = await create_entity(
|
||||
hass, pressure=native_value, 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)
|
||||
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,
|
||||
enable_custom_integrations,
|
||||
unit_system,
|
||||
):
|
||||
"""Test wind speed conversion."""
|
||||
hass.config.units = unit_system
|
||||
native_value = 10
|
||||
native_unit = SPEED_METERS_PER_SECOND
|
||||
|
||||
entity0 = await create_entity(
|
||||
hass, wind_speed=native_value, 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)
|
||||
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,
|
||||
enable_custom_integrations,
|
||||
unit_system,
|
||||
):
|
||||
"""Test visibility conversion."""
|
||||
hass.config.units = unit_system
|
||||
native_value = 10
|
||||
native_unit = LENGTH_MILES
|
||||
|
||||
entity0 = await create_entity(
|
||||
hass, visibility=native_value, visibility_unit=native_unit
|
||||
)
|
||||
|
||||
state = hass.states.get(entity0.entity_id)
|
||||
expected = convert_distance(native_value, native_unit, unit_system.length_unit)
|
||||
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,
|
||||
enable_custom_integrations,
|
||||
unit_system,
|
||||
):
|
||||
"""Test precipitation conversion."""
|
||||
hass.config.units = unit_system
|
||||
native_value = 30
|
||||
native_unit = LENGTH_MILLIMETERS
|
||||
|
||||
entity0 = await create_entity(
|
||||
hass, precipitation=native_value, 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
|
||||
)
|
||||
assert float(forecast[ATTR_FORECAST_PRECIPITATION]) == approx(expected, rel=1e-2)
|
126
tests/testing_config/custom_components/test/weather.py
Normal file
126
tests/testing_config/custom_components/test/weather.py
Normal file
@ -0,0 +1,126 @@
|
||||
"""
|
||||
Provide a mock weather platform.
|
||||
|
||||
Call init before using it in your tests to ensure clean test data.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_PRECIPITATION,
|
||||
ATTR_FORECAST_PRESSURE,
|
||||
ATTR_FORECAST_TEMP,
|
||||
ATTR_FORECAST_TEMP_LOW,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
ATTR_FORECAST_WIND_SPEED,
|
||||
Forecast,
|
||||
WeatherEntity,
|
||||
)
|
||||
|
||||
from tests.common import MockEntity
|
||||
|
||||
ENTITIES = []
|
||||
|
||||
|
||||
def init(empty=False):
|
||||
"""Initialize the platform with entities."""
|
||||
global ENTITIES
|
||||
ENTITIES = [] if empty else [MockWeather()]
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities_callback, discovery_info=None
|
||||
):
|
||||
"""Return mock entities."""
|
||||
async_add_entities_callback(ENTITIES)
|
||||
|
||||
|
||||
class MockWeather(MockEntity, WeatherEntity):
|
||||
"""Mock weather class."""
|
||||
|
||||
@property
|
||||
def temperature(self) -> float | None:
|
||||
"""Return the platform temperature."""
|
||||
return self._handle("temperature")
|
||||
|
||||
@property
|
||||
def temperature_unit(self) -> str | None:
|
||||
"""Return the unit of measurement for temperature."""
|
||||
return self._handle("temperature_unit")
|
||||
|
||||
@property
|
||||
def pressure(self) -> float | None:
|
||||
"""Return the pressure."""
|
||||
return self._handle("pressure")
|
||||
|
||||
@property
|
||||
def pressure_unit(self) -> str | None:
|
||||
"""Return the unit of measurement for pressure."""
|
||||
return self._handle("pressure_unit")
|
||||
|
||||
@property
|
||||
def humidity(self) -> float | None:
|
||||
"""Return the humidity."""
|
||||
return self._handle("humidity")
|
||||
|
||||
@property
|
||||
def wind_speed(self) -> float | None:
|
||||
"""Return the wind speed."""
|
||||
return self._handle("wind_speed")
|
||||
|
||||
@property
|
||||
def wind_speed_unit(self) -> str | None:
|
||||
"""Return the unit of measurement for wind speed."""
|
||||
return self._handle("wind_speed_unit")
|
||||
|
||||
@property
|
||||
def wind_bearing(self) -> float | str | None:
|
||||
"""Return the wind bearing."""
|
||||
return self._handle("wind_bearing")
|
||||
|
||||
@property
|
||||
def ozone(self) -> float | None:
|
||||
"""Return the ozone level."""
|
||||
return self._handle("ozone")
|
||||
|
||||
@property
|
||||
def visibility(self) -> float | None:
|
||||
"""Return the visibility."""
|
||||
return self._handle("visibility")
|
||||
|
||||
@property
|
||||
def visibility_unit(self) -> str | None:
|
||||
"""Return the unit of measurement for visibility."""
|
||||
return self._handle("visibility_unit")
|
||||
|
||||
@property
|
||||
def forecast(self) -> list[Forecast] | None:
|
||||
"""Return the forecast."""
|
||||
return self._handle("forecast")
|
||||
|
||||
@property
|
||||
def precipitation_unit(self) -> str | None:
|
||||
"""Return the native unit of measurement for accumulated precipitation."""
|
||||
return self._handle("precipitation_unit")
|
||||
|
||||
@property
|
||||
def condition(self) -> str | None:
|
||||
"""Return the current condition."""
|
||||
return self._handle("condition")
|
||||
|
||||
|
||||
class MockWeatherMockForecast(MockWeather):
|
||||
"""Mock weather class with mocked forecast."""
|
||||
|
||||
@property
|
||||
def forecast(self) -> list[Forecast] | None:
|
||||
"""Return the forecast."""
|
||||
return [
|
||||
{
|
||||
ATTR_FORECAST_TEMP: self.temperature,
|
||||
ATTR_FORECAST_TEMP_LOW: self.temperature,
|
||||
ATTR_FORECAST_PRESSURE: self.pressure,
|
||||
ATTR_FORECAST_WIND_SPEED: self.wind_speed,
|
||||
ATTR_FORECAST_WIND_BEARING: self.wind_bearing,
|
||||
ATTR_FORECAST_PRECIPITATION: self._values.get("precipitation"),
|
||||
}
|
||||
]
|
Loading…
x
Reference in New Issue
Block a user