Add AccuWeather integration (#37166)
* Initial commit * Fix strings * Fix unit system * Add config_flow tests * Simplify tests * More tests * Update comment * Fix pylint error * Run gen_requirements_all * Fix pyline error * Round precipitation and precipitation probability * Bump backend library * Bump backend library * Add undo update listener on unload * Add translation key for invalid_api_key * Remove entity_registry_enabled_default property * Suggested change * Bump librarypull/38181/head
parent
9fe142a114
commit
581c4a4edd
|
@ -8,6 +8,9 @@ omit =
|
|||
homeassistant/scripts/*.py
|
||||
|
||||
# omit pieces of code that rely on external devices being present
|
||||
homeassistant/components/accuweather/__init__.py
|
||||
homeassistant/components/accuweather/const.py
|
||||
homeassistant/components/accuweather/weather.py
|
||||
homeassistant/components/acer_projector/switch.py
|
||||
homeassistant/components/actiontec/device_tracker.py
|
||||
homeassistant/components/acmeda/__init__.py
|
||||
|
|
|
@ -14,6 +14,7 @@ homeassistant/scripts/check_config.py @kellerza
|
|||
|
||||
# Integrations
|
||||
homeassistant/components/abode/* @shred86
|
||||
homeassistant/components/accuweather/* @bieniu
|
||||
homeassistant/components/acmeda/* @atmurray
|
||||
homeassistant/components/adguard/* @frenck
|
||||
homeassistant/components/agent_dvr/* @ispysoftware
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
"""The AccuWeather component."""
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from async_timeout import timeout
|
||||
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import Config, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
ATTR_FORECAST,
|
||||
CONF_FORECAST,
|
||||
COORDINATOR,
|
||||
DOMAIN,
|
||||
UNDO_UPDATE_LISTENER,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = ["weather"]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: Config) -> bool:
|
||||
"""Set up configured AccuWeather."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry) -> bool:
|
||||
"""Set up AccuWeather as config entry."""
|
||||
api_key = config_entry.data[CONF_API_KEY]
|
||||
location_key = config_entry.unique_id
|
||||
forecast = config_entry.options.get(CONF_FORECAST, False)
|
||||
|
||||
_LOGGER.debug("Using location_key: %s, get forecast: %s", location_key, forecast)
|
||||
|
||||
websession = async_get_clientsession(hass)
|
||||
|
||||
coordinator = AccuWeatherDataUpdateCoordinator(
|
||||
hass, websession, api_key, location_key, forecast
|
||||
)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
if not coordinator.last_update_success:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
undo_listener = config_entry.add_update_listener(update_listener)
|
||||
|
||||
hass.data[DOMAIN][config_entry.entry_id] = {
|
||||
COORDINATOR: coordinator,
|
||||
UNDO_UPDATE_LISTENER: undo_listener,
|
||||
}
|
||||
|
||||
for component in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(config_entry, component)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, config_entry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = all(
|
||||
await asyncio.gather(
|
||||
*[
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, component)
|
||||
for component in PLATFORMS
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]()
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def update_listener(hass, config_entry):
|
||||
"""Update listener."""
|
||||
await hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
|
||||
class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching AccuWeather data API."""
|
||||
|
||||
def __init__(self, hass, session, api_key, location_key, forecast: bool):
|
||||
"""Initialize."""
|
||||
self.location_key = location_key
|
||||
self.forecast = forecast
|
||||
self.is_metric = hass.config.units.is_metric
|
||||
self.accuweather = AccuWeather(api_key, session, location_key=self.location_key)
|
||||
|
||||
# Enabling the forecast download increases the number of requests per data
|
||||
# update, we use 32 minutes for current condition only and 64 minutes for
|
||||
# current condition and forecast as update interval to not exceed allowed number
|
||||
# of requests. We have 50 requests allowed per day, so we use 45 and leave 5 as
|
||||
# a reserve for restarting HA.
|
||||
update_interval = (
|
||||
timedelta(minutes=64) if self.forecast else timedelta(minutes=32)
|
||||
)
|
||||
_LOGGER.debug("Data will be update every %s", update_interval)
|
||||
|
||||
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
|
||||
|
||||
async def _async_update_data(self):
|
||||
"""Update data via library."""
|
||||
try:
|
||||
with timeout(10):
|
||||
current = await self.accuweather.async_get_current_conditions()
|
||||
forecast = (
|
||||
await self.accuweather.async_get_forecast(metric=self.is_metric)
|
||||
if self.forecast
|
||||
else {}
|
||||
)
|
||||
except (
|
||||
ApiError,
|
||||
ClientConnectorError,
|
||||
InvalidApiKeyError,
|
||||
RequestsExceededError,
|
||||
) as error:
|
||||
raise UpdateFailed(error)
|
||||
_LOGGER.debug("Requests remaining: %s", self.accuweather.requests_remaining)
|
||||
return {**current, **{ATTR_FORECAST: forecast}}
|
|
@ -0,0 +1,112 @@
|
|||
"""Adds config flow for AccuWeather."""
|
||||
import asyncio
|
||||
|
||||
from accuweather import AccuWeather, ApiError, InvalidApiKeyError, RequestsExceededError
|
||||
from aiohttp import ClientError
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from async_timeout import timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import CONF_FORECAST, DOMAIN # pylint:disable=unused-import
|
||||
|
||||
|
||||
class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for AccuWeather."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
# Under the terms of use of the API, one user can use one free API key. Due to
|
||||
# the small number of requests allowed, we only allow one integration instance.
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
try:
|
||||
with timeout(10):
|
||||
accuweather = AccuWeather(
|
||||
user_input[CONF_API_KEY],
|
||||
websession,
|
||||
latitude=user_input[CONF_LATITUDE],
|
||||
longitude=user_input[CONF_LONGITUDE],
|
||||
)
|
||||
await accuweather.async_get_location()
|
||||
except (ApiError, ClientConnectorError, asyncio.TimeoutError, ClientError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidApiKeyError:
|
||||
errors[CONF_API_KEY] = "invalid_api_key"
|
||||
except RequestsExceededError:
|
||||
errors[CONF_API_KEY] = "requests_exceeded"
|
||||
else:
|
||||
await self.async_set_unique_id(
|
||||
accuweather.location_key, raise_on_progress=False
|
||||
)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_NAME], data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Optional(
|
||||
CONF_LATITUDE, default=self.hass.config.latitude
|
||||
): cv.latitude,
|
||||
vol.Optional(
|
||||
CONF_LONGITUDE, default=self.hass.config.longitude
|
||||
): cv.longitude,
|
||||
vol.Optional(
|
||||
CONF_NAME, default=self.hass.config.location_name
|
||||
): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Options callback for AccuWeather."""
|
||||
return AccuWeatherOptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class AccuWeatherOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Config flow options for AccuWeather."""
|
||||
|
||||
def __init__(self, config_entry):
|
||||
"""Initialize AccuWeather options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage the options."""
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_FORECAST,
|
||||
default=self.config_entry.options.get(CONF_FORECAST, False),
|
||||
): bool
|
||||
}
|
||||
),
|
||||
)
|
|
@ -0,0 +1,23 @@
|
|||
"""Constants for AccuWeather integration."""
|
||||
ATTRIBUTION = "Data provided by AccuWeather"
|
||||
ATTR_FORECAST = CONF_FORECAST = "forecast"
|
||||
COORDINATOR = "coordinator"
|
||||
DOMAIN = "accuweather"
|
||||
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||
|
||||
CONDITION_CLASSES = {
|
||||
"clear-night": [33, 34, 37],
|
||||
"cloudy": [7, 8, 38],
|
||||
"exceptional": [24, 30, 31],
|
||||
"fog": [11],
|
||||
"hail": [25],
|
||||
"lightning": [15],
|
||||
"lightning-rainy": [16, 17, 41, 42],
|
||||
"partlycloudy": [4, 6, 35, 36],
|
||||
"pouring": [18],
|
||||
"rainy": [12, 13, 14, 26, 39, 40],
|
||||
"snowy": [19, 20, 21, 22, 23, 43, 44],
|
||||
"snowy-rainy": [29],
|
||||
"sunny": [1, 2, 3, 5],
|
||||
"windy": [32],
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"domain": "accuweather",
|
||||
"name": "AccuWeather",
|
||||
"documentation": "https://github.com/bieniu/ha-accuweather",
|
||||
"requirements": ["accuweather==0.0.9"],
|
||||
"codeowners": ["@bieniu"],
|
||||
"config_flow": true
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "AccuWeather",
|
||||
"description": "If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/accuweather/\n\nWeather forecast is not enabled by default. You can enable it in the integration options.",
|
||||
"data": {
|
||||
"name": "Name of the integration",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"latitude": "Latitude",
|
||||
"longitude": "Longitude"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||
"requests_exceeded": "The allowed number of requests to Accuweather API has been exceeded. You have to wait or change API Key."
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "AccuWeather Options",
|
||||
"description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 64 minutes instead of every 32 minutes.",
|
||||
"data": {
|
||||
"forecast": "Weather forecast"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,179 @@
|
|||
"""Support for the AccuWeather service."""
|
||||
from statistics import mean
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_PRECIPITATION,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||
ATTR_FORECAST_TEMP,
|
||||
ATTR_FORECAST_TEMP_LOW,
|
||||
ATTR_FORECAST_TIME,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
ATTR_FORECAST_WIND_SPEED,
|
||||
WeatherEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME, STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
from homeassistant.util.dt import utc_from_timestamp
|
||||
|
||||
from .const import ATTR_FORECAST, ATTRIBUTION, CONDITION_CLASSES, COORDINATOR, DOMAIN
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Add a AccuWeather weather entity from a config_entry."""
|
||||
name = config_entry.data[CONF_NAME]
|
||||
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR]
|
||||
|
||||
async_add_entities([AccuWeatherEntity(name, coordinator)], False)
|
||||
|
||||
|
||||
class AccuWeatherEntity(WeatherEntity):
|
||||
"""Define an AccuWeather entity."""
|
||||
|
||||
def __init__(self, name, coordinator):
|
||||
"""Initialize."""
|
||||
self._name = name
|
||||
self.coordinator = coordinator
|
||||
self._attrs = {}
|
||||
self._unit_system = "Metric" if self.coordinator.is_metric else "Imperial"
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
"""Return the attribution."""
|
||||
return ATTRIBUTION
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return a unique_id for this entity."""
|
||||
return self.coordinator.location_key
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
"""Return the polling requirement of the entity."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return True if entity is available."""
|
||||
return self.coordinator.last_update_success
|
||||
|
||||
@property
|
||||
def condition(self):
|
||||
"""Return the current condition."""
|
||||
try:
|
||||
return [
|
||||
k
|
||||
for k, v in CONDITION_CLASSES.items()
|
||||
if self.coordinator.data["WeatherIcon"] in v
|
||||
][0]
|
||||
except IndexError:
|
||||
return STATE_UNKNOWN
|
||||
|
||||
@property
|
||||
def temperature(self):
|
||||
"""Return the temperature."""
|
||||
return self.coordinator.data["Temperature"][self._unit_system]["Value"]
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
"""Return the unit of measurement."""
|
||||
return TEMP_CELSIUS if self.coordinator.is_metric else TEMP_FAHRENHEIT
|
||||
|
||||
@property
|
||||
def pressure(self):
|
||||
"""Return the pressure."""
|
||||
return self.coordinator.data["Pressure"][self._unit_system]["Value"]
|
||||
|
||||
@property
|
||||
def humidity(self):
|
||||
"""Return the humidity."""
|
||||
return self.coordinator.data["RelativeHumidity"]
|
||||
|
||||
@property
|
||||
def wind_speed(self):
|
||||
"""Return the wind speed."""
|
||||
return self.coordinator.data["Wind"]["Speed"][self._unit_system]["Value"]
|
||||
|
||||
@property
|
||||
def wind_bearing(self):
|
||||
"""Return the wind bearing."""
|
||||
return self.coordinator.data["Wind"]["Direction"]["Degrees"]
|
||||
|
||||
@property
|
||||
def visibility(self):
|
||||
"""Return the visibility."""
|
||||
return self.coordinator.data["Visibility"][self._unit_system]["Value"]
|
||||
|
||||
@property
|
||||
def ozone(self):
|
||||
"""Return the ozone level."""
|
||||
# We only have ozone data for certain locations and only in the forecast data.
|
||||
if self.coordinator.forecast and self.coordinator.data[ATTR_FORECAST][0].get(
|
||||
"Ozone"
|
||||
):
|
||||
return self.coordinator.data[ATTR_FORECAST][0]["Ozone"]["Value"]
|
||||
return None
|
||||
|
||||
@property
|
||||
def forecast(self):
|
||||
"""Return the forecast array."""
|
||||
if self.coordinator.forecast:
|
||||
# remap keys from library to keys understood by the weather component
|
||||
forecast = [
|
||||
{
|
||||
ATTR_FORECAST_TIME: utc_from_timestamp(
|
||||
item["EpochDate"]
|
||||
).isoformat(),
|
||||
ATTR_FORECAST_TEMP: item["TemperatureMax"]["Value"],
|
||||
ATTR_FORECAST_TEMP_LOW: item["TemperatureMin"]["Value"],
|
||||
ATTR_FORECAST_PRECIPITATION: self._calc_precipitation(item),
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: round(
|
||||
mean(
|
||||
[
|
||||
item["PrecipitationProbabilityDay"],
|
||||
item["PrecipitationProbabilityNight"],
|
||||
]
|
||||
)
|
||||
),
|
||||
ATTR_FORECAST_WIND_SPEED: item["WindDay"]["Speed"]["Value"],
|
||||
ATTR_FORECAST_WIND_BEARING: item["WindDay"]["Direction"]["Degrees"],
|
||||
ATTR_FORECAST_CONDITION: [
|
||||
k for k, v in CONDITION_CLASSES.items() if item["IconDay"] in v
|
||||
][0],
|
||||
}
|
||||
for item in self.coordinator.data[ATTR_FORECAST]
|
||||
]
|
||||
return forecast
|
||||
return None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Connect to dispatcher listening for entity data notifications."""
|
||||
self.async_on_remove(
|
||||
self.coordinator.async_add_listener(self.async_write_ha_state)
|
||||
)
|
||||
|
||||
async def async_update(self):
|
||||
"""Update AccuWeather entity."""
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@staticmethod
|
||||
def _calc_precipitation(day: dict) -> float:
|
||||
"""Return sum of the precipitation."""
|
||||
precip_sum = 0
|
||||
precip_types = ["Rain", "Snow", "Ice"]
|
||||
for precip in precip_types:
|
||||
precip_sum = sum(
|
||||
[
|
||||
precip_sum,
|
||||
day[f"{precip}Day"]["Value"],
|
||||
day[f"{precip}Night"]["Value"],
|
||||
]
|
||||
)
|
||||
return round(precip_sum, 1)
|
|
@ -7,6 +7,7 @@ To update, run python3 -m script.hassfest
|
|||
|
||||
FLOWS = [
|
||||
"abode",
|
||||
"accuweather",
|
||||
"acmeda",
|
||||
"adguard",
|
||||
"agent_dvr",
|
||||
|
|
|
@ -102,6 +102,9 @@ YesssSMS==0.4.1
|
|||
# homeassistant.components.abode
|
||||
abodepy==0.19.0
|
||||
|
||||
# homeassistant.components.accuweather
|
||||
accuweather==0.0.9
|
||||
|
||||
# homeassistant.components.mcp23017
|
||||
adafruit-blinka==3.9.0
|
||||
|
||||
|
|
|
@ -45,6 +45,9 @@ YesssSMS==0.4.1
|
|||
# homeassistant.components.abode
|
||||
abodepy==0.19.0
|
||||
|
||||
# homeassistant.components.accuweather
|
||||
accuweather==0.0.9
|
||||
|
||||
# homeassistant.components.androidtv
|
||||
adb-shell[async]==0.2.0
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Tests for AccuWeather."""
|
|
@ -0,0 +1,158 @@
|
|||
"""Define tests for the AccuWeather config flow."""
|
||||
import json
|
||||
|
||||
from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.accuweather.const import CONF_FORECAST, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
VALID_CONFIG = {
|
||||
CONF_NAME: "abcd",
|
||||
CONF_API_KEY: "32-character-string-1234567890qw",
|
||||
CONF_LATITUDE: 55.55,
|
||||
CONF_LONGITUDE: 122.12,
|
||||
}
|
||||
|
||||
|
||||
async def test_show_form(hass):
|
||||
"""Test that the form is served with no input."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == SOURCE_USER
|
||||
|
||||
|
||||
async def test_invalid_api_key_1(hass):
|
||||
"""Test that errors are shown when API key is invalid."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data={
|
||||
CONF_NAME: "abcd",
|
||||
CONF_API_KEY: "foo",
|
||||
CONF_LATITUDE: 55.55,
|
||||
CONF_LONGITUDE: 122.12,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["errors"] == {CONF_API_KEY: "invalid_api_key"}
|
||||
|
||||
|
||||
async def test_invalid_api_key_2(hass):
|
||||
"""Test that errors are shown when API key is invalid."""
|
||||
with patch(
|
||||
"accuweather.AccuWeather._async_get_data",
|
||||
side_effect=InvalidApiKeyError("Invalid API key"),
|
||||
):
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG,
|
||||
)
|
||||
|
||||
assert result["errors"] == {CONF_API_KEY: "invalid_api_key"}
|
||||
|
||||
|
||||
async def test_api_error(hass):
|
||||
"""Test API error."""
|
||||
with patch(
|
||||
"accuweather.AccuWeather._async_get_data",
|
||||
side_effect=ApiError("Invalid response from AccuWeather API"),
|
||||
):
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG,
|
||||
)
|
||||
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_requests_exceeded_error(hass):
|
||||
"""Test requests exceeded error."""
|
||||
with patch(
|
||||
"accuweather.AccuWeather._async_get_data",
|
||||
side_effect=RequestsExceededError(
|
||||
"The allowed number of requests has been exceeded"
|
||||
),
|
||||
):
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG,
|
||||
)
|
||||
|
||||
assert result["errors"] == {CONF_API_KEY: "requests_exceeded"}
|
||||
|
||||
|
||||
async def test_integration_already_exists(hass):
|
||||
"""Test we only allow a single config flow."""
|
||||
with patch(
|
||||
"accuweather.AccuWeather._async_get_data",
|
||||
return_value=json.loads(load_fixture("accuweather/location_data.json")),
|
||||
):
|
||||
MockConfigEntry(
|
||||
domain=DOMAIN, unique_id="123456", data=VALID_CONFIG,
|
||||
).add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "single_instance_allowed"
|
||||
|
||||
|
||||
async def test_create_entry(hass):
|
||||
"""Test that the user step works."""
|
||||
with patch(
|
||||
"accuweather.AccuWeather._async_get_data",
|
||||
return_value=json.loads(load_fixture("accuweather/location_data.json")),
|
||||
):
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=VALID_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "abcd"
|
||||
assert result["data"][CONF_NAME] == "abcd"
|
||||
assert result["data"][CONF_LATITUDE] == 55.55
|
||||
assert result["data"][CONF_LONGITUDE] == 122.12
|
||||
assert result["data"][CONF_API_KEY] == "32-character-string-1234567890qw"
|
||||
|
||||
|
||||
async def test_options_flow(hass):
|
||||
"""Test config flow options."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, unique_id="123456", data=VALID_CONFIG,
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"accuweather.AccuWeather._async_get_data",
|
||||
return_value=json.loads(load_fixture("accuweather/location_data.json")),
|
||||
), patch(
|
||||
"accuweather.AccuWeather.async_get_current_conditions",
|
||||
return_value=json.loads(
|
||||
load_fixture("accuweather/current_conditions_data.json")
|
||||
),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], user_input={CONF_FORECAST: True}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert config_entry.options == {CONF_FORECAST: True}
|
|
@ -0,0 +1,290 @@
|
|||
{
|
||||
"WeatherIcon": 1,
|
||||
"HasPrecipitation": false,
|
||||
"PrecipitationType": null,
|
||||
"Temperature": {
|
||||
"Metric": {
|
||||
"Value": 22.6,
|
||||
"Unit": "C",
|
||||
"UnitType": 17
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 73.0,
|
||||
"Unit": "F",
|
||||
"UnitType": 18
|
||||
}
|
||||
},
|
||||
"RealFeelTemperature": {
|
||||
"Metric": {
|
||||
"Value": 25.1,
|
||||
"Unit": "C",
|
||||
"UnitType": 17
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 77.0,
|
||||
"Unit": "F",
|
||||
"UnitType": 18
|
||||
}
|
||||
},
|
||||
"RealFeelTemperatureShade": {
|
||||
"Metric": {
|
||||
"Value": 21.1,
|
||||
"Unit": "C",
|
||||
"UnitType": 17
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 70.0,
|
||||
"Unit": "F",
|
||||
"UnitType": 18
|
||||
}
|
||||
},
|
||||
"RelativeHumidity": 67,
|
||||
"IndoorRelativeHumidity": 67,
|
||||
"DewPoint": {
|
||||
"Metric": {
|
||||
"Value": 16.2,
|
||||
"Unit": "C",
|
||||
"UnitType": 17
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 61.0,
|
||||
"Unit": "F",
|
||||
"UnitType": 18
|
||||
}
|
||||
},
|
||||
"Wind": {
|
||||
"Direction": {
|
||||
"Degrees": 180,
|
||||
"Localized": "S",
|
||||
"English": "S"
|
||||
},
|
||||
"Speed": {
|
||||
"Metric": {
|
||||
"Value": 14.5,
|
||||
"Unit": "km/h",
|
||||
"UnitType": 7
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 9.0,
|
||||
"Unit": "mi/h",
|
||||
"UnitType": 9
|
||||
}
|
||||
}
|
||||
},
|
||||
"WindGust": {
|
||||
"Speed": {
|
||||
"Metric": {
|
||||
"Value": 20.3,
|
||||
"Unit": "km/h",
|
||||
"UnitType": 7
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 12.6,
|
||||
"Unit": "mi/h",
|
||||
"UnitType": 9
|
||||
}
|
||||
}
|
||||
},
|
||||
"UVIndex": 6,
|
||||
"UVIndexText": "High",
|
||||
"Visibility": {
|
||||
"Metric": {
|
||||
"Value": 16.1,
|
||||
"Unit": "km",
|
||||
"UnitType": 6
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 10.0,
|
||||
"Unit": "mi",
|
||||
"UnitType": 2
|
||||
}
|
||||
},
|
||||
"ObstructionsToVisibility": "",
|
||||
"CloudCover": 10,
|
||||
"Ceiling": {
|
||||
"Metric": {
|
||||
"Value": 3200.0,
|
||||
"Unit": "m",
|
||||
"UnitType": 5
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 10500.0,
|
||||
"Unit": "ft",
|
||||
"UnitType": 0
|
||||
}
|
||||
},
|
||||
"Pressure": {
|
||||
"Metric": {
|
||||
"Value": 1012.0,
|
||||
"Unit": "mb",
|
||||
"UnitType": 14
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 29.88,
|
||||
"Unit": "inHg",
|
||||
"UnitType": 12
|
||||
}
|
||||
},
|
||||
"PressureTendency": {
|
||||
"LocalizedText": "Falling",
|
||||
"Code": "F"
|
||||
},
|
||||
"Past24HourTemperatureDeparture": {
|
||||
"Metric": {
|
||||
"Value": 0.3,
|
||||
"Unit": "C",
|
||||
"UnitType": 17
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 0.0,
|
||||
"Unit": "F",
|
||||
"UnitType": 18
|
||||
}
|
||||
},
|
||||
"ApparentTemperature": {
|
||||
"Metric": {
|
||||
"Value": 22.8,
|
||||
"Unit": "C",
|
||||
"UnitType": 17
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 73.0,
|
||||
"Unit": "F",
|
||||
"UnitType": 18
|
||||
}
|
||||
},
|
||||
"WindChillTemperature": {
|
||||
"Metric": {
|
||||
"Value": 22.8,
|
||||
"Unit": "C",
|
||||
"UnitType": 17
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 73.0,
|
||||
"Unit": "F",
|
||||
"UnitType": 18
|
||||
}
|
||||
},
|
||||
"WetBulbTemperature": {
|
||||
"Metric": {
|
||||
"Value": 18.6,
|
||||
"Unit": "C",
|
||||
"UnitType": 17
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 65.0,
|
||||
"Unit": "F",
|
||||
"UnitType": 18
|
||||
}
|
||||
},
|
||||
"Precip1hr": {
|
||||
"Metric": {
|
||||
"Value": 0.0,
|
||||
"Unit": "mm",
|
||||
"UnitType": 3
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 0.0,
|
||||
"Unit": "in",
|
||||
"UnitType": 1
|
||||
}
|
||||
},
|
||||
"PrecipitationSummary": {
|
||||
"Precipitation": {
|
||||
"Metric": {
|
||||
"Value": 0.0,
|
||||
"Unit": "mm",
|
||||
"UnitType": 3
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 0.0,
|
||||
"Unit": "in",
|
||||
"UnitType": 1
|
||||
}
|
||||
},
|
||||
"PastHour": {
|
||||
"Metric": {
|
||||
"Value": 0.0,
|
||||
"Unit": "mm",
|
||||
"UnitType": 3
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 0.0,
|
||||
"Unit": "in",
|
||||
"UnitType": 1
|
||||
}
|
||||
},
|
||||
"Past3Hours": {
|
||||
"Metric": {
|
||||
"Value": 1.3,
|
||||
"Unit": "mm",
|
||||
"UnitType": 3
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 0.05,
|
||||
"Unit": "in",
|
||||
"UnitType": 1
|
||||
}
|
||||
},
|
||||
"Past6Hours": {
|
||||
"Metric": {
|
||||
"Value": 1.3,
|
||||
"Unit": "mm",
|
||||
"UnitType": 3
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 0.05,
|
||||
"Unit": "in",
|
||||
"UnitType": 1
|
||||
}
|
||||
},
|
||||
"Past9Hours": {
|
||||
"Metric": {
|
||||
"Value": 2.5,
|
||||
"Unit": "mm",
|
||||
"UnitType": 3
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 0.1,
|
||||
"Unit": "in",
|
||||
"UnitType": 1
|
||||
}
|
||||
},
|
||||
"Past12Hours": {
|
||||
"Metric": {
|
||||
"Value": 3.8,
|
||||
"Unit": "mm",
|
||||
"UnitType": 3
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 0.15,
|
||||
"Unit": "in",
|
||||
"UnitType": 1
|
||||
}
|
||||
},
|
||||
"Past18Hours": {
|
||||
"Metric": {
|
||||
"Value": 5.1,
|
||||
"Unit": "mm",
|
||||
"UnitType": 3
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 0.2,
|
||||
"Unit": "in",
|
||||
"UnitType": 1
|
||||
}
|
||||
},
|
||||
"Past24Hours": {
|
||||
"Metric": {
|
||||
"Value": 7.6,
|
||||
"Unit": "mm",
|
||||
"UnitType": 3
|
||||
},
|
||||
"Imperial": {
|
||||
"Value": 0.3,
|
||||
"Unit": "in",
|
||||
"UnitType": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"Version": 1,
|
||||
"Key": "268068",
|
||||
"Type": "City",
|
||||
"Rank": 85,
|
||||
"LocalizedName": "Piątek",
|
||||
"EnglishName": "Piątek",
|
||||
"PrimaryPostalCode": "",
|
||||
"Region": { "ID": "EUR", "LocalizedName": "Europe", "EnglishName": "Europe" },
|
||||
"Country": { "ID": "PL", "LocalizedName": "Poland", "EnglishName": "Poland" },
|
||||
"AdministrativeArea": {
|
||||
"ID": "10",
|
||||
"LocalizedName": "Łódź",
|
||||
"EnglishName": "Łódź",
|
||||
"Level": 1,
|
||||
"LocalizedType": "Voivodship",
|
||||
"EnglishType": "Voivodship",
|
||||
"CountryID": "PL"
|
||||
},
|
||||
"TimeZone": {
|
||||
"Code": "CEST",
|
||||
"Name": "Europe/Warsaw",
|
||||
"GmtOffset": 2.0,
|
||||
"IsDaylightSaving": true,
|
||||
"NextOffsetChange": "2020-10-25T01:00:00Z"
|
||||
},
|
||||
"GeoPosition": {
|
||||
"Latitude": 52.069,
|
||||
"Longitude": 19.479,
|
||||
"Elevation": {
|
||||
"Metric": { "Value": 94.0, "Unit": "m", "UnitType": 5 },
|
||||
"Imperial": { "Value": 308.0, "Unit": "ft", "UnitType": 0 }
|
||||
}
|
||||
},
|
||||
"IsAlias": false,
|
||||
"SupplementalAdminAreas": [
|
||||
{ "Level": 2, "LocalizedName": "Łęczyca", "EnglishName": "Łęczyca" },
|
||||
{ "Level": 3, "LocalizedName": "Piątek", "EnglishName": "Piątek" }
|
||||
],
|
||||
"DataSets": [
|
||||
"AirQualityCurrentConditions",
|
||||
"AirQualityForecasts",
|
||||
"Alerts",
|
||||
"ForecastConfidence",
|
||||
"FutureRadar",
|
||||
"MinuteCast",
|
||||
"Radar"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue