Refactor Forecast.io (#2217)

* Refactor Forecast.io

* Some more refactoring and code review workoff

* Dict switch refactor

* CamelCase for data lookup

* Fixing unit_of_measure update

* Better default return for unit_of_measurement

* Test fix
This commit is contained in:
Edward Romano 2016-06-13 21:54:49 -04:00 committed by Paulus Schoutsen
parent ab48010d14
commit 8e839be938
2 changed files with 93 additions and 101 deletions

View File

@ -6,8 +6,12 @@ https://home-assistant.io/components/sensor.forecast/
""" """
import logging import logging
from datetime import timedelta from datetime import timedelta
from requests.exceptions import ConnectionError as ConnectError, \
HTTPError, Timeout
from homeassistant.components.sensor import DOMAIN
from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS from homeassistant.const import CONF_API_KEY, TEMP_CELSIUS
from homeassistant.helpers import validate_config
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -48,21 +52,12 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120)
def setup_platform(hass, config, add_devices, discovery_info=None): def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the Forecast.io sensor.""" """Setup the Forecast.io sensor."""
import forecastio # Validate the configuration
if None in (hass.config.latitude, hass.config.longitude): if None in (hass.config.latitude, hass.config.longitude):
_LOGGER.error("Latitude or longitude not set in Home Assistant config") _LOGGER.error("Latitude or longitude not set in Home Assistant config")
return False return False
elif not validate_config({DOMAIN: config},
try: {DOMAIN: [CONF_API_KEY]}, _LOGGER):
forecast = forecastio.load_forecast(config.get(CONF_API_KEY, None),
hass.config.latitude,
hass.config.longitude)
forecast.currently()
except ValueError:
_LOGGER.error(
"Connection error "
"Please check your settings for Forecast.io.")
return False return False
if 'units' in config: if 'units' in config:
@ -72,43 +67,41 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
else: else:
units = 'us' units = 'us'
data = ForeCastData(config.get(CONF_API_KEY, None), # Create a data fetcher to support all of the configured sensors. Then make
hass.config.latitude, # the first call to init the data and confirm we can connect.
hass.config.longitude, try:
units) forecast_data = ForeCastData(
config.get(CONF_API_KEY, None), hass.config.latitude,
hass.config.longitude, units)
forecast_data.update_currently()
except ValueError as error:
_LOGGER.error(error)
return False
dev = [] # Initialize and add all of the sensors.
sensors = []
for variable in config['monitored_conditions']: for variable in config['monitored_conditions']:
if variable not in SENSOR_TYPES: if variable in SENSOR_TYPES:
_LOGGER.error('Sensor type: "%s" does not exist', variable) sensors.append(ForeCastSensor(forecast_data, variable))
else: else:
dev.append(ForeCastSensor(data, variable)) _LOGGER.error('Sensor type: "%s" does not exist', variable)
add_devices(dev) add_devices(sensors)
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class ForeCastSensor(Entity): class ForeCastSensor(Entity):
"""Implementation of a Forecast.io sensor.""" """Implementation of a Forecast.io sensor."""
def __init__(self, weather_data, sensor_type): def __init__(self, forecast_data, sensor_type):
"""Initialize the sensor.""" """Initialize the sensor."""
self.client_name = 'Weather' self.client_name = 'Weather'
self._name = SENSOR_TYPES[sensor_type][0] self._name = SENSOR_TYPES[sensor_type][0]
self.forecast_client = weather_data self.forecast_data = forecast_data
self.type = sensor_type self.type = sensor_type
self._state = None self._state = None
self._unit_system = self.forecast_client.unit_system self._unit_of_measurement = None
if self._unit_system == 'si':
self._unit_of_measurement = SENSOR_TYPES[self.type][1]
elif self._unit_system == 'us':
self._unit_of_measurement = SENSOR_TYPES[self.type][2]
elif self._unit_system == 'ca':
self._unit_of_measurement = SENSOR_TYPES[self.type][3]
elif self._unit_system == 'uk':
self._unit_of_measurement = SENSOR_TYPES[self.type][4]
elif self._unit_system == 'uk2':
self._unit_of_measurement = SENSOR_TYPES[self.type][5]
self.update() self.update()
@property @property
@ -129,75 +122,72 @@ class ForeCastSensor(Entity):
@property @property
def unit_system(self): def unit_system(self):
"""Return the unit system of this entity.""" """Return the unit system of this entity."""
return self._unit_system return self.forecast_data.unit_system
def update_unit_of_measurement(self):
"""Update units based on unit system."""
unit_index = {
'si': 1,
'us': 2,
'ca': 3,
'uk': 4,
'uk2': 5
}.get(self.unit_system, 1)
self._unit_of_measurement = SENSOR_TYPES[self.type][unit_index]
# pylint: disable=too-many-branches,too-many-statements
def update(self): def update(self):
"""Get the latest data from Forecast.io and updates the states.""" """Get the latest data from Forecast.io and updates the states."""
import forecastio # Call the API for new forecast data. Each sensor will re-trigger this
# same exact call, but thats fine. We cache results for a short period
# of time to prevent hitting API limits. Note that forecast.io will
# charge users for too many calls in 1 day, so take care when updating.
self.forecast_data.update()
self.update_unit_of_measurement()
self.forecast_client.update() if self.type == 'minutely_summary':
self.forecast_data.update_minutely()
minutely = self.forecast_data.data_minutely
self._state = getattr(minutely, 'summary', '')
elif self.type == 'hourly_summary':
self.forecast_data.update_hourly()
hourly = self.forecast_data.data_hourly
self._state = getattr(hourly, 'summary', '')
elif self.type == 'daily_summary':
self.forecast_data.update_daily()
daily = self.forecast_data.data_daily
self._state = getattr(daily, 'summary', '')
else:
self.forecast_data.update_currently()
currently = self.forecast_data.data_currently
self._state = self.get_currently_state(currently)
try: def get_currently_state(self, data):
if self.type == 'minutely_summary': """
self.forecast_client.update_minutely() Helper function that returns a new state based on the type.
self._state = self.forecast_client.data_minutely.summary
return
elif self.type == 'hourly_summary': If the sensor type is unknown, the current state is returned.
self.forecast_client.update_hourly() """
self._state = self.forecast_client.data_hourly.summary lookup_type = convert_to_camel(self.type)
return state = getattr(data, lookup_type, 0)
elif self.type == 'daily_summary': # Some state data needs to be rounded to whole values or converted to
self.forecast_client.update_daily() # percentages
self._state = self.forecast_client.data_daily.summary if self.type in ['precip_probability', 'cloud_cover', 'humidity']:
return return round(state * 100, 1)
elif (self.type in ['dew_point', 'temperature', 'apparent_temperature',
'pressure', 'ozone']):
return round(state, 1)
return state
except forecastio.utils.PropertyUnavailable:
return
self.forecast_client.update_currently() def convert_to_camel(data):
data = self.forecast_client.data_currently """
Convert snake case (foo_bar_bat) to camel case (fooBarBat).
try: This is not pythonic, but needed for certain situations
if self.type == 'summary': """
self._state = data.summary components = data.split('_')
elif self.type == 'icon': return components[0] + "".join(x.title() for x in components[1:])
self._state = data.icon
elif self.type == 'nearest_storm_distance':
self._state = data.nearestStormDistance
elif self.type == 'nearest_storm_bearing':
self._state = data.nearestStormBearing
elif self.type == 'precip_intensity':
self._state = data.precipIntensity
elif self.type == 'precip_type':
self._state = data.precipType
elif self.type == 'precip_probability':
self._state = round(data.precipProbability * 100, 1)
elif self.type == 'dew_point':
self._state = round(data.dewPoint, 1)
elif self.type == 'temperature':
self._state = round(data.temperature, 1)
elif self.type == 'apparent_temperature':
self._state = round(data.apparentTemperature, 1)
elif self.type == 'wind_speed':
self._state = data.windSpeed
elif self.type == 'wind_bearing':
self._state = data.windBearing
elif self.type == 'cloud_cover':
self._state = round(data.cloudCover * 100, 1)
elif self.type == 'humidity':
self._state = round(data.humidity * 100, 1)
elif self.type == 'pressure':
self._state = round(data.pressure, 1)
elif self.type == 'visibility':
self._state = data.visibility
elif self.type == 'ozone':
self._state = round(data.ozone, 1)
except forecastio.utils.PropertyUnavailable:
pass
class ForeCastData(object): class ForeCastData(object):
@ -226,10 +216,13 @@ class ForeCastData(object):
"""Get the latest data from Forecast.io.""" """Get the latest data from Forecast.io."""
import forecastio import forecastio
self.data = forecastio.load_forecast(self._api_key, try:
self.latitude, self.data = forecastio.load_forecast(self._api_key,
self.longitude, self.latitude,
units=self.units) self.longitude,
units=self.units)
except (ConnectError, HTTPError, Timeout, ValueError) as error:
raise ValueError("Unable to init Forecast.io. - %s", error)
self.unit_system = self.data.json['flags']['units'] self.unit_system = self.data.json['flags']['units']
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)

View File

@ -7,7 +7,6 @@ from unittest.mock import MagicMock, patch
import forecastio import forecastio
import httpretty import httpretty
import pytest
from requests.exceptions import HTTPError from requests.exceptions import HTTPError
from homeassistant.components.sensor import forecast from homeassistant.components.sensor import forecast
@ -46,8 +45,8 @@ class TestForecastSetup(unittest.TestCase):
msg = '400 Client Error: Bad Request for url: {}'.format(url) msg = '400 Client Error: Bad Request for url: {}'.format(url)
mock_get_forecast.side_effect = HTTPError(msg,) mock_get_forecast.side_effect = HTTPError(msg,)
with pytest.raises(HTTPError): response = forecast.setup_platform(self.hass, self.config, MagicMock())
forecast.setup_platform(self.hass, self.config, MagicMock()) self.assertFalse(response)
@httpretty.activate @httpretty.activate
@patch('forecastio.api.get_forecast', wraps=forecastio.api.get_forecast) @patch('forecastio.api.get_forecast', wraps=forecastio.api.get_forecast)
@ -74,4 +73,4 @@ class TestForecastSetup(unittest.TestCase):
forecast.setup_platform(self.hass, self.config, MagicMock()) forecast.setup_platform(self.hass, self.config, MagicMock())
self.assertTrue(mock_get_forecast.called) self.assertTrue(mock_get_forecast.called)
self.assertEqual(mock_get_forecast.call_count, 2) self.assertEqual(mock_get_forecast.call_count, 1)