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
24 changes: 24 additions & 0 deletions homeassistant/components/nws/.translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect, please try again",
"unknown": "Unexpected error"
},
"step": {
"user": {
Comment thread
MatthewFlamm marked this conversation as resolved.
Outdated
"data": {
"api_key": "API key (email)",
"latitude": "Latitude",
"longitude": "Longitude",
"station": "METAR station code"
},
"description": "If a METAR station code is not specified, the latitude and longitude will be used to find the closest station.",
"title": "Connect to the National Weather Service"
}
}
},
"title": "National Weather Service (NWS)"
}
73 changes: 44 additions & 29 deletions homeassistant/components/nws/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from pynws import SimpleNWS
import voluptuous as vol

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
Expand Down Expand Up @@ -52,38 +52,53 @@ def signal_unique_id(latitude, longitude):


async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the National Weather Service integration."""
if DOMAIN not in config:
return True

hass.data[DOMAIN] = hass.data.get(DOMAIN, {})
for entry in config[DOMAIN]:
latitude = entry.get(CONF_LATITUDE, hass.config.latitude)
longitude = entry.get(CONF_LONGITUDE, hass.config.longitude)
api_key = entry[CONF_API_KEY]

client_session = async_get_clientsession(hass)

if base_unique_id(latitude, longitude) in hass.data[DOMAIN]:
_LOGGER.error(
"Duplicate entry in config: latitude %s latitude: %s",
latitude,
longitude,
)
continue

nws_data = NwsData(hass, latitude, longitude, api_key, client_session)
hass.data[DOMAIN][base_unique_id(latitude, longitude)] = nws_data
async_track_time_interval(hass, nws_data.async_update, DEFAULT_SCAN_INTERVAL)

for component in PLATFORMS:
hass.async_create_task(
discovery.async_load_platform(hass, component, DOMAIN, entry, config)
)
"""Set up the National Weather Service (NWS) component."""
hass.data.setdefault(DOMAIN, {})
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up a National Weather Service entry."""
latitude = entry.data[CONF_LATITUDE]
longitude = entry.data[CONF_LONGITUDE]
api_key = entry.data[CONF_API_KEY]
station = entry.data[CONF_STATION]

client_session = async_get_clientsession(hass)

nws_data = NwsData(hass, latitude, longitude, api_key, client_session)
hass.data[DOMAIN][entry.entry_id] = nws_data

# async_set_station only does IO when station is None
await nws_data.async_set_station(station)
await nws_data.async_update()
Comment thread
bdraco marked this conversation as resolved.
Outdated

async_track_time_interval(hass, nws_data.async_update, DEFAULT_SCAN_INTERVAL)

for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
if len(hass.data[DOMAIN]) == 0:
hass.data.pop(DOMAIN)
return unload_ok


class NwsData:
"""Data class for National Weather Service integration."""

Expand Down
84 changes: 84 additions & 0 deletions homeassistant/components/nws/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Config flow for National Weather Service (NWS) integration."""
import logging

import aiohttp
import voluptuous as vol

from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from . import NwsData, base_unique_id
from .const import CONF_STATION, DOMAIN # pylint:disable=unused-import

_LOGGER = logging.getLogger(__name__)


async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.

Data has the keys from DATA_SCHEMA with values provided by the user.
"""
latitude = data[CONF_LATITUDE]
longitude = data[CONF_LONGITUDE]
api_key = data[CONF_API_KEY]
station = data.get(CONF_STATION)

client_session = async_get_clientsession(hass)
ha_api_key = f"{api_key} homeassistant"
nws = NwsData(hass, latitude, longitude, ha_api_key, client_session)

try:
await nws.async_set_station(station)
except aiohttp.ClientError as err:
_LOGGER.error("Could not connect: %s", err)
raise CannotConnect

return {"title": nws.station}


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for National Weather Service (NWS)."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL

async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
await self.async_set_unique_id(
base_unique_id(user_input[CONF_LATITUDE], user_input[CONF_LONGITUDE])
)
self._abort_if_unique_id_configured()
try:
info = await validate_input(self.hass, user_input)
user_input[CONF_STATION] = info["title"]
return self.async_create_entry(title=info["title"], data=user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"

data_schema = vol.Schema(
{
vol.Required(CONF_API_KEY): str,
vol.Required(
CONF_LATITUDE, default=self.hass.config.latitude
): cv.latitude,
vol.Required(
CONF_LONGITUDE, default=self.hass.config.longitude
): cv.longitude,
vol.Optional(CONF_STATION): str,
}
)

return self.async_show_form(
step_id="user", data_schema=data_schema, errors=errors
)


class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""
3 changes: 3 additions & 0 deletions homeassistant/components/nws/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,6 @@
"cloudy": ["Mostly cloudy", "Overcast"],
"partlycloudy": ["A few clouds", "Partly cloudy"],
}

