Add sensor for reading Austrian ZAMG weather conditions (#4347)

* Add sensor for reading ZAMG weather conditions

* Add to coveragerc; Correct some doc style problems

* More doc fixes

* More doc fixes

* Lose license and whatever.

* Don't return UNKNOWN for unknown variables

* Verify that the configured station id is actually one in the data set.

Don't warn about unknown stations, this cannot happen any more as the configuration parser now checks that.
This could still happen if the data set is incomplete though ...

* Clean up imports

* Clarify comment on throttling interval

* Base zamg sensor on Entity, not WeatherEntity, and delete unused code

* Fix formatting nits from flake8

* Use ATTR_FRIENDLY_NAME, clean up imports, remove unnecessary indirection.

* Use {}.format() instead of "" %

* Re-add unit of measurement that got lost somehow

* Use guard clauses instead of if-matroshka.
Wrap requests.get() in try/except for RequestException.

* Huh, how did this happen? White space corrections...

* Add sensor for reading ZAMG weather conditions

* Add to coveragerc; Correct some doc style problems

* More doc fixes

* More doc fixes

* Verify that the configured station id is actually one in the data set.

Don't warn about unknown stations, this cannot happen any more as the configuration parser now checks that.
This could still happen if the data set is incomplete though ...

* Lose license and whatever.

* Don't return UNKNOWN for unknown variables

* Clean up imports

* Clarify comment on throttling interval

* Base zamg sensor on Entity, not WeatherEntity, and delete unused code

* Fix formatting nits from flake8

* Use ATTR_FRIENDLY_NAME, clean up imports, remove unnecessary indirection.

* Use {}.format() instead of "" %

* Re-add unit of measurement that got lost somehow

* Use guard clauses instead of if-matroshka.
Wrap requests.get() in try/except for RequestException.

* Huh, how did this happen? White space corrections...

* Precipitation actually is a float, good it rained today

* Logger needs no module visibility

* Do not name sensors with _ to be in line with the other weather sensor platforms.

* Remove manually set friendly_name

* comment format police

* Less comments

* Update zamg.py
pull/4738/merge
Martin J. Laubach 2016-12-06 02:50:50 +01:00 committed by Paulus Schoutsen
parent b3253403aa
commit 1ae8256ffd
2 changed files with 248 additions and 1 deletions

View File

@ -315,7 +315,7 @@ omit =
homeassistant/components/sensor/waqi.py
homeassistant/components/sensor/xbox_live.py
homeassistant/components/sensor/yweather.py
homeassistant/components/sensor/waqi.py
homeassistant/components/sensor/zamg.py
homeassistant/components/switch/acer_projector.py
homeassistant/components/switch/anel_pwrctrl.py
homeassistant/components/switch/arest.py

View File

@ -0,0 +1,247 @@
"""
Sensor for data from Austrian "Zentralanstalt für Meteorologie und Geodynamik".
This is a sensor for the Austrian weather service "Zentralanstalt für
Meteorologie und Geodynamik" (aka ZAMG).
The configuration should look like this:
- platform: zamg
station_id: 11035
monitored_conditions:
- temperature
- humidity
- pressure
- wind_speed
- precipitation
Recognised conditions are:
pressure (Pressure at station level)
pressure_sealevel (Pressure at Sea Level)
humidity (Humidity)
wind_speed (Wind Speed)
wind_bearing (Wind Bearing)
wind_max_speed (Top Wind Speed)
wind_max_bearing (Top Wind Bearing)
sun_last_hour (Sun Last Hour Percentage)
temperature (Temperature)
precipitation (Precipitation)
dewpoint (Dew Point)
The following stations are available in the data set:
11010 Linz/Hörsching
11012 Kremsmünster
11022 Retz
11035 Wien/Hohe Warte
11036 Wien/Schwechat
11101 Bregenz
11121 Innsbruck
11126 Patscherkofel
11130 Kufstein
11150 Salzburg
11155 Feuerkogel
11157 Aigen im Ennstal
11171 Mariazell
11190 Eisenstadt
11204 Lienz
"""
import csv
from datetime import timedelta
import logging
import requests
import voluptuous as vol
from homeassistant.components.weather import (
ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_ATTRIBUTION,
ATTR_WEATHER_PRESSURE, ATTR_WEATHER_TEMPERATURE,
ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (
CONF_MONITORED_CONDITIONS, CONF_NAME, __version__
)
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
DEFAULT_NAME = 'zamg'
ATTRIBUTION = 'Data provided by ZAMG'
# Data source only updates once per hour, so throttle to 30min to have
# reasonably recent data
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30)
CONF_STATION_ID = "station_id"
VALID_STATION_IDS = (
'11010', '11012', '11022', '11035', '11036', '11101', '11121', '11126',
'11130', '11150', '11155', '11157', '11171', '11190', '11204'
)
SENSOR_TYPES = {
ATTR_WEATHER_PRESSURE: ('Pressure', 'hPa', 'LDstat hPa', float),
'pressure_sealevel': ('Pressure at Sea Level', 'hPa', 'LDred hPa', float),
ATTR_WEATHER_HUMIDITY: ('Humidity', '%', 'RF %', int),
ATTR_WEATHER_WIND_SPEED: ('Wind Speed', 'km/h', 'WG km/h', float),
ATTR_WEATHER_WIND_BEARING: ('Wind Bearing', '°', 'WR °', int),
'wind_max_speed': ('Top Wind Speed', 'km/h', 'WSG km/h', float),
'wind_max_bearing': ('Top Wind Bearing', '°', 'WSR °', int),
'sun_last_hour': ('Sun Last Hour', '%', 'SO %', int),
ATTR_WEATHER_TEMPERATURE: ('Temperature', '°C', 'T °C', float),
'precipitation': ('Precipitation', 'l/m²', 'N l/m²', float),
'dewpoint': ('Dew Point', '°C', 'TP °C', float),
# The following probably not useful for general consumption,
# but we need them to fill in internal attributes
'station_name': ('Station Name', None, 'Name', str),
'station_elevation': ('Station Elevation', 'm', 'Höhe m', int),
'update_date': ('Update Date', None, 'Datum', str),
'update_time': ('Update Time', None, 'Zeit', str),
}
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Required(CONF_STATION_ID):
vol.All(cv.string, vol.In(VALID_STATION_IDS)),
vol.Required(CONF_MONITORED_CONDITIONS):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup platform."""
station_id = config.get(CONF_STATION_ID)
name = config.get(CONF_NAME)
logger = logging.getLogger(__name__)
probe = ZamgData(station_id=station_id, logger=logger)
sensors = [ZAMGWeather(probe, variable, name)
for variable in config[CONF_MONITORED_CONDITIONS]]
add_devices(sensors, True)
class ZAMGWeather(Entity):
"""
I am a weather wrapper for a specific station and a specific attribute.
Multiple instances (one for each condition) will refer to the same
probe, so things will only get fetched once.
"""
def __init__(self, probe, variable, name):
"""Init condition sensor."""
self.probe = probe
self.client_name = name
self.variable = variable
def update(self):
"""Delegate update to probe."""
self.probe.update()
@property
def name(self):
"""Build name of sensor."""
return '{} {}'.format(self.client_name, self.variable)
@property
def state(self):
"""Return state."""
return self.probe.get_data(self.variable)
@property
def unit_of_measurement(self):
"""Unit of measurement."""
return SENSOR_TYPES[self.variable][1]
@property
def state_attributes(self):
"""Return the state attributes."""
return {
ATTR_WEATHER_ATTRIBUTION: ATTRIBUTION,
"station": self.probe.get_data('station_name'),
"updated": "%s %s" % (self.probe.get_data('update_date'),
self.probe.get_data('update_time'))
}
class ZamgData(object):
"""
I represent weather data for a specific site.
From the web site:
Sie beinhalten neben Stationsnummer, Stationsname, Seehöhe der Station,
Messdatum und Messzeit (Lokalzeit) die meteorologischen Messwerte von
Temperatur, Taupunkt, relative Luftfeuchtigkeit, Richtung und
Geschwindigkeit des Windmittels und der Windspitze, Niederschlagssumme
der letzten Stunde, Luftdruck reduziert auf Meeresniveau und Luftdruck
auf Stationsniveau sowie die Sonnenscheindauer der letzten Stunde (in
Prozent). Die Messstationen, die diese Daten liefern, sind über das
Bundesgebiet verteilt und beinhalten alle Landeshauptstädte sowie
die wichtigsten Bergstationen.
"""
API_URL = "http://www.zamg.ac.at/ogd/"
API_FIELDS = {
v[2]: (k, v[3])
for k, v in SENSOR_TYPES.items()
}
API_HEADERS = {
'User-Agent': 'home-assistant.zamg/' + __version__,
}
def __init__(self, logger, station_id):
"""Initialize the probe."""
self._logger = logger
self._station_id = station_id
self.data = {}
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""
Update data set.
Fetch a new data set from the zamg server, parse it and
update internal state accordingly
"""
try:
response = requests.get(self.API_URL,
headers=self.API_HEADERS, timeout=15)
except requests.exceptions.RequestException:
self._logger.exception("While fetching data from server")
return
if response.status_code != 200:
self._logger.error("API call returned with status %s",
response.status_code)
return
content_type = response.headers.get('Content-Type', 'whatever')
if content_type != 'text/csv':
self._logger.error("Expected text/csv but got %s",
content_type)
return
response.encoding = 'UTF8'
content = response.text
data = (line for line in content.split('\n'))
reader = csv.DictReader(data, delimiter=';', quotechar='"')
for row in reader:
if row.get("Station", None) == self._station_id:
self.data = {
self.API_FIELDS.get(k)[0]:
self.API_FIELDS.get(k)[1](v.replace(',', '.'))
for k, v in row.items()
if v and k in self.API_FIELDS
}
break
def get_data(self, variable):
"""Generic accessor for data."""
return self.data.get(variable)