Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions homeassistant/components/metoffice/weather.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
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
Expand Down Expand Up @@ -123,11 +123,6 @@ def visibility(self):
_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."""
Expand Down
90 changes: 80 additions & 10 deletions homeassistant/components/weather/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@

SCAN_INTERVAL = timedelta(seconds=30)

ROUNDING_PRECISION = 2


class Forecast(TypedDict, total=False):
"""Typed weather forecast dict."""
Expand Down Expand Up @@ -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."""
Expand All @@ -156,17 +172,27 @@ def ozone(self) -> float | None:

@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 (
Expand All @@ -178,11 +204,14 @@ def precision(self) -> float:
@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:
Expand All @@ -192,15 +221,28 @@ def state_attributes(self):
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),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is hass config doing the unit conversion?

wind_seed should be imported directly from util.unit_system instead.

The same applies to all other unit conversions happening here.

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:
Expand All @@ -220,6 +262,34 @@ def state_attributes(self):
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
170 changes: 170 additions & 0 deletions tests/components/weather/test_init.py
Original file line number Diff line number Diff line change
@@ -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)
Loading