From 549133a06246e52833e5a8cf66b8d18b9d2e2422 Mon Sep 17 00:00:00 2001 From: mjj4791 Date: Mon, 5 Jun 2017 08:48:11 +0200 Subject: [PATCH] Added buienradar sensor and weather (#7592) * Added buienradar sensor and weather * used external library for parsing * used external library for parsing * updated buienradar lib to 0.4 * Make sure you import 3rd party libraries inside methods. * Make sure you import 3rd party libraries inside methods. * clean up code; optimized * imports, sensor name and attributes * updated requirements to match imports * use asyncio for http get --- .coveragerc | 2 + homeassistant/components/sensor/buienradar.py | 327 ++++++++++++++++++ .../components/weather/buienradar.py | 119 +++++++ requirements_all.txt | 3 + 4 files changed, 451 insertions(+) create mode 100755 homeassistant/components/sensor/buienradar.py create mode 100755 homeassistant/components/weather/buienradar.py diff --git a/.coveragerc b/.coveragerc index 302a65e6e87..3d4d6fa2ebc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -375,6 +375,7 @@ omit = homeassistant/components/sensor/blockchain.py homeassistant/components/sensor/bom.py homeassistant/components/sensor/broadlink.py + homeassistant/components/sensor/buienradar.py homeassistant/components/sensor/dublin_bus_transport.py homeassistant/components/sensor/coinmarketcap.py homeassistant/components/sensor/cert_expiry.py @@ -497,6 +498,7 @@ omit = homeassistant/components/tts/picotts.py homeassistant/components/upnp.py homeassistant/components/weather/bom.py + homeassistant/components/weather/buienradar.py homeassistant/components/weather/metoffice.py homeassistant/components/weather/openweathermap.py homeassistant/components/weather/zamg.py diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py new file mode 100755 index 00000000000..4974e7c45ce --- /dev/null +++ b/homeassistant/components/sensor/buienradar.py @@ -0,0 +1,327 @@ +""" +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', '°', 'mdi:compass-outline'], + 'windazimuth': ['Wind direction azimuth', None, '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) diff --git a/homeassistant/components/weather/buienradar.py b/homeassistant/components/weather/buienradar.py new file mode 100755 index 00000000000..e46651a5e86 --- /dev/null +++ b/homeassistant/components/weather/buienradar.py @@ -0,0 +1,119 @@ +""" +Support for Buienradar.nl weather service. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/weather.buienradar/ +""" +import logging +import asyncio +from datetime import timedelta +from homeassistant.components.weather import ( + WeatherEntity, PLATFORM_SCHEMA) +from homeassistant.const import \ + CONF_NAME, TEMP_CELSIUS, 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.buienradar import ( + BrData) +from homeassistant.helpers.event import ( + async_track_time_interval) +import voluptuous as vol + +_LOGGER = logging.getLogger(__name__) + +CONF_FORECAST = 'forecast' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_LATITUDE): cv.latitude, + vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_FORECAST, default=True): cv.boolean, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the buienradar 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 False + + coordinates = {CONF_LATITUDE: float(latitude), + CONF_LONGITUDE: float(longitude)} + + # create weather data: + data = BrData(hass, coordinates, None) + # create weather device: + async_add_devices([BrWeather(data, config.get(CONF_FORECAST, True), + config.get(CONF_NAME, None))]) + + # Update weather every 10 minutes, since + # the data gets updated every 10 minutes + async_track_time_interval(hass, data.async_update, timedelta(minutes=10)) + # schedule the first update in 1 minute from now: + data.schedule_update(1) + + +class BrWeather(WeatherEntity): + """Representation of a weather condition.""" + + def __init__(self, data, forecast, stationname=None): + """Initialise the platform with a data instance and station name.""" + self._stationname = stationname + self._forecast = forecast + self._data = data + + @property + def attribution(self): + """Return the attribution.""" + return self._data.attribution + + @property + def name(self): + """Return the name of the sensor.""" + return self._stationname or 'BR {}'.format(self._data.stationname + or '(unknown station)') + + @property + def condition(self): + """Return the name of the sensor.""" + return self._data.condition + + @property + def temperature(self): + """Return the name of the sensor.""" + return self._data.temperature + + @property + def pressure(self): + """Return the name of the sensor.""" + return self._data.pressure + + @property + def humidity(self): + """Return the name of the sensor.""" + return self._data.humidity + + @property + def wind_speed(self): + """Return the name of the sensor.""" + return self._data.wind_speed + + @property + def wind_bearing(self): + """Return the name of the sensor.""" + return self._data.wind_bearing + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS + + @property + def forecast(self): + """Return the forecast.""" + if self._forecast: + return self._data.forecast diff --git a/requirements_all.txt b/requirements_all.txt index a29c56d31f2..994f9974855 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -114,6 +114,9 @@ boto3==1.4.3 # homeassistant.components.switch.broadlink broadlink==0.3 +# homeassistant.components.sensor.buienradar +buienradar==0.4 + # homeassistant.components.notify.ciscospark ciscosparkapi==0.4.2