Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,7 @@ omit =
homeassistant/components/vacuum/xiaomi_miio.py
homeassistant/components/weather/bom.py
homeassistant/components/weather/buienradar.py
homeassistant/components/weather/darksky.py
homeassistant/components/weather/metoffice.py
homeassistant/components/weather/openweathermap.py
homeassistant/components/weather/yweather.py
Expand Down
189 changes: 189 additions & 0 deletions homeassistant/components/weather/darksky.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
"""
Patform for retrieving meteorological data from Dark Sky.

For more details about this platform, please refer to the documentation
https://home-assistant.io/components/
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale URL.

"""
from datetime import datetime, timedelta
import logging

from requests.exceptions import (
ConnectionError as ConnectError, HTTPError, Timeout)
import voluptuous as vol

from homeassistant.components.weather import (
ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME, PLATFORM_SCHEMA, WeatherEntity)
from homeassistant.const import (
CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS,
TEMP_FAHRENHEIT)
import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle

REQUIREMENTS = ['python-forecastio==1.3.5']

_LOGGER = logging.getLogger(__name__)

ATTRIBUTION = "Powered by Dark Sky"

ATTR_DAILY_FORECAST_SUMMARY = 'daily_forecast_summary'
ATTR_HOURLY_FORECAST_SUMMARY = 'hourly_forecast_summary'

CONF_UNITS = 'units'

DEFAULT_NAME = 'Dark Sky'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_LATITUDE): cv.latitude,
vol.Optional(CONF_LONGITUDE): cv.longitude,
vol.Optional(CONF_UNITS): vol.In(['auto', 'si', 'us', 'ca', 'uk', 'uk2']),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})

MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=3)


def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Dark Sky weather."""
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
name = config.get(CONF_NAME)

units = config.get(CONF_UNITS)
if not units:
units = 'si' if hass.config.units.is_metric else 'us'

dark_sky = DarkSkyData(
config.get(CONF_API_KEY), latitude, longitude, units)

add_devices([DarkSkyWeather(name, dark_sky)], True)


class DarkSkyWeather(WeatherEntity):
"""Representation of a weather condition."""

def __init__(self, name, dark_sky):
"""Initialize Dark Sky weather."""
self._name = name
self._dark_sky = dark_sky

self._ds_data = None
self._ds_currently = None
self._ds_hourly = None
self._ds_daily = None

@property
def attribution(self):
"""Return the attribution."""
return ATTRIBUTION

@property
def name(self):
"""Return the name of the sensor."""
return self._name

@property
def temperature(self):
"""Return the temperature."""
return self._ds_currently.get('temperature')

@property
def temperature_unit(self):
"""Return the unit of measurement."""
return TEMP_FAHRENHEIT if 'us' in self._dark_sky.units \
else TEMP_CELSIUS

@property
def humidity(self):
"""Return the humidity."""
return self._ds_currently.get('humidity') * 100.0

@property
def wind_speed(self):
"""Return the wind speed."""
return self._ds_currently.get('windSpeed')

@property
def pressure(self):
"""Return the pressure."""
return self._ds_currently.get('pressure')

@property
def condition(self):
"""Return the weather condition."""
return self._ds_currently.get('summary')

@property
def forecast(self):
"""Return the forecast array."""
return [{
ATTR_FORECAST_TIME:
datetime.fromtimestamp(entry.d.get('time')).isoformat(),
ATTR_FORECAST_TEMP: entry.d.get('temperature')}
for entry in self._ds_hourly.data]

@property
def hourly_forecast_summary(self):
"""Return a summary of the hourly forecast."""
return self._ds_hourly.summary

@property
def daily_forecast_summary(self):
"""Return a summary of the daily forecast."""
return self._ds_daily.summary

@property
def state_attributes(self):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should not overwrite this but implement device_state_attributes. Those will update the state attributes in the entity base class.

"""Return the state attributes."""
attrs = super().state_attributes
attrs.update({
ATTR_DAILY_FORECAST_SUMMARY: self.daily_forecast_summary,
ATTR_HOURLY_FORECAST_SUMMARY: self.hourly_forecast_summary
})
return attrs

def update(self):
"""Get the latest data from Dark Sky."""
self._dark_sky.update()

self._ds_data = self._dark_sky.data
self._ds_currently = self._dark_sky.currently.d
self._ds_hourly = self._dark_sky.hourly
self._ds_daily = self._dark_sky.daily


class DarkSkyData(object):
"""Get the latest data from Dark Sky."""

def __init__(self, api_key, latitude, longitude, units):
"""Initialize the data object."""
self._api_key = api_key
self.latitude = latitude
self.longitude = longitude
self.requested_units = units

self.data = None
self.currently = None
self.hourly = None
self.daily = None

@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from Dark Sky."""
import forecastio

try:
self.data = forecastio.load_forecast(
self._api_key, self.latitude, self.longitude,
units=self.requested_units)
self.currently = self.data.currently()
self.hourly = self.data.hourly()
self.daily = self.data.daily()
except (ConnectError, HTTPError, Timeout, ValueError) as error:
_LOGGER.error("Unable to connect to Dark Sky. %s", error)
self.data = None

@property
def units(self):
"""Get the unit system of returned data."""
return self.data.json.get('flags').get('units')
1 change: 1 addition & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,7 @@ python-ecobee-api==0.0.14
python-etherscan-api==0.0.1

# homeassistant.components.sensor.darksky
# homeassistant.components.weather.darksky
python-forecastio==1.3.5

# homeassistant.components.gc100
Expand Down
1 change: 1 addition & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ pymonoprice==0.3
pynx584==0.4

# homeassistant.components.sensor.darksky
# homeassistant.components.weather.darksky
python-forecastio==1.3.5

# homeassistant.components.sensor.whois
Expand Down
51 changes: 51 additions & 0 deletions tests/components/weather/test_darksky.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""The tests for the Dark Sky weather component."""
import re
import unittest
from unittest.mock import patch

import forecastio
import requests_mock

from homeassistant.components import weather
from homeassistant.util.unit_system import METRIC_SYSTEM
from homeassistant.setup import setup_component

from tests.common import load_fixture, get_test_home_assistant


class TestDarkSky(unittest.TestCase):
"""Test the Dark Sky weather component."""

def setUp(self):
"""Setup things to be run when tests are started."""
self.hass = get_test_home_assistant()
self.hass.config.units = METRIC_SYSTEM
self.lat = self.hass.config.latitude = 37.8267
self.lon = self.hass.config.longitude = -122.423

def tearDown(self):
"""Stop down everything that was started."""
self.hass.stop()

@requests_mock.Mocker()
@patch('forecastio.api.get_forecast', wraps=forecastio.api.get_forecast)
def test_setup(self, mock_req, mock_get_forecast):
"""Test for successfully setting up the forecast.io platform."""
uri = (r'https://api.(darksky.net|forecast.io)\/forecast\/(\w+)\/'
r'(-?\d+\.?\d*),(-?\d+\.?\d*)')
mock_req.get(re.compile(uri),
text=load_fixture('darksky.json'))

self.assertTrue(setup_component(self.hass, weather.DOMAIN, {
'weather': {
'name': 'test',
'platform': 'darksky',
'api_key': 'foo',
}
}))

self.assertTrue(mock_get_forecast.called)
self.assertEqual(mock_get_forecast.call_count, 1)

state = self.hass.states.get('weather.test')
self.assertEqual(state.state, 'Clear')