""" Support for Buienradar.nl weather service. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.buienradar/ """ import asyncio from datetime import timedelta import logging import async_timeout import aiohttp import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_LATITUDE, CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_CELSIUS) from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import ( async_track_point_in_utc_time) from homeassistant.util import dt as dt_util REQUIREMENTS = ['buienradar==0.4'] _LOGGER = logging.getLogger(__name__) # Supported sensor types: SENSOR_TYPES = { 'stationname': ['Stationname', None, None], 'symbol': ['Symbol', None, None], 'humidity': ['Humidity', '%', 'mdi:water-percent'], 'temperature': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'], 'groundtemperature': ['Ground Temperature', TEMP_CELSIUS, 'mdi:thermometer'], 'windspeed': ['Wind speed', 'm/s', 'mdi:weather-windy'], 'windforce': ['Wind force', 'Bft', 'mdi:weather-windy'], 'winddirection': ['Wind direction', None, 'mdi:compass-outline'], 'windazimuth': ['Wind direction azimuth', '°', 'mdi:compass-outline'], 'pressure': ['Pressure', 'hPa', 'mdi:gauge'], 'visibility': ['Visibility', 'm', None], 'windgust': ['Wind gust', 'm/s', 'mdi:weather-windy'], 'precipitation': ['Precipitation', 'mm/h', 'mdi:weather-pouring'], 'irradiance': ['Irradiance', 'W/m2', 'mdi:sunglasses'], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_MONITORED_CONDITIONS, default=['symbol', 'temperature']): vol.All( cv.ensure_list, vol.Length(min=1), [vol.In(SENSOR_TYPES.keys())]), vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, }) @asyncio.coroutine def async_setup_platform(hass, config, async_add_devices, discovery_info=None): """Setup the buienradar sensor.""" 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 HomeAssistant config") return False coordinates = {CONF_LATITUDE: float(latitude), CONF_LONGITUDE: float(longitude)} dev = [] for sensor_type in config[CONF_MONITORED_CONDITIONS]: dev.append(BrSensor(sensor_type, config.get(CONF_NAME, 'br'))) async_add_devices(dev) data = BrData(hass, coordinates, dev) # schedule the first update in 1 minute from now: _LOGGER.debug("Start running....") yield from data.schedule_update(1) class BrSensor(Entity): """Representation of an Buienradar sensor.""" def __init__(self, sensor_type, client_name): """Initialize the sensor.""" self.client_name = client_name self._name = SENSOR_TYPES[sensor_type][0] self.type = sensor_type self._state = None self._unit_of_measurement = SENSOR_TYPES[self.type][1] self._entity_picture = None self._attribution = None self._stationname = None def load_data(self, data): """Load the sensor with relevant data.""" # Find sensor from buienradar.buienradar import (ATTRIBUTION, IMAGE, STATIONNAME, SYMBOL) self._attribution = data.get(ATTRIBUTION) self._stationname = data.get(STATIONNAME) if self.type == SYMBOL: # update weather symbol & status text new_state = data.get(self.type) img = data.get(IMAGE) # pylint: disable=protected-access if new_state != self._state or img != self._entity_picture: self._state = new_state self._entity_picture = img return True else: # update all other sensors new_state = data.get(self.type) # pylint: disable=protected-access if new_state != self._state: self._state = new_state return True @property def attribution(self): """Return the attribution.""" return self._attribution @property def name(self): """Return the name of the sensor.""" return '{} {}'.format(self.client_name, self._name) @property def state(self): """Return the state of the device.""" return self._state @property def should_poll(self): # pylint: disable=no-self-use """No polling needed.""" return False @property def entity_picture(self): """Weather symbol if type is symbol.""" from buienradar.buienradar import SYMBOL if self.type != SYMBOL: return None else: return self._entity_picture @property def device_state_attributes(self): """Return the state attributes.""" return { ATTR_ATTRIBUTION: self._attribution, SENSOR_TYPES['stationname'][0]: self._stationname, } @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement @property def icon(self): """Return possible sensor specific icon.""" return SENSOR_TYPES[self.type][2] class BrData(object): """Get the latest data and updates the states.""" def __init__(self, hass, coordinates, devices): """Initialize the data object.""" self.devices = devices self.data = {} self.hass = hass self.coordinates = coordinates @asyncio.coroutine def update_devices(self): """Update all devices/sensors.""" if self.devices: tasks = [] # Update all devices for dev in self.devices: if dev.load_data(self.data): tasks.append(dev.async_update_ha_state()) if tasks: yield from asyncio.wait(tasks, loop=self.hass.loop) @asyncio.coroutine def schedule_update(self, minute=1): """Schedule an update after minute minutes.""" _LOGGER.debug("Scheduling next update in %s minutes.", minute) nxt = dt_util.utcnow() + timedelta(minutes=minute) async_track_point_in_utc_time(self.hass, self.async_update, nxt) @asyncio.coroutine def get_data(self, url): """Load xmpl data from specified url.""" from buienradar.buienradar import (CONTENT, MESSAGE, STATUS_CODE, SUCCESS) _LOGGER.debug("Calling url: %s...", url) result = {SUCCESS: False, MESSAGE: None} resp = None try: websession = async_get_clientsession(self.hass) with async_timeout.timeout(10, loop=self.hass.loop): resp = yield from websession.get(url) result[SUCCESS] = (resp.status == 200) result[STATUS_CODE] = resp.status result[CONTENT] = yield from resp.text() return result except (asyncio.TimeoutError, aiohttp.ClientError) as err: result[MESSAGE] = "%s" % err return result finally: if resp is not None: yield from resp.release() @asyncio.coroutine def async_update(self, *_): """Update the data from buienradar.""" from buienradar.buienradar import (parse_data, CONTENT, DATA, MESSAGE, STATUS_CODE, SUCCESS) result = yield from self.get_data('http://xml.buienradar.nl') if result.get(SUCCESS, False) is False: result = yield from self.get_data('http://api.buienradar.nl') if result.get(SUCCESS): result = parse_data(result.get(CONTENT), latitude=self.coordinates[CONF_LATITUDE], longitude=self.coordinates[CONF_LONGITUDE]) if result.get(SUCCESS): self.data = result.get(DATA) yield from self.update_devices() yield from self.schedule_update(10) else: yield from self.schedule_update(2) else: # unable to get the data _LOGGER.warning("Unable to retrieve data from Buienradar." "(Msg: %s, status: %s,)", result.get(MESSAGE), result.get(STATUS_CODE),) # schedule new call yield from self.schedule_update(2) @property def attribution(self): """Return the attribution.""" from buienradar.buienradar import ATTRIBUTION return self.data.get(ATTRIBUTION) @property def stationname(self): """Return the name of the selected weatherstation.""" from buienradar.buienradar import STATIONNAME return self.data.get(STATIONNAME) @property def condition(self): """Return the condition.""" from buienradar.buienradar import SYMBOL return self.data.get(SYMBOL) @property def temperature(self): """Return the temperature, or None.""" from buienradar.buienradar import TEMPERATURE try: return float(self.data.get(TEMPERATURE)) except (ValueError, TypeError): return None @property def pressure(self): """Return the pressure, or None.""" from buienradar.buienradar import PRESSURE try: return float(self.data.get(PRESSURE)) except (ValueError, TypeError): return None @property def humidity(self): """Return the humidity, or None.""" from buienradar.buienradar import HUMIDITY try: return int(self.data.get(HUMIDITY)) except (ValueError, TypeError): return None @property def wind_speed(self): """Return the windspeed, or None.""" from buienradar.buienradar import WINDSPEED try: return float(self.data.get(WINDSPEED)) except (ValueError, TypeError): return None @property def wind_bearing(self): """Return the wind bearing, or None.""" from buienradar.buienradar import WINDDIRECTION try: return int(self.data.get(WINDDIRECTION)) except (ValueError, TypeError): return None @property def forecast(self): """Return the forecast data.""" from buienradar.buienradar import FORECAST return self.data.get(FORECAST)