DAYNIGHT = "daynight"
HOURLY = "hourly"
3 changes: 2 additions & 1 deletion homeassistant/components/nws/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"documentation": "https://www.home-assistant.io/integrations/nws",
"codeowners": ["@MatthewFlamm"],
"requirements": ["pynws==0.10.4"],
"quality_scale": "silver"
"quality_scale": "silver",
"config_flow": true
}
24 changes: 24 additions & 0 deletions homeassistant/components/nws/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"title": "National Weather Service (NWS)",
"config": {
"step": {
"user": {
"description": "If a METAR station code is not specified, the latitude and longitude will be used to find the closest station.",
"title": "Connect to the National Weather Service",
"data": {
"api_key": "API key (email)",
"latitude": "Latitude",
"longitude": "Longitude",
"station": "METAR station code"
}
}
},
"error": {
"cannot_connect": "Failed to connect, please try again",
"unknown": "Unexpected error"
},
"abort": {
"already_configured": "Device is already configured"
}
}
}
41 changes: 12 additions & 29 deletions homeassistant/components/nws/weather.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
"""Support for NWS weather service."""
import asyncio
import logging

import aiohttp

from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_TEMP,
Expand All @@ -13,8 +10,6 @@
WeatherEntity,
)
from homeassistant.const import (
CONF_LATITUDE,
CONF_LONGITUDE,
LENGTH_KILOMETERS,
LENGTH_METERS,
LENGTH_MILES,
Expand All @@ -25,8 +20,8 @@
TEMP_FAHRENHEIT,
)
from homeassistant.core import callback
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.util.distance import convert as convert_distance
from homeassistant.util.pressure import convert as convert_pressure
from homeassistant.util.temperature import convert as convert_temperature
Expand All @@ -38,8 +33,9 @@
ATTR_FORECAST_PRECIP_PROB,
ATTRIBUTION,
CONDITION_CLASSES,
CONF_STATION,
DAYNIGHT,
DOMAIN,
HOURLY,
)

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -73,28 +69,15 @@ def convert_condition(time, weather):
return cond, max(prec_probs)


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
async def async_setup_entry(
hass: HomeAssistantType, entry: ConfigType, async_add_entities
) -> None:
"""Set up the NWS weather platform."""
if discovery_info is None:
return
latitude = discovery_info.get(CONF_LATITUDE, hass.config.latitude)
longitude = discovery_info.get(CONF_LONGITUDE, hass.config.longitude)
station = discovery_info.get(CONF_STATION)

nws_data = hass.data[DOMAIN][base_unique_id(latitude, longitude)]

try:
await nws_data.async_set_station(station)
except (aiohttp.ClientError, asyncio.TimeoutError) as err:
_LOGGER.error("Error automatically setting station: %s", str(err))
raise PlatformNotReady

await nws_data.async_update()

nws_data = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
[
NWSWeather(nws_data, "daynight", hass.config.units),
NWSWeather(nws_data, "hourly", hass.config.units),
NWSWeather(nws_data, DAYNIGHT, hass.config.units),
NWSWeather(nws_data, HOURLY, hass.config.units),
],
False,
)
Expand Down Expand Up @@ -131,7 +114,7 @@ async def async_added_to_hass(self) -> None:
def _update_callback(self) -> None:
"""Load data from integration."""
self.observation = self.nws.observation
if self.mode == "daynight":
if self.mode == DAYNIGHT:
self._forecast = self.nws.forecast
else:
self._forecast = self.nws.forecast_hourly
Expand Down Expand Up @@ -259,7 +242,7 @@ def forecast(self):
ATTR_FORECAST_TIME: forecast_entry.get("startTime"),
}

if self.mode == "daynight":
if self.mode == DAYNIGHT:
data[ATTR_FORECAST_DAYTIME] = forecast_entry.get("isDaytime")
time = forecast_entry.get("iconTime")
weather = forecast_entry.get("iconWeather")
Expand Down Expand Up @@ -292,7 +275,7 @@ def unique_id(self):
@property
def available(self):
"""Return if state is available."""
if self.mode == "daynight":
if self.mode == DAYNIGHT:
return (
self.nws.update_observation_success and self.nws.update_forecast_success
)
Expand Down
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"notion",
"nuheat",
"nut",
"nws",
"opentherm_gw",
"openuv",
"owntracks",
Expand Down
11 changes: 9 additions & 2 deletions tests/components/nws/const.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Helpers for interacting with pynws."""
from homeassistant.components.nws.const import DOMAIN
from homeassistant.components.nws.const import CONF_STATION
from homeassistant.components.nws.weather import ATTR_FORECAST_PRECIP_PROB
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
Expand All @@ -16,6 +16,8 @@
)
from homeassistant.const import (
CONF_API_KEY,
CONF_LATITUDE,
CONF_LONGITUDE,
LENGTH_KILOMETERS,
LENGTH_METERS,
LENGTH_MILES,
Expand All @@ -29,7 +31,12 @@
from homeassistant.util.pressure import convert as convert_pressure
from homeassistant.util.temperature import convert as convert_temperature

MINIMAL_CONFIG = {DOMAIN: [{CONF_API_KEY: "test"}]}
NWS_CONFIG = {
CONF_API_KEY: "test",
CONF_LATITUDE: 35,
CONF_LONGITUDE: -75,
CONF_STATION: "ABC",
}

DEFAULT_STATIONS = ["ABC", "XYZ"]

Expand Down
Loading