"""Support for UK Met Office weather service.""" from datetime import timedelta import logging import datapoint as dp import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, CONF_NAME, LENGTH_KILOMETERS, SPEED_MILES_PER_HOUR, TEMP_CELSIUS, UNIT_PERCENTAGE, UV_INDEX, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) ATTR_LAST_UPDATE = "last_update" ATTR_SENSOR_ID = "sensor_id" ATTR_SITE_ID = "site_id" ATTR_SITE_NAME = "site_name" 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": [], } DEFAULT_NAME = "Met Office" VISIBILITY_CLASSES = { "VP": "<1", "PO": "1-4", "MO": "4-10", "GO": "10-20", "VG": "20-40", "EX": ">40", } MIN_TIME_BETWEEN_UPDATES = 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", SPEED_MILES_PER_HOUR], "wind_direction": ["Wind Direction", None], "wind_gust": ["Wind Gust", SPEED_MILES_PER_HOUR], "visibility": ["Visibility", None], "visibility_distance": ["Visibility Distance", LENGTH_KILOMETERS], "uv": ["UV", UV_INDEX], "precipitation": ["Probability of Precipitation", UNIT_PERCENTAGE], "humidity": ["Humidity", UNIT_PERCENTAGE], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_MONITORED_CONDITIONS, default=[]): vol.All( cv.ensure_list, [vol.In(SENSOR_TYPES)] ), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Inclusive( CONF_LATITUDE, "coordinates", "Latitude and longitude must exist together" ): cv.latitude, vol.Inclusive( CONF_LONGITUDE, "coordinates", "Latitude and longitude must exist together" ): cv.longitude, } ) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Met Office sensor platform.""" api_key = config.get(CONF_API_KEY) latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) name = config.get(CONF_NAME) datapoint = dp.connection(api_key=api_key) if None in (latitude, longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") return 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 if not site: _LOGGER.error("Unable to get nearest Met Office forecast site") return data = MetOfficeCurrentData(hass, datapoint, site) data.update() if data.data is None: return sensors = [] for variable in config[CONF_MONITORED_CONDITIONS]: sensors.append(MetOfficeCurrentSensor(site, data, variable, name)) add_entities(sensors, True) class MetOfficeCurrentSensor(Entity): """Implementation of a Met Office current sensor.""" def __init__(self, site, data, condition, name): """Initialize the sensor.""" self._condition = condition self.data = data self._name = name self.site = site @property def name(self): """Return the name of the sensor.""" return f"{self._name} {SENSOR_TYPES[self._condition][0]}" @property def state(self): """Return the state of the sensor.""" if self._condition == "visibility_distance" and hasattr( self.data.data, "visibility" ): return VISIBILITY_CLASSES.get(self.data.data.visibility.value) if hasattr(self.data.data, self._condition): 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] return variable.value return None @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[ATTR_ATTRIBUTION] = ATTRIBUTION attr[ATTR_LAST_UPDATE] = self.data.data.date attr[ATTR_SENSOR_ID] = self._condition attr[ATTR_SITE_ID] = self.site.id attr[ATTR_SITE_NAME] = self.site.name return attr def update(self): """Update current conditions.""" self.data.update() class MetOfficeCurrentData: """Get data from Datapoint.""" def __init__(self, hass, datapoint, site): """Initialize the data object.""" self._datapoint = datapoint self._site = site self.data = None @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Datapoint.""" 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