-
-
Notifications
You must be signed in to change notification settings - Fork 31.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Apple WeatherKit integration (#99895)
- Loading branch information
Showing
21 changed files
with
11,431 additions
and
1 deletion.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ | |
"homekit", | ||
"ibeacon", | ||
"icloud", | ||
"itunes" | ||
"itunes", | ||
"weatherkit" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
"""Integration for Apple's WeatherKit API.""" | ||
from __future__ import annotations | ||
|
||
from apple_weatherkit.client import ( | ||
WeatherKitApiClient, | ||
WeatherKitApiClientAuthenticationError, | ||
WeatherKitApiClientError, | ||
) | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import Platform | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryNotReady | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
|
||
from .const import ( | ||
CONF_KEY_ID, | ||
CONF_KEY_PEM, | ||
CONF_SERVICE_ID, | ||
CONF_TEAM_ID, | ||
DOMAIN, | ||
LOGGER, | ||
) | ||
from .coordinator import WeatherKitDataUpdateCoordinator | ||
|
||
PLATFORMS: list[Platform] = [Platform.WEATHER] | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Set up this integration using UI.""" | ||
hass.data.setdefault(DOMAIN, {}) | ||
coordinator = WeatherKitDataUpdateCoordinator( | ||
hass=hass, | ||
client=WeatherKitApiClient( | ||
key_id=entry.data[CONF_KEY_ID], | ||
service_id=entry.data[CONF_SERVICE_ID], | ||
team_id=entry.data[CONF_TEAM_ID], | ||
key_pem=entry.data[CONF_KEY_PEM], | ||
session=async_get_clientsession(hass), | ||
), | ||
) | ||
|
||
try: | ||
await coordinator.update_supported_data_sets() | ||
except WeatherKitApiClientAuthenticationError as ex: | ||
LOGGER.error("Authentication error initializing integration: %s", ex) | ||
return False | ||
except WeatherKitApiClientError as ex: | ||
raise ConfigEntryNotReady from ex | ||
|
||
await coordinator.async_config_entry_first_refresh() | ||
hass.data[DOMAIN][entry.entry_id] = coordinator | ||
|
||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
return True | ||
|
||
|
||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Handle removal of an entry.""" | ||
if unloaded := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): | ||
hass.data[DOMAIN].pop(entry.entry_id) | ||
return unloaded |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
"""Adds config flow for WeatherKit.""" | ||
from __future__ import annotations | ||
|
||
from collections.abc import Mapping | ||
from typing import Any | ||
|
||
from apple_weatherkit.client import ( | ||
WeatherKitApiClient, | ||
WeatherKitApiClientAuthenticationError, | ||
WeatherKitApiClientCommunicationError, | ||
WeatherKitApiClientError, | ||
) | ||
import voluptuous as vol | ||
|
||
from homeassistant import config_entries, data_entry_flow | ||
from homeassistant.const import CONF_LATITUDE, CONF_LOCATION, CONF_LONGITUDE | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
from homeassistant.helpers.selector import ( | ||
LocationSelector, | ||
LocationSelectorConfig, | ||
TextSelector, | ||
TextSelectorConfig, | ||
) | ||
|
||
from .const import ( | ||
CONF_KEY_ID, | ||
CONF_KEY_PEM, | ||
CONF_SERVICE_ID, | ||
CONF_TEAM_ID, | ||
DOMAIN, | ||
LOGGER, | ||
) | ||
|
||
DATA_SCHEMA = vol.Schema( | ||
{ | ||
vol.Required(CONF_LOCATION): LocationSelector( | ||
LocationSelectorConfig(radius=False, icon="") | ||
), | ||
# Auth | ||
vol.Required(CONF_KEY_ID): str, | ||
vol.Required(CONF_SERVICE_ID): str, | ||
vol.Required(CONF_TEAM_ID): str, | ||
vol.Required(CONF_KEY_PEM): TextSelector( | ||
TextSelectorConfig( | ||
multiline=True, | ||
) | ||
), | ||
} | ||
) | ||
|
||
|
||
class WeatherKitUnsupportedLocationError(Exception): | ||
"""Error to indicate a location is unsupported.""" | ||
|
||
|
||
class WeatherKitFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): | ||
"""Config flow for WeatherKit.""" | ||
|
||
VERSION = 1 | ||
|
||
async def async_step_user( | ||
self, | ||
user_input: dict[str, Any] | None = None, | ||
) -> data_entry_flow.FlowResult: | ||
"""Handle a flow initialized by the user.""" | ||
errors = {} | ||
if user_input is not None: | ||
try: | ||
await self._test_config(user_input) | ||
except WeatherKitUnsupportedLocationError as exception: | ||
LOGGER.error(exception) | ||
errors["base"] = "unsupported_location" | ||
except WeatherKitApiClientAuthenticationError as exception: | ||
LOGGER.warning(exception) | ||
errors["base"] = "invalid_auth" | ||
except WeatherKitApiClientCommunicationError as exception: | ||
LOGGER.error(exception) | ||
errors["base"] = "cannot_connect" | ||
except WeatherKitApiClientError as exception: | ||
LOGGER.exception(exception) | ||
errors["base"] = "unknown" | ||
else: | ||
# Flatten location | ||
location = user_input.pop(CONF_LOCATION) | ||
user_input[CONF_LATITUDE] = location[CONF_LATITUDE] | ||
user_input[CONF_LONGITUDE] = location[CONF_LONGITUDE] | ||
|
||
return self.async_create_entry( | ||
title=f"{user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}", | ||
data=user_input, | ||
) | ||
|
||
suggested_values: Mapping[str, Any] = { | ||
CONF_LOCATION: { | ||
CONF_LATITUDE: self.hass.config.latitude, | ||
CONF_LONGITUDE: self.hass.config.longitude, | ||
} | ||
} | ||
|
||
data_schema = self.add_suggested_values_to_schema(DATA_SCHEMA, suggested_values) | ||
return self.async_show_form( | ||
step_id="user", | ||
data_schema=data_schema, | ||
errors=errors, | ||
) | ||
|
||
async def _test_config(self, user_input: dict[str, Any]) -> None: | ||
"""Validate credentials.""" | ||
client = WeatherKitApiClient( | ||
key_id=user_input[CONF_KEY_ID], | ||
service_id=user_input[CONF_SERVICE_ID], | ||
team_id=user_input[CONF_TEAM_ID], | ||
key_pem=user_input[CONF_KEY_PEM], | ||
session=async_get_clientsession(self.hass), | ||
) | ||
|
||
location = user_input[CONF_LOCATION] | ||
availability = await client.get_availability( | ||
location[CONF_LATITUDE], | ||
location[CONF_LONGITUDE], | ||
) | ||
|
||
if len(availability) == 0: | ||
raise WeatherKitUnsupportedLocationError( | ||
"API does not support this location" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
"""Constants for WeatherKit.""" | ||
from logging import Logger, getLogger | ||
|
||
LOGGER: Logger = getLogger(__package__) | ||
|
||
NAME = "Apple WeatherKit" | ||
DOMAIN = "weatherkit" | ||
ATTRIBUTION = "Data provided by Apple Weather. https://developer.apple.com/weatherkit/data-source-attribution/" | ||
|
||
CONF_KEY_ID = "key_id" | ||
CONF_SERVICE_ID = "service_id" | ||
CONF_TEAM_ID = "team_id" | ||
CONF_KEY_PEM = "key_pem" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
"""DataUpdateCoordinator for WeatherKit integration.""" | ||
from __future__ import annotations | ||
|
||
from datetime import timedelta | ||
|
||
from apple_weatherkit import DataSetType | ||
from apple_weatherkit.client import WeatherKitApiClient, WeatherKitApiClientError | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||
|
||
from .const import DOMAIN, LOGGER | ||
|
||
REQUESTED_DATA_SETS = [ | ||
DataSetType.CURRENT_WEATHER, | ||
DataSetType.DAILY_FORECAST, | ||
DataSetType.HOURLY_FORECAST, | ||
] | ||
|
||
|
||
class WeatherKitDataUpdateCoordinator(DataUpdateCoordinator): | ||
"""Class to manage fetching data from the API.""" | ||
|
||
config_entry: ConfigEntry | ||
supported_data_sets: list[DataSetType] | None = None | ||
|
||
def __init__( | ||
self, | ||
hass: HomeAssistant, | ||
client: WeatherKitApiClient, | ||
) -> None: | ||
"""Initialize.""" | ||
self.client = client | ||
super().__init__( | ||
hass=hass, | ||
logger=LOGGER, | ||
name=DOMAIN, | ||
update_interval=timedelta(minutes=15), | ||
) | ||
|
||
async def update_supported_data_sets(self): | ||
"""Obtain the supported data sets for this location and store them.""" | ||
supported_data_sets = await self.client.get_availability( | ||
self.config_entry.data[CONF_LATITUDE], | ||
self.config_entry.data[CONF_LONGITUDE], | ||
) | ||
|
||
self.supported_data_sets = [ | ||
data_set | ||
for data_set in REQUESTED_DATA_SETS | ||
if data_set in supported_data_sets | ||
] | ||
|
||
LOGGER.debug("Supported data sets: %s", self.supported_data_sets) | ||
|
||
async def _async_update_data(self): | ||
"""Update the current weather and forecasts.""" | ||
try: | ||
if not self.supported_data_sets: | ||
await self.update_supported_data_sets() | ||
|
||
return await self.client.get_weather_data( | ||
self.config_entry.data[CONF_LATITUDE], | ||
self.config_entry.data[CONF_LONGITUDE], | ||
self.supported_data_sets, | ||
) | ||
except WeatherKitApiClientError as exception: | ||
raise UpdateFailed(exception) from exception |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"domain": "weatherkit", | ||
"name": "Apple WeatherKit", | ||
"codeowners": ["@tjhorner"], | ||
"config_flow": true, | ||
"documentation": "https://www.home-assistant.io/integrations/weatherkit", | ||
"iot_class": "cloud_polling", | ||
"requirements": ["apple_weatherkit==1.0.1"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"config": { | ||
"step": { | ||
"user": { | ||
"title": "WeatherKit setup", | ||
"description": "Enter your location details and WeatherKit authentication credentials below.", | ||
"data": { | ||
"name": "Name", | ||
"location": "[%key:common::config_flow::data::location%]", | ||
"key_id": "Key ID", | ||
"team_id": "Apple team ID", | ||
"service_id": "Service ID", | ||
"key_pem": "Private key (.p8)" | ||
} | ||
} | ||
}, | ||
"error": { | ||
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]", | ||
"unsupported_location": "Apple WeatherKit does not provide data for this location.", | ||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", | ||
"unknown": "[%key:common::config_flow::error::unknown%]", | ||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" | ||
} | ||
} | ||
} |
Oops, something went wrong.