diff --git a/homeassistant/components/template/weather.py b/homeassistant/components/template/weather.py index eabafb89803c15..aa6a17ef711774 100644 --- a/homeassistant/components/template/weather.py +++ b/homeassistant/components/template/weather.py @@ -1,4 +1,6 @@ """Template platform that aggregates meteorological data.""" +import logging + import voluptuous as vol from homeassistant.components.weather import ( @@ -20,10 +22,18 @@ ENTITY_ID_FORMAT, WeatherEntity, ) -from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID +from homeassistant.const import ( + CONF_NAME, + CONF_UNIQUE_ID, + LENGTH_KILOMETERS, + PRESSURE_HPA, + PRESSURE_INHG, +) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import PLATFORM_SCHEMA from homeassistant.helpers.entity import async_generate_entity_id +from homeassistant.util.pressure import convert as convert_pressure +from homeassistant.util.unit_system import METRIC_SYSTEM from .template_entity import TemplateEntity @@ -74,6 +84,8 @@ } ) +_LOGGER = logging.getLogger(__name__) + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Template weather.""" @@ -183,32 +195,80 @@ def temperature_unit(self): @property def humidity(self): """Return the humidity.""" + # unit from template: % return self._humidity @property def wind_speed(self): """Return the wind speed.""" - return self._wind_speed + # unit from template: km/h + + if self._wind_speed is None or self._wind_speed == "": + return None + + try: + return self.hass.config.units.length( + float(self._wind_speed), LENGTH_KILOMETERS + ) + except ValueError: + _LOGGER.warning( + "Weather template '%s': wind_speed_template must expand to a number, but results in '%s'", + self.entity_id, + self._wind_speed, + ) + return None @property def wind_bearing(self): """Return the wind bearing.""" + # unit from template: ° return self._wind_bearing @property def ozone(self): """Return the ozone level.""" + # unit from template: ppm (parts per million) return self._ozone @property def visibility(self): """Return the visibility.""" - return self._visibility + # unit from template: km + + if self._visibility is None or self._visibility == "": + return None + + try: + return self.hass.config.units.length( + float(self._visibility), LENGTH_KILOMETERS + ) + except ValueError: + _LOGGER.warning( + "Weather template '%s': visibility_template must expand to a number, but results in '%s'", + self.entity_id, + self._visibility, + ) + return None @property def pressure(self): """Return the air pressure.""" - return self._pressure + # unit from template: hPa + + if self._pressure is None or self._pressure == "": + return None + + try: + if self.hass.config.units == METRIC_SYSTEM: + return float(self._pressure) + return convert_pressure(float(self._pressure), PRESSURE_HPA, PRESSURE_INHG) + except ValueError: + _LOGGER.warning( + "Weather template '%s': pressure_template must expand to a number, but results in '%s'", + self.entity_id, + self._pressure, + ) + return None @property def forecast(self): diff --git a/tests/components/template/test_weather.py b/tests/components/template/test_weather.py index 649a54aa3aaccf..74e8a1dbe90e42 100644 --- a/tests/components/template/test_weather.py +++ b/tests/components/template/test_weather.py @@ -1,4 +1,6 @@ """The tests for the Template Weather platform.""" +from pytest import approx + from homeassistant.components.weather import ( ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, @@ -11,10 +13,20 @@ DOMAIN, ) from homeassistant.setup import async_setup_component +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem + + +async def test_template_state_text_metric(hass): + """Test the state text of a template with metric unit system.""" + await _test_template_state_text(hass, METRIC_SYSTEM) -async def test_template_state_text(hass): - """Test the state text of a template.""" +async def test_template_state_text_imperial(hass): + """Test the state text of a template with imperial unit system.""" + await _test_template_state_text(hass, IMPERIAL_SYSTEM) + + +async def _test_template_state_text(hass, unit_system: UnitSystem): await async_setup_component( hass, DOMAIN, @@ -43,21 +55,26 @@ async def test_template_state_text(hass): await hass.async_start() await hass.async_block_till_done() + hass.config.units = unit_system + await hass.async_block_till_done() + hass.states.async_set("sensor.attribution", "The custom attribution") await hass.async_block_till_done() - hass.states.async_set("sensor.temperature", 22.3) + # all temperature sensors are automatically converted into hass.config.units.temperature + # therefore, value will be interpreted as-is + hass.states.async_set("sensor.temperature", 22.3) # in °C or °F await hass.async_block_till_done() - hass.states.async_set("sensor.humidity", 60) + hass.states.async_set("sensor.humidity", 60) # in % await hass.async_block_till_done() - hass.states.async_set("sensor.pressure", 1000) + hass.states.async_set("sensor.pressure", 1002.3) # in hPa await hass.async_block_till_done() - hass.states.async_set("sensor.windspeed", 20) + hass.states.async_set("sensor.windspeed", 10) # in km/h await hass.async_block_till_done() - hass.states.async_set("sensor.windbearing", 180) + hass.states.async_set("sensor.windbearing", 180) # in ° await hass.async_block_till_done() - hass.states.async_set("sensor.ozone", 25) + hass.states.async_set("sensor.ozone", 25) # in ppm await hass.async_block_till_done() - hass.states.async_set("sensor.visibility", 4.6) + hass.states.async_set("sensor.visibility", 4.6) # in km await hass.async_block_till_done() state = hass.states.get("weather.test") @@ -67,10 +84,63 @@ async def test_template_state_text(hass): data = state.attributes assert data.get(ATTR_WEATHER_ATTRIBUTION) == "The custom attribution" - assert data.get(ATTR_WEATHER_TEMPERATURE) == 22.3 - assert data.get(ATTR_WEATHER_HUMIDITY) == 60 - assert data.get(ATTR_WEATHER_PRESSURE) == 1000 - assert data.get(ATTR_WEATHER_WIND_SPEED) == 20 - assert data.get(ATTR_WEATHER_WIND_BEARING) == 180 - assert data.get(ATTR_WEATHER_OZONE) == 25 - assert data.get(ATTR_WEATHER_VISIBILITY) == 4.6 + assert data.get(ATTR_WEATHER_HUMIDITY) == 60 # in % + assert data.get(ATTR_WEATHER_WIND_BEARING) == 180 # in ° + assert data.get(ATTR_WEATHER_OZONE) == 25 # in ppm + + if unit_system is METRIC_SYSTEM: + assert data.get(ATTR_WEATHER_TEMPERATURE) == approx(22.3) # in °C + assert data.get(ATTR_WEATHER_PRESSURE) == approx(1002.3) # in hpa + assert data.get(ATTR_WEATHER_WIND_SPEED) == approx(10) # in km/h + assert data.get(ATTR_WEATHER_VISIBILITY) == approx(4.6) # in km + else: + assert data.get(ATTR_WEATHER_TEMPERATURE) == approx(22) # in °F (rounded) + assert data.get(ATTR_WEATHER_PRESSURE) == approx(29.597899) # in inhg + assert data.get(ATTR_WEATHER_WIND_SPEED) == approx(6.21371) # in mph + assert data.get(ATTR_WEATHER_VISIBILITY) == approx(2.858306) # in mi + + +async def test_template_state_text_empty_sensor_values(hass): + """Test the state text of a template if sensors report empty values.""" + await async_setup_component( + hass, + DOMAIN, + { + "weather": [ + {"weather": {"platform": "demo"}}, + { + "platform": "template", + "name": "test", + "attribution_template": "{{ states('sensor.attribution') }}", + "condition_template": "sunny", + "forecast_template": "{{ states.weather.demo.attributes.forecast }}", + "temperature_template": "{{ states('sensor.temperature') | float }}", + "humidity_template": "{{ states('sensor.humidity') | int }}", + "pressure_template": "{{ states('sensor.pressure') }}", + "wind_speed_template": "{{ states('sensor.windspeed') }}", + "wind_bearing_template": "{{ states('sensor.windbearing') }}", + "ozone_template": "{{ states('sensor.ozone') }}", + "visibility_template": "{{ states('sensor.visibility') }}", + }, + ] + }, + ) + await hass.async_block_till_done() + + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("sensor.pressure", "") + await hass.async_block_till_done() + hass.states.async_set("sensor.windspeed", "") + await hass.async_block_till_done() + hass.states.async_set("sensor.visibility", "") + await hass.async_block_till_done() + + state = hass.states.get("weather.test") + assert state is not None + + data = state.attributes + assert data.get(ATTR_WEATHER_PRESSURE) is None + assert data.get(ATTR_WEATHER_WIND_SPEED) is None + assert data.get(ATTR_WEATHER_VISIBILITY) is None