diff --git a/.coveragerc b/.coveragerc index 5931322f80b..1302ee911a3 100644 --- a/.coveragerc +++ b/.coveragerc @@ -326,6 +326,7 @@ omit = homeassistant/components/nest/* homeassistant/components/netatmo/* homeassistant/components/netgear_lte/* + homeassistant/components/meteo_france.py homeassistant/components/notify/aws_lambda.py homeassistant/components/notify/aws_sns.py homeassistant/components/notify/aws_sqs.py @@ -650,6 +651,7 @@ omit = homeassistant/components/weather/buienradar.py homeassistant/components/weather/darksky.py homeassistant/components/weather/met.py + homeassistant/components/weather/meteo_france.py homeassistant/components/weather/metoffice.py homeassistant/components/weather/openweathermap.py homeassistant/components/weather/zamg.py diff --git a/homeassistant/components/meteo_france.py b/homeassistant/components/meteo_france.py new file mode 100644 index 00000000000..fa68021d91c --- /dev/null +++ b/homeassistant/components/meteo_france.py @@ -0,0 +1,141 @@ +""" +Support for Meteo France weather forecast. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/meteo_france/ +""" +import logging +import datetime + +import voluptuous as vol + +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS, TEMP_CELSIUS) +from homeassistant.util import Throttle +from homeassistant.helpers.discovery import load_platform +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['meteofrance==0.3.4'] +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'meteo_france' +SCAN_INTERVAL = datetime.timedelta(minutes=5) +ATTRIBUTION = "Data provided by Météo-France" +CONF_CITY = 'city' +DEFAULT_WEATHER_CARD = True +DATA_METEO_FRANCE = 'data_meteo_france' + +SENSOR_TYPES = { + 'rain_chance': ['Rain chance', '%'], + 'freeze_chance': ['Freeze chance', '%'], + 'thunder_chance': ['Thunder chance', '%'], + 'snow_chance': ['Snow chance', '%'], + 'weather': ['Weather', None], + 'wind_speed': ['Wind Speed', 'km/h'], + 'next_rain': ['Next rain', 'min'], + 'temperature': ['Temperature', TEMP_CELSIUS], + 'uv': ['UV', None], +} + +CONDITION_CLASSES = { + 'clear-night': ['Nuit Claire'], + 'cloudy': ['Très nuageux'], + 'fog': ['Brume ou bancs de brouillard', + 'Brouillard', 'Brouillard givrant'], + 'hail': ['Risque de grêle'], + 'lightning': ["Risque d'orages", 'Orages'], + 'lightning-rainy': ['Pluie orageuses', 'Pluies orageuses', + 'Averses orageuses'], + 'partlycloudy': ['Ciel voilé', 'Ciel voilé nuit', 'Éclaircies'], + 'pouring': ['Pluie forte'], + 'rainy': ['Bruine / Pluie faible', 'Bruine', 'Pluie faible', + 'Pluies éparses / Rares averses', 'Pluies éparses', + 'Rares averses', 'Pluie / Averses', 'Averses', 'Pluie'], + 'snowy': ['Neige / Averses de neige', 'Neige', 'Averses de neige', + 'Neige forte', 'Quelques flocons'], + 'snowy-rainy': ['Pluie et neige', 'Pluie verglaçante'], + 'sunny': ['Ensoleillé'], + 'windy': [], + 'windy-variant': [], + 'exceptional': [], +} + + +def has_all_unique_cities(value): + """Validate that all cities are unique.""" + cities = [location[CONF_CITY] for location in value] + vol.Schema(vol.Unique())(cities) + return value + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.All(cv.ensure_list, [vol.Schema({ + vol.Required(CONF_CITY): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + })], has_all_unique_cities) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Meteo-France component.""" + hass.data[DATA_METEO_FRANCE] = {} + + for location in config[DOMAIN]: + + city = location[CONF_CITY] + + from meteofrance.client import meteofranceClient, meteofranceError + + try: + client = meteofranceClient(city) + except meteofranceError as exp: + _LOGGER.error(exp) + return + + client.need_rain_forecast = bool(CONF_MONITORED_CONDITIONS in location + and 'next_rain' in + location[CONF_MONITORED_CONDITIONS]) + + hass.data[DATA_METEO_FRANCE][city] = MeteoFranceUpdater(client) + hass.data[DATA_METEO_FRANCE][city].update() + + if CONF_MONITORED_CONDITIONS in location: + monitored_conditions = location[CONF_MONITORED_CONDITIONS] + load_platform( + hass, + 'sensor', + DOMAIN, + {CONF_CITY: city, + CONF_MONITORED_CONDITIONS: monitored_conditions}, + config) + + load_platform( + hass, + 'weather', + DOMAIN, + {CONF_CITY: city}, + config) + + return True + + +class MeteoFranceUpdater: + """Update data from Meteo-France.""" + + def __init__(self, client): + """Initialize the data object.""" + self._client = client + + def get_data(self): + """Get the latest data from Meteo-France.""" + return self._client.get_data() + + @Throttle(SCAN_INTERVAL) + def update(self): + """Get the latest data from Meteo-France.""" + from meteofrance.client import meteofranceError + try: + self._client.update() + except meteofranceError as exp: + _LOGGER.error(exp) diff --git a/homeassistant/components/sensor/meteo_france.py b/homeassistant/components/sensor/meteo_france.py index 1e18b1518a7..12933c02e81 100644 --- a/homeassistant/components/sensor/meteo_france.py +++ b/homeassistant/components/sensor/meteo_france.py @@ -4,64 +4,34 @@ Support for Meteo France raining forecast. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.meteo_france/ """ - import logging -import datetime -import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.meteo_france import (SENSOR_TYPES, + DATA_METEO_FRANCE, + CONF_CITY, + ATTRIBUTION) from homeassistant.const import ( - CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION, TEMP_CELSIUS) + CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION) from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['meteofrance==0.2.7'] _LOGGER = logging.getLogger(__name__) -CONF_ATTRIBUTION = "Data provided by Meteo-France" -CONF_POSTAL_CODE = 'postal_code' - STATE_ATTR_FORECAST = '1h rain forecast' -SCAN_INTERVAL = datetime.timedelta(minutes=5) - -SENSOR_TYPES = { - 'rain_chance': ['Rain chance', '%'], - 'freeze_chance': ['Freeze chance', '%'], - 'thunder_chance': ['Thunder chance', '%'], - 'snow_chance': ['Snow chance', '%'], - 'weather': ['Weather', None], - 'wind_speed': ['Wind Speed', 'km/h'], - 'next_rain': ['Next rain', 'min'], - 'temperature': ['Temperature', TEMP_CELSIUS], - 'uv': ['UV', None], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_POSTAL_CODE): cv.string, - vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), -}) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Meteo-France sensor.""" - postal_code = config[CONF_POSTAL_CODE] - - from meteofrance.client import meteofranceClient, meteofranceError - - try: - meteofrance_client = meteofranceClient(postal_code) - except meteofranceError as exp: - _LOGGER.error(exp) + if discovery_info is None: return - client = MeteoFranceUpdater(meteofrance_client) + city = discovery_info[CONF_CITY] + monitored_conditions = discovery_info[CONF_MONITORED_CONDITIONS] + + client = hass.data[DATA_METEO_FRANCE][city] add_entities([MeteoFranceSensor(variable, client) - for variable in config[CONF_MONITORED_CONDITIONS]], + for variable in monitored_conditions], True) @@ -96,10 +66,10 @@ class MeteoFranceSensor(Entity): }, ** self._data["next_rain_intervals"], **{ - ATTR_ATTRIBUTION: CONF_ATTRIBUTION + ATTR_ATTRIBUTION: ATTRIBUTION } } - return {ATTR_ATTRIBUTION: CONF_ATTRIBUTION} + return {ATTR_ATTRIBUTION: ATTRIBUTION} @property def unit_of_measurement(self): @@ -116,24 +86,3 @@ class MeteoFranceSensor(Entity): _LOGGER.error("No condition `%s` for location `%s`", self._condition, self._data["name"]) self._state = None - - -class MeteoFranceUpdater: - """Update data from Meteo-France.""" - - def __init__(self, client): - """Initialize the data object.""" - self._client = client - - def get_data(self): - """Get the latest data from Meteo-France.""" - return self._client.get_data() - - @Throttle(SCAN_INTERVAL) - def update(self): - """Get the latest data from Meteo-France.""" - from meteofrance.client import meteofranceError - try: - self._client.update() - except meteofranceError as exp: - _LOGGER.error(exp) diff --git a/homeassistant/components/weather/meteo_france.py b/homeassistant/components/weather/meteo_france.py new file mode 100644 index 00000000000..a9d11d198f6 --- /dev/null +++ b/homeassistant/components/weather/meteo_france.py @@ -0,0 +1,112 @@ +""" +Support for Meteo france weather service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/weather.meteo_france/ +""" +import logging +from datetime import datetime, timedelta + +from homeassistant.components.meteo_france import (DATA_METEO_FRANCE, + CONDITION_CLASSES, + CONF_CITY, + ATTRIBUTION) +from homeassistant.components.weather import ( + WeatherEntity, ATTR_FORECAST_CONDITION, + ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME) +from homeassistant.const import TEMP_CELSIUS + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Meteo-France weather platform.""" + if discovery_info is None: + return + + city = discovery_info[CONF_CITY] + + client = hass.data[DATA_METEO_FRANCE][city] + + add_entities([MeteoFranceWeather(client)], True) + + +class MeteoFranceWeather(WeatherEntity): + """Representation of a weather condition.""" + + def __init__(self, client): + """Initialise the platform with a data instance and station name.""" + self._client = client + self._data = {} + + def update(self): + """Update current conditions.""" + self._client.update() + self._data = self._client.get_data() + + @property + def name(self): + """Return the name of the sensor.""" + return self._data["name"] + + @property + def condition(self): + """Return the current condition.""" + return self.format_condition(self._data["weather"]) + + @property + def temperature(self): + """Return the platform temperature.""" + return self._data["temperature"] + + @property + def humidity(self): + """Return the platform temperature.""" + return None + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def wind_speed(self): + """Return the wind speed.""" + return self._data["wind_speed"] + + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self._data["wind_bearing"] + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def forecast(self): + """Return the forecast.""" + reftime = datetime.now().replace(hour=12, minute=00) + reftime += timedelta(hours=24) + forecast_data = [] + for key in self._data["forecast"]: + value = self._data["forecast"][key] + data_dict = { + ATTR_FORECAST_TIME: reftime.isoformat(), + ATTR_FORECAST_TEMP: int(value['max_temp']), + ATTR_FORECAST_TEMP_LOW: int(value['min_temp']), + ATTR_FORECAST_CONDITION: + self.format_condition(value["weather"]) + } + reftime = reftime + timedelta(hours=24) + forecast_data.append(data_dict) + return forecast_data + + @staticmethod + def format_condition(condition): + """Return condition from dict CONDITION_CLASSES.""" + for key, value in CONDITION_CLASSES.items(): + if condition in value: + return key + return condition diff --git a/requirements_all.txt b/requirements_all.txt index b91036fa1f6..01a2b576e5a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -681,8 +681,8 @@ mbddns==0.1.2 # homeassistant.components.notify.message_bird messagebird==1.2.0 -# homeassistant.components.sensor.meteo_france -meteofrance==0.2.7 +# homeassistant.components.meteo_france +meteofrance==0.3.4 # homeassistant.components.sensor.mfi # homeassistant.components.switch.mfi