From ed93c3b2c172012586ac3521fb50336f704c1ef9 Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Sun, 24 Mar 2019 13:37:31 -0400 Subject: [PATCH] Fix pressure in dark sky and openweathermap and add pressure utility (#21210) --- homeassistant/components/darksky/weather.py | 10 ++- .../components/openweathermap/weather.py | 10 ++- homeassistant/const.py | 8 +++ homeassistant/util/pressure.py | 51 ++++++++++++++ homeassistant/util/unit_system.py | 52 ++++++++------- tests/helpers/test_template.py | 3 +- tests/util/test_pressure.py | 66 +++++++++++++++++++ tests/util/test_unit_system.py | 48 ++++++++++++-- 8 files changed, 212 insertions(+), 36 deletions(-) create mode 100644 homeassistant/util/pressure.py create mode 100644 tests/util/test_pressure.py diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index d5cbcb4785a..5b3db4312bf 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -12,10 +12,10 @@ from homeassistant.components.weather import ( ATTR_FORECAST_WIND_SPEED, PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME, - TEMP_CELSIUS, TEMP_FAHRENHEIT) + PRESSURE_HPA, PRESSURE_INHG, TEMP_CELSIUS, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle - +from homeassistant.util.pressure import convert as convert_pressure REQUIREMENTS = ['python-forecastio==1.4.0'] _LOGGER = logging.getLogger(__name__) @@ -131,7 +131,11 @@ class DarkSkyWeather(WeatherEntity): @property def pressure(self): """Return the pressure.""" - return self._ds_currently.get('pressure') + pressure = self._ds_currently.get('pressure') + if 'us' in self._dark_sky.units: + return round( + convert_pressure(pressure, PRESSURE_HPA, PRESSURE_INHG), 2) + return pressure @property def visibility(self): diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index 58016dd3e2c..8a37bc97575 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -10,10 +10,10 @@ from homeassistant.components.weather import ( ATTR_FORECAST_WIND_SPEED, PLATFORM_SCHEMA, WeatherEntity) from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME, - STATE_UNKNOWN, TEMP_CELSIUS) + PRESSURE_HPA, PRESSURE_INHG, STATE_UNKNOWN, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle - +from homeassistant.util.pressure import convert as convert_pressure REQUIREMENTS = ['pyowm==2.10.0'] _LOGGER = logging.getLogger(__name__) @@ -114,7 +114,11 @@ class OpenWeatherMapWeather(WeatherEntity): @property def pressure(self): """Return the pressure.""" - return self.data.get_pressure().get('press') + pressure = self.data.get_pressure().get('press') + if self.hass.config.units.name == 'imperial': + return round( + convert_pressure(pressure, PRESSURE_HPA, PRESSURE_INHG), 2) + return pressure @property def humidity(self): diff --git a/homeassistant/const.py b/homeassistant/const.py index f825e066f76..2d2f00f1e16 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -343,6 +343,13 @@ LENGTH_FEET = 'ft' # type: str LENGTH_YARD = 'yd' # type: str LENGTH_MILES = 'mi' # type: str +# Pressure units +PRESSURE_PA = 'Pa' # type: str +PRESSURE_HPA = 'hPa' # type: str +PRESSURE_MBAR = 'mbar' # type: str +PRESSURE_INHG = 'inHg' # type: str +PRESSURE_PSI = 'psi' # type: str + # Volume units VOLUME_LITERS = 'L' # type: str VOLUME_MILLILITERS = 'mL' # type: str @@ -455,6 +462,7 @@ UNIT_NOT_RECOGNIZED_TEMPLATE = '{} is not a recognized {} unit.' # type: str LENGTH = 'length' # type: str MASS = 'mass' # type: str +PRESSURE = 'pressure' # type: str VOLUME = 'volume' # type: str TEMPERATURE = 'temperature' # type: str SPEED_MS = 'speed_ms' # type: str diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py new file mode 100644 index 00000000000..ecfa6344d29 --- /dev/null +++ b/homeassistant/util/pressure.py @@ -0,0 +1,51 @@ +"""Pressure util functions.""" + +import logging +from numbers import Number + +from homeassistant.const import ( + PRESSURE_PA, + PRESSURE_HPA, + PRESSURE_MBAR, + PRESSURE_INHG, + PRESSURE_PSI, + UNIT_NOT_RECOGNIZED_TEMPLATE, + PRESSURE, +) + +_LOGGER = logging.getLogger(__name__) + +VALID_UNITS = [ + PRESSURE_PA, + PRESSURE_HPA, + PRESSURE_MBAR, + PRESSURE_INHG, + PRESSURE_PSI, +] + +UNIT_CONVERSION = { + PRESSURE_PA: 1, + PRESSURE_HPA: 1 / 100, + PRESSURE_MBAR: 1 / 100, + PRESSURE_INHG: 1 / 3386.389, + PRESSURE_PSI: 1 / 6894.757, +} + + +def convert(value: float, unit_1: str, unit_2: str) -> float: + """Convert one unit of measurement to another.""" + if unit_1 not in VALID_UNITS: + raise ValueError( + UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, PRESSURE)) + if unit_2 not in VALID_UNITS: + raise ValueError( + UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_2, PRESSURE)) + + if not isinstance(value, Number): + raise TypeError('{} is not of numeric type'.format(value)) + + if unit_1 == unit_2 or unit_1 not in VALID_UNITS: + return value + + pascals = value / UNIT_CONVERSION[unit_1] + return pascals * UNIT_CONVERSION[unit_2] diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 5f6d202b5e9..8e506dfca2e 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -5,27 +5,19 @@ from typing import Optional from numbers import Number from homeassistant.const import ( - TEMP_CELSIUS, TEMP_FAHRENHEIT, LENGTH_CENTIMETERS, LENGTH_METERS, - LENGTH_KILOMETERS, LENGTH_INCHES, LENGTH_FEET, LENGTH_YARD, LENGTH_MILES, - VOLUME_LITERS, VOLUME_MILLILITERS, VOLUME_GALLONS, VOLUME_FLUID_OUNCE, + TEMP_CELSIUS, TEMP_FAHRENHEIT, LENGTH_MILES, LENGTH_KILOMETERS, + PRESSURE_PA, PRESSURE_PSI, VOLUME_LITERS, VOLUME_GALLONS, MASS_GRAMS, MASS_KILOGRAMS, MASS_OUNCES, MASS_POUNDS, - CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH, MASS, VOLUME, - TEMPERATURE, UNIT_NOT_RECOGNIZED_TEMPLATE) + CONF_UNIT_SYSTEM_METRIC, CONF_UNIT_SYSTEM_IMPERIAL, LENGTH, MASS, PRESSURE, + VOLUME, TEMPERATURE, UNIT_NOT_RECOGNIZED_TEMPLATE) from homeassistant.util import temperature as temperature_util from homeassistant.util import distance as distance_util +from homeassistant.util import pressure as pressure_util from homeassistant.util import volume as volume_util _LOGGER = logging.getLogger(__name__) -LENGTH_UNITS = [ - LENGTH_MILES, - LENGTH_YARD, - LENGTH_FEET, - LENGTH_INCHES, - LENGTH_KILOMETERS, - LENGTH_METERS, - LENGTH_CENTIMETERS, -] +LENGTH_UNITS = distance_util.VALID_UNITS MASS_UNITS = [ MASS_POUNDS, @@ -34,12 +26,9 @@ MASS_UNITS = [ MASS_GRAMS, ] -VOLUME_UNITS = [ - VOLUME_GALLONS, - VOLUME_FLUID_OUNCE, - VOLUME_LITERS, - VOLUME_MILLILITERS, -] +PRESSURE_UNITS = pressure_util.VALID_UNITS + +VOLUME_UNITS = volume_util.VALID_UNITS TEMPERATURE_UNITS = [ TEMP_FAHRENHEIT, @@ -57,6 +46,8 @@ def is_valid_unit(unit: str, unit_type: str) -> bool: units = MASS_UNITS elif unit_type == VOLUME: units = VOLUME_UNITS + elif unit_type == PRESSURE: + units = PRESSURE_UNITS else: return False @@ -67,7 +58,7 @@ class UnitSystem: """A container for units of measure.""" def __init__(self, name: str, temperature: str, length: str, - volume: str, mass: str) -> None: + volume: str, mass: str, pressure: str) -> None: """Initialize the unit system object.""" errors = \ ', '.join(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit, unit_type) @@ -75,7 +66,8 @@ class UnitSystem: (temperature, TEMPERATURE), (length, LENGTH), (volume, VOLUME), - (mass, MASS), ] + (mass, MASS), + (pressure, PRESSURE), ] if not is_valid_unit(unit, unit_type)) # type: str if errors: @@ -85,6 +77,7 @@ class UnitSystem: self.temperature_unit = temperature self.length_unit = length self.mass_unit = mass + self.pressure_unit = pressure self.volume_unit = volume @property @@ -109,6 +102,14 @@ class UnitSystem: return distance_util.convert(length, from_unit, self.length_unit) + def pressure(self, pressure: Optional[float], from_unit: str) -> float: + """Convert the given pressure to this unit system.""" + if not isinstance(pressure, Number): + raise TypeError('{} is not a numeric value.'.format(str(pressure))) + + return pressure_util.convert(pressure, from_unit, + self.pressure_unit) + def volume(self, volume: Optional[float], from_unit: str) -> float: """Convert the given volume to this unit system.""" if not isinstance(volume, Number): @@ -121,13 +122,16 @@ class UnitSystem: return { LENGTH: self.length_unit, MASS: self.mass_unit, + PRESSURE: self.pressure_unit, TEMPERATURE: self.temperature_unit, VOLUME: self.volume_unit } METRIC_SYSTEM = UnitSystem(CONF_UNIT_SYSTEM_METRIC, TEMP_CELSIUS, - LENGTH_KILOMETERS, VOLUME_LITERS, MASS_GRAMS) + LENGTH_KILOMETERS, VOLUME_LITERS, MASS_GRAMS, + PRESSURE_PA) IMPERIAL_SYSTEM = UnitSystem(CONF_UNIT_SYSTEM_IMPERIAL, TEMP_FAHRENHEIT, - LENGTH_MILES, VOLUME_GALLONS, MASS_POUNDS) + LENGTH_MILES, VOLUME_GALLONS, MASS_POUNDS, + PRESSURE_PSI) diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 3febd4037ad..73fe36af26d 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -15,6 +15,7 @@ from homeassistant.const import ( LENGTH_METERS, TEMP_CELSIUS, MASS_GRAMS, + PRESSURE_PA, VOLUME_LITERS, MATCH_ALL, ) @@ -33,7 +34,7 @@ class TestHelpersTemplate(unittest.TestCase): self.hass = get_test_home_assistant() self.hass.config.units = UnitSystem('custom', TEMP_CELSIUS, LENGTH_METERS, VOLUME_LITERS, - MASS_GRAMS) + MASS_GRAMS, PRESSURE_PA) # pylint: disable=invalid-name def tearDown(self): diff --git a/tests/util/test_pressure.py b/tests/util/test_pressure.py new file mode 100644 index 00000000000..a3e6efb3754 --- /dev/null +++ b/tests/util/test_pressure.py @@ -0,0 +1,66 @@ +"""Test homeassistant pressure utility functions.""" +import unittest +import pytest + +from homeassistant.const import (PRESSURE_PA, PRESSURE_HPA, PRESSURE_MBAR, + PRESSURE_INHG, PRESSURE_PSI) +import homeassistant.util.pressure as pressure_util + +INVALID_SYMBOL = 'bob' +VALID_SYMBOL = PRESSURE_PA + + +class TestPressureUtil(unittest.TestCase): + """Test the pressure utility functions.""" + + def test_convert_same_unit(self): + """Test conversion from any unit to same unit.""" + assert pressure_util.convert(2, PRESSURE_PA, PRESSURE_PA) == 2 + assert pressure_util.convert(3, PRESSURE_HPA, PRESSURE_HPA) == 3 + assert pressure_util.convert(4, PRESSURE_MBAR, PRESSURE_MBAR) == 4 + assert pressure_util.convert(5, PRESSURE_INHG, PRESSURE_INHG) == 5 + + def test_convert_invalid_unit(self): + """Test exception is thrown for invalid units.""" + with pytest.raises(ValueError): + pressure_util.convert(5, INVALID_SYMBOL, VALID_SYMBOL) + + with pytest.raises(ValueError): + pressure_util.convert(5, VALID_SYMBOL, INVALID_SYMBOL) + + def test_convert_nonnumeric_value(self): + """Test exception is thrown for nonnumeric type.""" + with pytest.raises(TypeError): + pressure_util.convert('a', PRESSURE_HPA, PRESSURE_INHG) + + def test_convert_from_hpascals(self): + """Test conversion from hPA to other units.""" + hpascals = 1000 + self.assertAlmostEqual( + pressure_util.convert(hpascals, PRESSURE_HPA, PRESSURE_PSI), + 14.5037743897) + self.assertAlmostEqual( + pressure_util.convert(hpascals, PRESSURE_HPA, PRESSURE_INHG), + 29.5299801647) + self.assertAlmostEqual( + pressure_util.convert(hpascals, PRESSURE_HPA, PRESSURE_PA), + 100000) + self.assertAlmostEqual( + pressure_util.convert(hpascals, PRESSURE_HPA, PRESSURE_MBAR), + 1000) + + def test_convert_from_inhg(self): + """Test conversion from inHg to other units.""" + inhg = 30 + self.assertAlmostEqual( + pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_PSI), + 14.7346266155) + self.assertAlmostEqual( + pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_HPA), + 1015.9167) + self.assertAlmostEqual( + pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_PA), + 101591.67) + self.assertAlmostEqual( + pressure_util.convert(inhg, PRESSURE_INHG, PRESSURE_MBAR), + 1015.9167) diff --git a/tests/util/test_unit_system.py b/tests/util/test_unit_system.py index 31b2d49b4ec..533ce3c0a15 100644 --- a/tests/util/test_unit_system.py +++ b/tests/util/test_unit_system.py @@ -10,10 +10,12 @@ from homeassistant.const import ( LENGTH_METERS, LENGTH_KILOMETERS, MASS_GRAMS, + PRESSURE_PA, VOLUME_LITERS, TEMP_CELSIUS, LENGTH, MASS, + PRESSURE, TEMPERATURE, VOLUME ) @@ -30,19 +32,23 @@ class TestUnitSystem(unittest.TestCase): """Test errors are raised when invalid units are passed in.""" with pytest.raises(ValueError): UnitSystem(SYSTEM_NAME, INVALID_UNIT, LENGTH_METERS, VOLUME_LITERS, - MASS_GRAMS) + MASS_GRAMS, PRESSURE_PA) with pytest.raises(ValueError): UnitSystem(SYSTEM_NAME, TEMP_CELSIUS, INVALID_UNIT, VOLUME_LITERS, - MASS_GRAMS) + MASS_GRAMS, PRESSURE_PA) with pytest.raises(ValueError): UnitSystem(SYSTEM_NAME, TEMP_CELSIUS, LENGTH_METERS, INVALID_UNIT, - MASS_GRAMS) + MASS_GRAMS, PRESSURE_PA) with pytest.raises(ValueError): UnitSystem(SYSTEM_NAME, TEMP_CELSIUS, LENGTH_METERS, VOLUME_LITERS, - INVALID_UNIT) + INVALID_UNIT, PRESSURE_PA) + + with pytest.raises(ValueError): + UnitSystem(SYSTEM_NAME, TEMP_CELSIUS, LENGTH_METERS, VOLUME_LITERS, + MASS_GRAMS, INVALID_UNIT) def test_invalid_value(self): """Test no conversion happens if value is non-numeric.""" @@ -50,6 +56,10 @@ class TestUnitSystem(unittest.TestCase): METRIC_SYSTEM.length('25a', LENGTH_KILOMETERS) with pytest.raises(TypeError): METRIC_SYSTEM.temperature('50K', TEMP_CELSIUS) + with pytest.raises(TypeError): + METRIC_SYSTEM.volume('50L', VOLUME_LITERS) + with pytest.raises(TypeError): + METRIC_SYSTEM.pressure('50Pa', PRESSURE_PA) def test_as_dict(self): """Test that the as_dict() method returns the expected dictionary.""" @@ -57,7 +67,8 @@ class TestUnitSystem(unittest.TestCase): LENGTH: LENGTH_KILOMETERS, TEMPERATURE: TEMP_CELSIUS, VOLUME: VOLUME_LITERS, - MASS: MASS_GRAMS + MASS: MASS_GRAMS, + PRESSURE: PRESSURE_PA } assert expected == METRIC_SYSTEM.as_dict() @@ -108,12 +119,39 @@ class TestUnitSystem(unittest.TestCase): assert 3.106855 == \ IMPERIAL_SYSTEM.length(5, METRIC_SYSTEM.length_unit) + def test_pressure_same_unit(self): + """Test no conversion happens if to unit is same as from unit.""" + assert 5 == \ + METRIC_SYSTEM.pressure(5, METRIC_SYSTEM.pressure_unit) + + def test_pressure_unknown_unit(self): + """Test no conversion happens if unknown unit.""" + with pytest.raises(ValueError): + METRIC_SYSTEM.pressure(5, 'K') + + def test_pressure_to_metric(self): + """Test pressure conversion to metric system.""" + assert 25 == \ + METRIC_SYSTEM.pressure(25, METRIC_SYSTEM.pressure_unit) + self.assertAlmostEqual( + METRIC_SYSTEM.pressure(14.7, IMPERIAL_SYSTEM.pressure_unit), + 101352.932, places=1) + + def test_pressure_to_imperial(self): + """Test pressure conversion to imperial system.""" + assert 77 == \ + IMPERIAL_SYSTEM.pressure(77, IMPERIAL_SYSTEM.pressure_unit) + self.assertAlmostEqual( + IMPERIAL_SYSTEM.pressure(101352.932, METRIC_SYSTEM.pressure_unit), + 14.7, places=4) + def test_properties(self): """Test the unit properties are returned as expected.""" assert LENGTH_KILOMETERS == METRIC_SYSTEM.length_unit assert TEMP_CELSIUS == METRIC_SYSTEM.temperature_unit assert MASS_GRAMS == METRIC_SYSTEM.mass_unit assert VOLUME_LITERS == METRIC_SYSTEM.volume_unit + assert PRESSURE_PA == METRIC_SYSTEM.pressure_unit def test_is_metric(self): """Test the is metric flag."""