diff --git a/homeassistant/components/weather/ipma.py b/homeassistant/components/weather/ipma.py new file mode 100644 index 0000000000000000000000000000000000000000..ef4f1b349d72a5c3d87d3d0a81fdb45cf04f3992 --- /dev/null +++ b/homeassistant/components/weather/ipma.py @@ -0,0 +1,172 @@ +""" +Support for IPMA weather service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/weather.ipma/ +""" +import logging +from datetime import timedelta + +import async_timeout +import voluptuous as vol + +from homeassistant.components.weather import ( + WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, + ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) +from homeassistant.const import \ + CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers import config_validation as cv +from homeassistant.util import Throttle + +REQUIREMENTS = ['pyipma==1.1.3'] + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = 'Instituto Português do Mar e Atmosfera' + +ATTR_WEATHER_DESCRIPTION = "description" + +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) + +CONDITION_CLASSES = { + 'cloudy': [4, 5, 24, 25, 27], + 'fog': [16, 17, 26], + 'hail': [21, 22], + 'lightning': [19], + 'lightning-rainy': [20, 23], + 'partlycloudy': [2, 3], + 'pouring': [8, 11], + 'rainy': [6, 7, 9, 10, 12, 13, 14, 15], + 'snowy': [18], + 'snowy-rainy': [], + 'sunny': [1], + 'windy': [], + 'windy-variant': [], + 'exceptional': [], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, +}) + + +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the ipma platform.""" + latitude = config.get(CONF_LATITUDE, hass.config.latitude) + longitude = config.get(CONF_LONGITUDE, hass.config.longitude) + + if None in (latitude, longitude): + _LOGGER.error("Latitude or longitude not set in Home Assistant config") + return + + from pyipma import Station + + websession = async_get_clientsession(hass) + with async_timeout.timeout(10, loop=hass.loop): + station = await Station.get(websession, float(latitude), + float(longitude)) + + _LOGGER.debug("Initializing ipma weather: coordinates %s, %s", + latitude, longitude) + + async_add_devices([IPMAWeather(station, config)], True) + + +class IPMAWeather(WeatherEntity): + """Representation of a weather condition.""" + + def __init__(self, station, config): + """Initialise the platform with a data instance and station name.""" + self._station_name = config.get(CONF_NAME, station.local) + self._station = station + self._condition = None + self._forecast = None + self._description = None + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): + """Update Condition and Forecast.""" + with async_timeout.timeout(10, loop=self.hass.loop): + self._condition = await self._station.observation() + self._forecast = await self._station.forecast() + self._description = self._forecast[0].description + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def name(self): + """Return the name of the station.""" + return self._station_name + + @property + def condition(self): + """Return the current condition.""" + return next((k for k, v in CONDITION_CLASSES.items() + if self._forecast[0].idWeatherType in v), None) + + @property + def temperature(self): + """Return the current temperature.""" + return self._condition.temperature + + @property + def pressure(self): + """Return the current pressure.""" + return self._condition.pressure + + @property + def humidity(self): + """Return the name of the sensor.""" + return self._condition.humidity + + @property + def wind_speed(self): + """Return the current windspeed.""" + return self._condition.windspeed + + @property + def wind_bearing(self): + """Return the current wind bearing (degrees).""" + return self._condition.winddirection + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def forecast(self): + """Return the forecast array.""" + if self._forecast: + fcdata_out = [] + for data_in in self._forecast: + data_out = {} + data_out[ATTR_FORECAST_TIME] = data_in.forecastDate + data_out[ATTR_FORECAST_CONDITION] =\ + next((k for k, v in CONDITION_CLASSES.items() + if int(data_in.idWeatherType) in v), None) + data_out[ATTR_FORECAST_TEMP_LOW] = data_in.tMin + data_out[ATTR_FORECAST_TEMP] = data_in.tMax + data_out[ATTR_FORECAST_PRECIPITATION] = data_in.precipitaProb + + fcdata_out.append(data_out) + + return fcdata_out + + @property + def device_state_attributes(self): + """Return the state attributes.""" + data = dict() + + if self._description: + data[ATTR_WEATHER_DESCRIPTION] = self._description + + return data diff --git a/requirements_all.txt b/requirements_all.txt index fd2bb5b4f5a1e54cbfe7f833fa39c50dd1805d97..fcd1e3726e94056344988816f481544b16f8bbf1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -834,6 +834,9 @@ pyialarm==0.2 # homeassistant.components.device_tracker.icloud pyicloud==0.9.1 +# homeassistant.components.weather.ipma +pyipma==1.1.3 + # homeassistant.components.sensor.irish_rail_transport pyirishrail==0.0.2 diff --git a/tests/components/weather/test_ipma.py b/tests/components/weather/test_ipma.py new file mode 100644 index 0000000000000000000000000000000000000000..7df6166a2b6a18a73386ffcaaf9783f500c0be85 --- /dev/null +++ b/tests/components/weather/test_ipma.py @@ -0,0 +1,85 @@ +"""The tests for the IPMA weather component.""" +import unittest +from unittest.mock import patch +from collections import namedtuple + +from homeassistant.components import weather +from homeassistant.components.weather import ( + ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED) +from homeassistant.util.unit_system import METRIC_SYSTEM +from homeassistant.setup import setup_component + +from tests.common import get_test_home_assistant, MockDependency + + +class MockStation(): + """Mock Station from pyipma.""" + + @classmethod + async def get(cls, websession, lat, lon): + """Mock Factory.""" + return MockStation() + + async def observation(self): + """Mock Observation.""" + Observation = namedtuple('Observation', ['temperature', 'humidity', + 'windspeed', 'winddirection', + 'precipitation', 'pressure', + 'description']) + + return Observation(18, 71.0, 3.94, 'NW', 0, 1000.0, '---') + + async def forecast(self): + """Mock Forecast.""" + Forecast = namedtuple('Forecast', ['precipitaProb', 'tMin', 'tMax', + 'predWindDir', 'idWeatherType', + 'classWindSpeed', 'longitude', + 'forecastDate', 'classPrecInt', + 'latitude', 'description']) + + return [Forecast(73.0, 13.7, 18.7, 'NW', 6, 2, -8.64, + '2018-05-31', 2, 40.61, + 'Aguaceiros, com vento Moderado de Noroeste')] + + @property + def local(self): + """Mock location.""" + return "HomeTown" + + +class TestIPMA(unittest.TestCase): + """Test the IPMA weather component.""" + + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + self.hass.config.units = METRIC_SYSTEM + self.lat = self.hass.config.latitude = 40.00 + self.lon = self.hass.config.longitude = -8.00 + + def tearDown(self): + """Stop down everything that was started.""" + self.hass.stop() + + @MockDependency("pyipma") + @patch("pyipma.Station", new=MockStation) + def test_setup(self, mock_pyipma): + """Test for successfully setting up the IPMA platform.""" + self.assertTrue(setup_component(self.hass, weather.DOMAIN, { + 'weather': { + 'name': 'HomeTown', + 'platform': 'ipma', + } + })) + + state = self.hass.states.get('weather.hometown') + self.assertEqual(state.state, 'rainy') + + data = state.attributes + self.assertEqual(data.get(ATTR_WEATHER_TEMPERATURE), 18.0) + self.assertEqual(data.get(ATTR_WEATHER_HUMIDITY), 71) + self.assertEqual(data.get(ATTR_WEATHER_PRESSURE), 1000.0) + self.assertEqual(data.get(ATTR_WEATHER_WIND_SPEED), 3.94) + self.assertEqual(data.get(ATTR_WEATHER_WIND_BEARING), 'NW') + self.assertEqual(state.attributes.get('friendly_name'), 'HomeTown')