2015-11-30 16:00:28 +00:00
|
|
|
"""
|
|
|
|
homeassistant.components.sensor.yr
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Yr.no weather service.
|
|
|
|
|
|
|
|
Configuration:
|
|
|
|
|
|
|
|
Will show a symbol for the current weather as default:
|
|
|
|
sensor:
|
|
|
|
platform: yr
|
|
|
|
|
|
|
|
Will show temperatue and wind direction:
|
|
|
|
sensor:
|
|
|
|
platform: yr
|
|
|
|
monitored_conditions:
|
|
|
|
- temperature
|
|
|
|
- windDirection
|
|
|
|
|
2015-12-01 11:46:08 +00:00
|
|
|
Will show all available sensors:
|
|
|
|
sensor:
|
|
|
|
platform: yr
|
|
|
|
monitored_conditions:
|
|
|
|
- temperature
|
|
|
|
- symbol
|
|
|
|
- precipitation
|
|
|
|
- windSpeed
|
|
|
|
- pressure
|
|
|
|
- windDirection
|
|
|
|
- humidity
|
|
|
|
- fog
|
|
|
|
- cloudiness
|
|
|
|
- lowClouds
|
|
|
|
- mediumClouds
|
|
|
|
- highClouds
|
|
|
|
- dewpointTemperature
|
|
|
|
|
2015-11-30 16:00:28 +00:00
|
|
|
"""
|
|
|
|
import logging
|
|
|
|
import datetime
|
|
|
|
import urllib.request
|
2015-12-21 09:29:12 +00:00
|
|
|
import requests
|
2015-11-30 16:00:28 +00:00
|
|
|
|
|
|
|
from homeassistant.const import ATTR_ENTITY_PICTURE
|
|
|
|
from homeassistant.helpers.entity import Entity
|
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2015-12-01 11:57:08 +00:00
|
|
|
|
2015-12-01 12:24:32 +00:00
|
|
|
REQUIREMENTS = ['xmltodict', 'astral==0.8.1']
|
|
|
|
|
2015-11-30 16:00:28 +00:00
|
|
|
# Sensor types are defined like so:
|
|
|
|
SENSOR_TYPES = {
|
|
|
|
'symbol': ['Symbol', ''],
|
2015-12-01 12:31:55 +00:00
|
|
|
'precipitation': ['Condition', 'mm'],
|
2015-11-30 16:00:28 +00:00
|
|
|
'temperature': ['Temperature', '°C'],
|
|
|
|
'windSpeed': ['Wind speed', 'm/s'],
|
|
|
|
'pressure': ['Pressure', 'hPa'],
|
|
|
|
'windDirection': ['Wind direction', '°'],
|
2015-12-01 12:31:55 +00:00
|
|
|
'humidity': ['Humidity', '%'],
|
2015-11-30 16:00:28 +00:00
|
|
|
'fog': ['Fog', '%'],
|
|
|
|
'cloudiness': ['Cloudiness', '%'],
|
|
|
|
'lowClouds': ['Low clouds', '%'],
|
|
|
|
'mediumClouds': ['Medium clouds', '%'],
|
|
|
|
'highClouds': ['High clouds', '%'],
|
|
|
|
'dewpointTemperature': ['Dewpoint temperature', '°C'],
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
|
|
|
""" Get the yr.no sensor. """
|
|
|
|
|
|
|
|
if None in (hass.config.latitude, hass.config.longitude):
|
|
|
|
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
|
|
|
return False
|
|
|
|
|
|
|
|
from astral import Location, GoogleGeocoder
|
|
|
|
location = Location(('', '', hass.config.latitude, hass.config.longitude,
|
|
|
|
hass.config.time_zone, 0))
|
|
|
|
|
|
|
|
google = GoogleGeocoder()
|
|
|
|
try:
|
|
|
|
google._get_elevation(location) # pylint: disable=protected-access
|
|
|
|
_LOGGER.info(
|
|
|
|
'Retrieved elevation from Google: %s', location.elevation)
|
|
|
|
elevation = location.elevation
|
|
|
|
except urllib.error.URLError:
|
|
|
|
# If no internet connection available etc.
|
|
|
|
elevation = 0
|
|
|
|
|
|
|
|
coordinates = dict(lat=hass.config.latitude,
|
|
|
|
lon=hass.config.longitude, msl=elevation)
|
|
|
|
|
2015-12-04 14:05:23 +00:00
|
|
|
weather = YrData(coordinates)
|
|
|
|
|
2015-11-30 16:00:28 +00:00
|
|
|
dev = []
|
|
|
|
if 'monitored_conditions' in config:
|
|
|
|
for variable in config['monitored_conditions']:
|
|
|
|
if variable not in SENSOR_TYPES:
|
|
|
|
_LOGGER.error('Sensor type: "%s" does not exist', variable)
|
|
|
|
else:
|
2015-12-04 14:05:23 +00:00
|
|
|
dev.append(YrSensor(variable, weather))
|
2015-11-30 16:00:28 +00:00
|
|
|
|
2015-12-02 12:04:23 +00:00
|
|
|
# add symbol as default sensor
|
2015-11-30 16:00:28 +00:00
|
|
|
if len(dev) == 0:
|
2015-12-04 14:05:23 +00:00
|
|
|
dev.append(YrSensor("symbol", weather))
|
2015-11-30 16:00:28 +00:00
|
|
|
add_devices(dev)
|
|
|
|
|
|
|
|
|
|
|
|
# pylint: disable=too-many-instance-attributes
|
|
|
|
class YrSensor(Entity):
|
|
|
|
""" Implements an Yr.no sensor. """
|
|
|
|
|
2015-12-04 14:05:23 +00:00
|
|
|
def __init__(self, sensor_type, weather):
|
2015-12-21 09:42:42 +00:00
|
|
|
self.client_name = 'yr'
|
2015-11-30 16:00:28 +00:00
|
|
|
self._name = SENSOR_TYPES[sensor_type][0]
|
|
|
|
self.type = sensor_type
|
|
|
|
self._state = None
|
2015-12-04 14:05:23 +00:00
|
|
|
self._weather = weather
|
2015-12-01 11:46:08 +00:00
|
|
|
self._info = ''
|
2015-11-30 16:00:28 +00:00
|
|
|
self._unit_of_measurement = SENSOR_TYPES[self.type][1]
|
2015-12-01 11:46:08 +00:00
|
|
|
self._update = datetime.datetime.fromtimestamp(0)
|
2015-11-30 16:00:28 +00:00
|
|
|
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
2015-12-21 09:42:42 +00:00
|
|
|
return '{} {}'.format(self.client_name, self._name)
|
2015-11-30 16:00:28 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def state(self):
|
|
|
|
""" Returns the state of the device. """
|
|
|
|
return self._state
|
|
|
|
|
|
|
|
@property
|
|
|
|
def state_attributes(self):
|
|
|
|
""" Returns state attributes. """
|
|
|
|
data = {}
|
2015-12-21 09:33:19 +00:00
|
|
|
data[''] = "Weather forecast from yr.no, delivered by the"\
|
2015-11-30 16:00:28 +00:00
|
|
|
" Norwegian Meteorological Institute and the NRK"
|
|
|
|
if self.type == 'symbol':
|
|
|
|
symbol_nr = self._state
|
|
|
|
data[ATTR_ENTITY_PICTURE] = "http://api.met.no/weatherapi/weathericon/1.1/" \
|
|
|
|
"?symbol=" + str(symbol_nr) + \
|
|
|
|
";content_type=image/png"
|
2015-12-04 14:10:26 +00:00
|
|
|
|
2015-11-30 16:00:28 +00:00
|
|
|
return data
|
|
|
|
|
|
|
|
@property
|
|
|
|
def unit_of_measurement(self):
|
|
|
|
""" Unit of measurement of this entity, if any. """
|
|
|
|
return self._unit_of_measurement
|
|
|
|
|
2015-12-01 11:46:08 +00:00
|
|
|
@property
|
|
|
|
def should_poll(self):
|
|
|
|
""" Return True if entity has to be polled for state. """
|
|
|
|
return True
|
|
|
|
|
|
|
|
# pylint: disable=too-many-branches, too-many-return-statements
|
2015-11-30 16:00:28 +00:00
|
|
|
def update(self):
|
|
|
|
""" Gets the latest data from yr.no and updates the states. """
|
2015-12-01 12:40:26 +00:00
|
|
|
|
2015-12-04 14:05:23 +00:00
|
|
|
self._weather.update()
|
|
|
|
now = datetime.datetime.now()
|
2015-12-02 12:04:23 +00:00
|
|
|
# check if data should be updated
|
2015-12-21 09:29:12 +00:00
|
|
|
if now <= self._update:
|
|
|
|
return
|
|
|
|
|
|
|
|
time_data = self._weather.data['product']['time']
|
|
|
|
|
|
|
|
# pylint: disable=consider-using-enumerate
|
|
|
|
# find sensor
|
|
|
|
for k in range(len(time_data)):
|
|
|
|
valid_from = datetime.datetime.strptime(time_data[k]['@from'],
|
|
|
|
"%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
valid_to = datetime.datetime.strptime(time_data[k]['@to'],
|
|
|
|
"%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
self._update = valid_to
|
|
|
|
self._info = "Forecast between " + time_data[k]['@from'] \
|
|
|
|
+ " and " + time_data[k]['@to'] + ". "
|
|
|
|
|
|
|
|
temp_data = time_data[k]['location']
|
|
|
|
if self.type not in temp_data and now >= valid_to:
|
|
|
|
continue
|
|
|
|
if self.type == 'precipitation' and valid_from < now:
|
|
|
|
self._state = temp_data[self.type]['@value']
|
|
|
|
return
|
|
|
|
elif self.type == 'symbol' and valid_from < now:
|
|
|
|
self._state = temp_data[self.type]['@number']
|
|
|
|
return
|
|
|
|
elif self.type == 'temperature':
|
|
|
|
self._state = temp_data[self.type]['@value']
|
|
|
|
return
|
|
|
|
elif self.type == 'windSpeed':
|
|
|
|
self._state = temp_data[self.type]['@mps']
|
|
|
|
return
|
|
|
|
elif self.type == 'pressure':
|
|
|
|
self._state = temp_data[self.type]['@value']
|
|
|
|
return
|
|
|
|
elif self.type == 'windDirection':
|
|
|
|
self._state = float(temp_data[self.type]['@deg'])
|
|
|
|
return
|
|
|
|
elif self.type == 'humidity':
|
|
|
|
self._state = temp_data[self.type]['@value']
|
|
|
|
return
|
|
|
|
elif self.type == 'fog':
|
|
|
|
self._state = temp_data[self.type]['@percent']
|
|
|
|
return
|
|
|
|
elif self.type == 'cloudiness':
|
|
|
|
self._state = temp_data[self.type]['@percent']
|
|
|
|
return
|
|
|
|
elif self.type == 'lowClouds':
|
|
|
|
self._state = temp_data[self.type]['@percent']
|
|
|
|
return
|
|
|
|
elif self.type == 'mediumClouds':
|
|
|
|
self._state = temp_data[self.type]['@percent']
|
|
|
|
return
|
|
|
|
elif self.type == 'highClouds':
|
|
|
|
self._state = temp_data[self.type]['@percent']
|
|
|
|
return
|
|
|
|
elif self.type == 'dewpointTemperature':
|
|
|
|
self._state = temp_data[self.type]['@value']
|
|
|
|
return
|
2015-12-04 14:05:23 +00:00
|
|
|
|
|
|
|
|
|
|
|
# pylint: disable=too-few-public-methods
|
|
|
|
class YrData(object):
|
|
|
|
""" Gets the latest data and updates the states. """
|
|
|
|
|
|
|
|
def __init__(self, coordinates):
|
|
|
|
self._url = 'http://api.yr.no/weatherapi/locationforecast/1.9/?' \
|
|
|
|
'lat={lat};lon={lon};msl={msl}'.format(**coordinates)
|
|
|
|
|
|
|
|
self._nextrun = datetime.datetime.fromtimestamp(0)
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
def update(self):
|
|
|
|
""" Gets the latest data from yr.no """
|
|
|
|
now = datetime.datetime.now()
|
|
|
|
# check if new will be available
|
2015-12-21 09:29:12 +00:00
|
|
|
if now <= self._nextrun:
|
|
|
|
return
|
|
|
|
try:
|
|
|
|
response = requests.get(self._url)
|
|
|
|
except requests.RequestException:
|
|
|
|
return
|
|
|
|
if response.status_code != 200:
|
|
|
|
return
|
|
|
|
data = response.text
|
|
|
|
|
|
|
|
import xmltodict
|
|
|
|
self.data = xmltodict.parse(data)['weatherdata']
|
|
|
|
model = self.data['meta']['model']
|
|
|
|
if '@nextrun' not in model:
|
|
|
|
model = model[0]
|
|
|
|
self._nextrun = datetime.datetime.strptime(model['@nextrun'],
|
|
|
|
"%Y-%m-%dT%H:%M:%SZ")
|