From 2d6b09586db3d9d8c3c082ea117c9edc22e93107 Mon Sep 17 00:00:00 2001 From: Jacob Tomlinson Date: Fri, 31 Mar 2017 21:03:27 +0100 Subject: [PATCH] Added Met Office weather and sensor components (#6742) * Added Met Office weather and sensor components * Removed unnecessary dependancy * Generated requirements * Fix time interval * Updated coverage * Some review changes * Allow user to specify lat and lon in component * Added missing import * Fixed unit * Fixed import indent * Updated condition to use CONDITION_CLASSES --- .coveragerc | 4 +- homeassistant/components/sensor/metoffice.py | 176 ++++++++++++++++++ homeassistant/components/weather/metoffice.py | 122 ++++++++++++ requirements_all.txt | 4 + 4 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/sensor/metoffice.py create mode 100644 homeassistant/components/weather/metoffice.py diff --git a/.coveragerc b/.coveragerc index 52b88444dc6..939565ed43a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -147,7 +147,7 @@ omit = homeassistant/components/tado.py homeassistant/components/*/tado.py - + homeassistant/components/alarm_control_panel/alarmdotcom.py homeassistant/components/alarm_control_panel/concord232.py homeassistant/components/alarm_control_panel/nx584.py @@ -359,6 +359,7 @@ omit = homeassistant/components/sensor/linux_battery.py homeassistant/components/sensor/loopenergy.py homeassistant/components/sensor/lyft.py + homeassistant/components/sensor/metoffice.py homeassistant/components/sensor/miflora.py homeassistant/components/sensor/modem_callerid.py homeassistant/components/sensor/mqtt_room.py @@ -434,6 +435,7 @@ omit = homeassistant/components/tts/picotts.py homeassistant/components/upnp.py homeassistant/components/weather/bom.py + homeassistant/components/weather/metoffice.py homeassistant/components/weather/openweathermap.py homeassistant/components/weather/zamg.py homeassistant/components/zeroconf.py diff --git a/homeassistant/components/sensor/metoffice.py b/homeassistant/components/sensor/metoffice.py new file mode 100644 index 00000000000..725fca1db44 --- /dev/null +++ b/homeassistant/components/sensor/metoffice.py @@ -0,0 +1,176 @@ +""" +Support for UK Met Office weather service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.metoffice/ +""" + +import logging +from datetime import timedelta + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS, TEMP_CELSIUS, STATE_UNKNOWN, CONF_NAME, + ATTR_ATTRIBUTION, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE) +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['datapoint==0.4.3'] + +CONF_ATTRIBUTION = "Data provided by the Met Office" + +CONDITION_CLASSES = { + 'cloudy': ["7", "8"], + 'fog': ["5", "6"], + 'hail': ["19", "20", "21"], + 'lightning': ["30"], + 'lightning-rainy': ["28", "29"], + 'partlycloudy': ["2", "3"], + 'pouring': ["13", "14", "15"], + 'rainy': ["9", "10", "11", "12"], + 'snowy': ["22", "23", "24", "25", "26", "27"], + 'snowy-rainy': ["16", "17", "18"], + 'sunny': ["0", "1"], + 'windy': [], + 'windy-variant': [], + 'exceptional': [], +} + +SCAN_INTERVAL = timedelta(minutes=35) + +# Sensor types are defined like: Name, units +SENSOR_TYPES = { + 'name': ['Station Name', None], + 'weather': ['Weather', None], + 'temperature': ['Temperature', TEMP_CELSIUS], + 'feels_like_temperature': ['Feels Like Temperature', TEMP_CELSIUS], + 'wind_speed': ['Wind Speed', 'm/s'], + 'wind_direction': ['Wind Direction', None], + 'wind_gust': ['Wind Gust', 'm/s'], + 'visibility': ['Visibility', 'km'], + 'uv': ['UV', None], + 'precipitation': ['Probability of Precipitation', '%'], + 'humidity': ['Humidity', '%'] +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=None): cv.string, + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_MONITORED_CONDITIONS, default=[]): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup the sensor platform.""" + import datapoint as dp + datapoint = dp.connection(api_key=config.get(CONF_API_KEY)) + + 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 False + + try: + site = datapoint.get_nearest_site(latitude=latitude, + longitude=longitude) + except dp.exceptions.APIException as err: + _LOGGER.error("Received error from Met Office Datapoint: %s", err) + return False + + if not site: + _LOGGER.error("Unable to get nearest Met Office forecast site") + return False + + # Get data + data = MetOfficeCurrentData(hass, datapoint, site) + try: + data.update() + except (ValueError, dp.exceptions.APIException) as err: + _LOGGER.error("Received error from Met Office Datapoint: %s", err) + return False + + # Add + add_devices([MetOfficeCurrentSensor(site, data, variable) + for variable in config[CONF_MONITORED_CONDITIONS]]) + return True + + +class MetOfficeCurrentSensor(Entity): + """Implementation of a Met Office current sensor.""" + + def __init__(self, site, data, condition): + """Initialize the sensor.""" + self.site = site + self.data = data + self._condition = condition + + @property + def name(self): + """Return the name of the sensor.""" + return 'Met Office {}'.format(SENSOR_TYPES[self._condition][0]) + + @property + def state(self): + """Return the state of the sensor.""" + if self._condition in self.data.data.__dict__.keys(): + variable = getattr(self.data.data, self._condition) + if self._condition == "weather": + return [k for k, v in CONDITION_CLASSES.items() if + self.data.data.weather.value in v][0] + else: + return variable.value + else: + return STATE_UNKNOWN + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return SENSOR_TYPES[self._condition][1] + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attr = {} + attr['Sensor Id'] = self._condition + attr['Site Id'] = self.site.id + attr['Site Name'] = self.site.name + attr['Last Update'] = self.data.lastupdate + attr[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION + return attr + + def update(self): + """Update current conditions.""" + self.data.update() + + +class MetOfficeCurrentData(object): + """Get data from Datapoint.""" + + def __init__(self, hass, datapoint, site): + """Initialize the data object.""" + self._hass = hass + self._datapoint = datapoint + self._site = site + self.data = None + + @Throttle(SCAN_INTERVAL) + def update(self): + """Get the latest data from Datapoint.""" + import datapoint as dp + + try: + forecast = self._datapoint.get_forecast_for_site(self._site.id, + "3hourly") + self.data = forecast.now() + except (ValueError, dp.exceptions.APIException) as err: + _LOGGER.error("Check Met Office %s", err.args) + self.data = None + raise diff --git a/homeassistant/components/weather/metoffice.py b/homeassistant/components/weather/metoffice.py new file mode 100644 index 00000000000..50bbb84faa7 --- /dev/null +++ b/homeassistant/components/weather/metoffice.py @@ -0,0 +1,122 @@ +""" +Support for UK Met Office weather service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/weather.metoffice/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.weather import WeatherEntity, PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_NAME, TEMP_CELSIUS, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE) +from homeassistant.helpers import config_validation as cv +# Reuse data and API logic from the sensor implementation +from homeassistant.components.sensor.metoffice import \ + MetOfficeCurrentData, CONF_ATTRIBUTION, CONDITION_CLASSES + +_LOGGER = logging.getLogger(__name__) + +REQUIREMENTS = ['datapoint==0.4.3'] + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME): cv.string, + vol.Required(CONF_API_KEY): cv.string, +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Met Office weather platform.""" + import datapoint as dp + datapoint = dp.connection(api_key=config.get(CONF_API_KEY)) + + 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 False + + try: + site = datapoint.get_nearest_site(latitude=latitude, + longitude=longitude) + except dp.exceptions.APIException as err: + _LOGGER.error("Received error from Met Office Datapoint: %s", err) + return False + + if not site: + _LOGGER.error("Unable to get nearest Met Office forecast site") + return False + + # Get data + data = MetOfficeCurrentData(hass, datapoint, site) + try: + data.update() + except (ValueError, dp.exceptions.APIException) as err: + _LOGGER.error("Received error from Met Office Datapoint: %s", err) + return False + add_devices([MetOfficeWeather(site, data, config.get(CONF_NAME))], + True) + return True + + +class MetOfficeWeather(WeatherEntity): + """Implementation of a Met Office weather condition.""" + + def __init__(self, site, data, config): + """Initialise the platform with a data instance and site.""" + self.data = data + self.site = site + + def update(self): + """Update current conditions.""" + self.data.update() + + @property + def name(self): + """Return the name of the sensor.""" + return 'Met Office ({})'.format(self.site.name) + + @property + def condition(self): + """Return the current condition.""" + return [k for k, v in CONDITION_CLASSES.items() if + self.data.data.weather.value in v][0] + + # Now implement the WeatherEntity interface + + @property + def temperature(self): + """Return the platform temperature.""" + return self.data.data.temperature.value + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def pressure(self): + """Return the mean sea-level pressure.""" + return None + + @property + def humidity(self): + """Return the relative humidity.""" + return self.data.data.humidity.value + + @property + def wind_speed(self): + """Return the wind speed.""" + return self.data.data.wind_speed.value + + @property + def wind_bearing(self): + """Return the wind bearing.""" + return self.data.data.wind_direction.value + + @property + def attribution(self): + """Return the attribution.""" + return CONF_ATTRIBUTION diff --git a/requirements_all.txt b/requirements_all.txt index 678f1d8c464..9caba7d75d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -105,6 +105,10 @@ colorlog>2.1,<3 # homeassistant.components.binary_sensor.concord232 concord232==0.14 +# homeassistant.components.sensor.metoffice +# homeassistant.components.weather.metoffice +datapoint==0.4.3 + # homeassistant.components.light.decora # decora==0.3