Fix and improvment of Swiss Hydrological Data component (#17166)

* Fix and improvment of Swiss Hydrological Data component

* changed component to get data from a REST API rather than from crawling the website

* fixed several issues and lint errors

* Fix and improvment of Swiss Hydrological Data component

* Minor changes

- Simplify the sensor configuration (expose value as attributes rather than sensor)
- Make the setup fail if station is not available
- Add unique ID
- Prepare for config flow
This commit is contained in:
bouni 2018-11-11 17:48:44 +01:00 committed by Fabian Affolter
parent 02cc6a2f9a
commit 372470f52a
2 changed files with 108 additions and 117 deletions

View File

@ -4,145 +4,160 @@ Support for hydrological data from the Federal Office for the Environment FOEN.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.swiss_hydrological_data/ https://home-assistant.io/components/sensor.swiss_hydrological_data/
""" """
import logging
from datetime import timedelta from datetime import timedelta
import logging
import voluptuous as vol import voluptuous as vol
import requests
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS
TEMP_CELSIUS, CONF_NAME, STATE_UNKNOWN, ATTR_ATTRIBUTION)
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
REQUIREMENTS = ['xmltodict==0.11.0'] REQUIREMENTS = ['swisshydrodata==0.0.3']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
_RESOURCE = 'http://www.hydrodata.ch/xml/SMS.xml'
ATTRIBUTION = "Data provided by the Swiss Federal Office for the " \
"Environment FOEN"
ATTR_DELTA_24H = 'delta-24h'
ATTR_MAX_1H = 'max-1h'
ATTR_MAX_24H = 'max-24h'
ATTR_MEAN_1H = 'mean-1h'
ATTR_MEAN_24H = 'mean-24h'
ATTR_MIN_1H = 'min-1h'
ATTR_MIN_24H = 'min-24h'
ATTR_PREVIOUS_24H = 'previous-24h'
ATTR_STATION = 'station'
ATTR_STATION_UPDATE = 'station_update'
ATTR_WATER_BODY = 'water_body'
ATTR_WATER_BODY_TYPE = 'water_body_type'
CONF_STATION = 'station' CONF_STATION = 'station'
CONF_ATTRIBUTION = "Data provided by the Swiss Federal Office for the " \
"Environment FOEN"
DEFAULT_NAME = 'Water temperature' MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
ICON = 'mdi:cup-water' SENSOR_DISCHARGE = 'discharge'
SENSOR_LEVEL = 'level'
SENSOR_TEMPERATURE = 'temperature'
ATTR_LOCATION = 'location' CONDITIONS = {
ATTR_UPDATE = 'update' SENSOR_DISCHARGE: 'mdi:waves',
ATTR_DISCHARGE = 'discharge' SENSOR_LEVEL: 'mdi:zodiac-aquarius',
ATTR_WATERLEVEL = 'level' SENSOR_TEMPERATURE: 'mdi:oil-temperature',
ATTR_DISCHARGE_MEAN = 'discharge_mean' }
ATTR_WATERLEVEL_MEAN = 'level_mean'
ATTR_TEMPERATURE_MEAN = 'temperature_mean'
ATTR_DISCHARGE_MAX = 'discharge_max'
ATTR_WATERLEVEL_MAX = 'level_max'
ATTR_TEMPERATURE_MAX = 'temperature_max'
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) CONDITION_DETAILS = [
ATTR_DELTA_24H,
ATTR_MAX_1H,
ATTR_MAX_24H,
ATTR_MEAN_1H,
ATTR_MEAN_24H,
ATTR_MIN_1H,
ATTR_MIN_24H,
ATTR_PREVIOUS_24H,
]
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_STATION): vol.Coerce(int), vol.Required(CONF_STATION): vol.Coerce(int),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS, default=[SENSOR_TEMPERATURE]):
vol.All(cv.ensure_list, [vol.In(CONDITIONS)]),
}) })
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Swiss hydrological sensor.""" """Set up the Swiss hydrological sensor."""
import xmltodict
name = config.get(CONF_NAME)
station = config.get(CONF_STATION) station = config.get(CONF_STATION)
monitored_conditions = config.get(CONF_MONITORED_CONDITIONS)
try: hydro_data = HydrologicalData(station)
response = requests.get(_RESOURCE, timeout=5) hydro_data.update()
if any(str(station) == location.get('@StrNr') for location in
xmltodict.parse(response.text)['AKT_Data']['MesPar']) is False:
_LOGGER.error("The given station does not exist: %s", station)
return False
except requests.exceptions.ConnectionError:
_LOGGER.error("The URL is not accessible")
return False
data = HydrologicalData(station) if hydro_data.data is None:
add_entities([SwissHydrologicalDataSensor(name, data)], True) _LOGGER.error("The station doesn't exists: %s", station)
return
entities = []
for condition in monitored_conditions:
entities.append(
SwissHydrologicalDataSensor(hydro_data, station, condition))
add_entities(entities, True)
class SwissHydrologicalDataSensor(Entity): class SwissHydrologicalDataSensor(Entity):
"""Implementation of an Swiss hydrological sensor.""" """Implementation of a Swiss hydrological sensor."""
def __init__(self, name, data): def __init__(self, hydro_data, station, condition):
"""Initialize the sensor.""" """Initialize the Swiss hydrological sensor."""
self.data = data self.hydro_data = hydro_data
self._name = name self._condition = condition
self._unit_of_measurement = TEMP_CELSIUS self._data = self._state = self._unit_of_measurement = None
self._state = None self._icon = CONDITIONS[condition]
self._station = station
@property @property
def name(self): def name(self):
"""Return the name of the sensor.""" """Return the name of the sensor."""
return self._name return "{0} {1}".format(self._data['water-body-name'], self._condition)
@property
def unique_id(self) -> str:
"""Return a unique, friendly identifier for this entity."""
return '{0}_{1}'.format(self._station, self._condition)
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any.""" """Return the unit of measurement of this entity, if any."""
if self._state is not STATE_UNKNOWN: if self._state is not None:
return self._unit_of_measurement return self.hydro_data.data['parameters'][self._condition]['unit']
return None return None
@property @property
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
try: if isinstance(self._state, (int, float)):
return round(float(self._state), 1) return round(self._state, 2)
except ValueError: return None
return STATE_UNKNOWN
@property @property
def device_state_attributes(self): def device_state_attributes(self):
"""Return the state attributes.""" """Return the device state attributes."""
attributes = {} attrs = {}
if self.data.measurings is not None:
if '02' in self.data.measurings:
attributes[ATTR_WATERLEVEL] = self.data.measurings['02'][
'current']
attributes[ATTR_WATERLEVEL_MEAN] = self.data.measurings['02'][
'mean']
attributes[ATTR_WATERLEVEL_MAX] = self.data.measurings['02'][
'max']
if '03' in self.data.measurings:
attributes[ATTR_TEMPERATURE_MEAN] = self.data.measurings['03'][
'mean']
attributes[ATTR_TEMPERATURE_MAX] = self.data.measurings['03'][
'max']
if '10' in self.data.measurings:
attributes[ATTR_DISCHARGE] = self.data.measurings['10'][
'current']
attributes[ATTR_DISCHARGE_MEAN] = self.data.measurings['10'][
'current']
attributes[ATTR_DISCHARGE_MAX] = self.data.measurings['10'][
'max']
attributes[ATTR_LOCATION] = self.data.measurings['location'] if not self._data:
attributes[ATTR_UPDATE] = self.data.measurings['update_time'] attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
attributes[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION return attrs
return attributes
attrs[ATTR_WATER_BODY_TYPE] = self._data['water-body-type']
attrs[ATTR_STATION] = self._data['name']
attrs[ATTR_STATION_UPDATE] = \
self._data['parameters'][self._condition]['datetime']
attrs[ATTR_ATTRIBUTION] = ATTRIBUTION
for entry in CONDITION_DETAILS:
attrs[entry.replace('-', '_')] = \
self._data['parameters'][self._condition][entry]
return attrs
@property @property
def icon(self): def icon(self):
"""Icon to use in the frontend, if any.""" """Icon to use in the frontend."""
return ICON return self._icon
def update(self): def update(self):
"""Get the latest data and update the states.""" """Get the latest data and update the state."""
self.data.update() self.hydro_data.update()
if self.data.measurings is not None: self._data = self.hydro_data.data
if '03' not in self.data.measurings:
self._state = STATE_UNKNOWN if self._data is None:
else: self._state = None
self._state = self.data.measurings['03']['current'] else:
self._state = self._data['parameters'][self._condition]['value']
class HydrologicalData: class HydrologicalData:
@ -151,38 +166,12 @@ class HydrologicalData:
def __init__(self, station): def __init__(self, station):
"""Initialize the data object.""" """Initialize the data object."""
self.station = station self.station = station
self.measurings = None self.data = None
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
"""Get the latest data from hydrodata.ch.""" """Get the latest data."""
import xmltodict from swisshydrodata import SwissHydroData
details = {} shd = SwissHydroData()
try: self.data = shd.get_station(self.station)
response = requests.get(_RESOURCE, timeout=5)
except requests.exceptions.ConnectionError:
_LOGGER.error("Unable to retrieve data from %s", _RESOURCE)
try:
stations = xmltodict.parse(response.text)['AKT_Data']['MesPar']
# Water level: Typ="02", temperature: Typ="03", discharge: Typ="10"
for station in stations:
if str(self.station) != station.get('@StrNr'):
continue
for data in ['02', '03', '10']:
if data != station.get('@Typ'):
continue
values = station.get('Wert')
if values is not None:
details[data] = {
'current': values[0],
'max': list(values[4].items())[1][1],
'mean': list(values[3].items())[1][1]}
details['location'] = station.get('Name')
details['update_time'] = station.get('Zeit')
self.measurings = details
except AttributeError:
self.measurings = None

View File

@ -1474,6 +1474,9 @@ suds-passworddigest-homeassistant==0.1.2a0.dev0
# homeassistant.components.camera.onvif # homeassistant.components.camera.onvif
suds-py3==1.3.3.0 suds-py3==1.3.3.0
# homeassistant.components.sensor.swiss_hydrological_data
swisshydrodata==0.0.3
# homeassistant.components.tahoma # homeassistant.components.tahoma
tahoma-api==0.0.13 tahoma-api==0.0.13
@ -1606,7 +1609,6 @@ xknx==0.9.1
# homeassistant.components.media_player.bluesound # homeassistant.components.media_player.bluesound
# homeassistant.components.sensor.startca # homeassistant.components.sensor.startca
# homeassistant.components.sensor.swiss_hydrological_data
# homeassistant.components.sensor.ted5000 # homeassistant.components.sensor.ted5000
# homeassistant.components.sensor.yr # homeassistant.components.sensor.yr
# homeassistant.components.sensor.zestimate # homeassistant.components.sensor.zestimate