mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
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:
parent
ab48010d14
commit
8e839be938
@ -6,8 +6,12 @@ https://home-assistant.io/components/sensor.forecast/
|
||||
"""
|
||||
import logging
|
||||
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.helpers import validate_config
|
||||
from homeassistant.helpers.entity import Entity
|
||||
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):
|
||||
"""Setup the Forecast.io sensor."""
|
||||
import forecastio
|
||||
|
||||
# Validate the configuration
|
||||
if None in (hass.config.latitude, hass.config.longitude):
|
||||
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
|
||||
return False
|
||||
|
||||
try:
|
||||
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.")
|
||||
elif not validate_config({DOMAIN: config},
|
||||
{DOMAIN: [CONF_API_KEY]}, _LOGGER):
|
||||
return False
|
||||
|
||||
if 'units' in config:
|
||||
@ -72,43 +67,41 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||
else:
|
||||
units = 'us'
|
||||
|
||||
data = ForeCastData(config.get(CONF_API_KEY, None),
|
||||
hass.config.latitude,
|
||||
hass.config.longitude,
|
||||
units)
|
||||
# Create a data fetcher to support all of the configured sensors. Then make
|
||||
# the first call to init the data and confirm we can connect.
|
||||
try:
|
||||
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']:
|
||||
if variable not in SENSOR_TYPES:
|
||||
_LOGGER.error('Sensor type: "%s" does not exist', variable)
|
||||
if variable in SENSOR_TYPES:
|
||||
sensors.append(ForeCastSensor(forecast_data, variable))
|
||||
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
|
||||
class ForeCastSensor(Entity):
|
||||
"""Implementation of a Forecast.io sensor."""
|
||||
|
||||
def __init__(self, weather_data, sensor_type):
|
||||
def __init__(self, forecast_data, sensor_type):
|
||||
"""Initialize the sensor."""
|
||||
self.client_name = 'Weather'
|
||||
self._name = SENSOR_TYPES[sensor_type][0]
|
||||
self.forecast_client = weather_data
|
||||
self.forecast_data = forecast_data
|
||||
self.type = sensor_type
|
||||
self._state = None
|
||||
self._unit_system = self.forecast_client.unit_system
|
||||
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._unit_of_measurement = None
|
||||
|
||||
self.update()
|
||||
|
||||
@property
|
||||
@ -129,75 +122,72 @@ class ForeCastSensor(Entity):
|
||||
@property
|
||||
def unit_system(self):
|
||||
"""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):
|
||||
"""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:
|
||||
if self.type == 'minutely_summary':
|
||||
self.forecast_client.update_minutely()
|
||||
self._state = self.forecast_client.data_minutely.summary
|
||||
return
|
||||
def get_currently_state(self, data):
|
||||
"""
|
||||
Helper function that returns a new state based on the type.
|
||||
|
||||
elif self.type == 'hourly_summary':
|
||||
self.forecast_client.update_hourly()
|
||||
self._state = self.forecast_client.data_hourly.summary
|
||||
return
|
||||
If the sensor type is unknown, the current state is returned.
|
||||
"""
|
||||
lookup_type = convert_to_camel(self.type)
|
||||
state = getattr(data, lookup_type, 0)
|
||||
|
||||
elif self.type == 'daily_summary':
|
||||
self.forecast_client.update_daily()
|
||||
self._state = self.forecast_client.data_daily.summary
|
||||
return
|
||||
# Some state data needs to be rounded to whole values or converted to
|
||||
# percentages
|
||||
if self.type in ['precip_probability', 'cloud_cover', 'humidity']:
|
||||
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()
|
||||
data = self.forecast_client.data_currently
|
||||
def convert_to_camel(data):
|
||||
"""
|
||||
Convert snake case (foo_bar_bat) to camel case (fooBarBat).
|
||||
|
||||
try:
|
||||
if self.type == 'summary':
|
||||
self._state = data.summary
|
||||
elif self.type == 'icon':
|
||||
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
|
||||
This is not pythonic, but needed for certain situations
|
||||
"""
|
||||
components = data.split('_')
|
||||
return components[0] + "".join(x.title() for x in components[1:])
|
||||
|
||||
|
||||
class ForeCastData(object):
|
||||
@ -226,10 +216,13 @@ class ForeCastData(object):
|
||||
"""Get the latest data from Forecast.io."""
|
||||
import forecastio
|
||||
|
||||
self.data = forecastio.load_forecast(self._api_key,
|
||||
self.latitude,
|
||||
self.longitude,
|
||||
units=self.units)
|
||||
try:
|
||||
self.data = forecastio.load_forecast(self._api_key,
|
||||
self.latitude,
|
||||
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']
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
|
@ -7,7 +7,6 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import forecastio
|
||||
import httpretty
|
||||
import pytest
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from homeassistant.components.sensor import forecast
|
||||
@ -46,8 +45,8 @@ class TestForecastSetup(unittest.TestCase):
|
||||
msg = '400 Client Error: Bad Request for url: {}'.format(url)
|
||||
mock_get_forecast.side_effect = HTTPError(msg,)
|
||||
|
||||
with pytest.raises(HTTPError):
|
||||
forecast.setup_platform(self.hass, self.config, MagicMock())
|
||||
response = forecast.setup_platform(self.hass, self.config, MagicMock())
|
||||
self.assertFalse(response)
|
||||
|
||||
@httpretty.activate
|
||||
@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())
|
||||
self.assertTrue(mock_get_forecast.called)
|
||||
self.assertEqual(mock_get_forecast.call_count, 2)
|
||||
self.assertEqual(mock_get_forecast.call_count, 1)
|
||||
|
Loading…
x
Reference in New Issue
Block a user