Skip to content

Commit 3aff225

Browse files
tronikosjoostlek
andauthored
Add Google Weather integration (home-assistant#147015)
Co-authored-by: Joost Lekkerkerker <[email protected]>
1 parent 04458e0 commit 3aff225

26 files changed

+2287
-0
lines changed

.strict-typing

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ homeassistant.components.google_cloud.*
231231
homeassistant.components.google_drive.*
232232
homeassistant.components.google_photos.*
233233
homeassistant.components.google_sheets.*
234+
homeassistant.components.google_weather.*
234235
homeassistant.components.govee_ble.*
235236
homeassistant.components.gpsd.*
236237
homeassistant.components.greeneye_monitor.*

CODEOWNERS

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

homeassistant/brands/google.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"google_tasks",
1616
"google_translate",
1717
"google_travel_time",
18+
"google_weather",
1819
"google_wifi",
1920
"google",
2021
"nest",
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""The Google Weather integration."""
2+
3+
from __future__ import annotations
4+
5+
import asyncio
6+
7+
from google_weather_api import GoogleWeatherApi
8+
9+
from homeassistant.const import CONF_API_KEY, Platform
10+
from homeassistant.core import HomeAssistant
11+
from homeassistant.helpers.aiohttp_client import async_get_clientsession
12+
13+
from .const import CONF_REFERRER
14+
from .coordinator import (
15+
GoogleWeatherConfigEntry,
16+
GoogleWeatherCurrentConditionsCoordinator,
17+
GoogleWeatherDailyForecastCoordinator,
18+
GoogleWeatherHourlyForecastCoordinator,
19+
GoogleWeatherRuntimeData,
20+
GoogleWeatherSubEntryRuntimeData,
21+
)
22+
23+
_PLATFORMS: list[Platform] = [Platform.WEATHER]
24+
25+
26+
async def async_setup_entry(
27+
hass: HomeAssistant, entry: GoogleWeatherConfigEntry
28+
) -> bool:
29+
"""Set up Google Weather from a config entry."""
30+
31+
api = GoogleWeatherApi(
32+
session=async_get_clientsession(hass),
33+
api_key=entry.data[CONF_API_KEY],
34+
referrer=entry.data.get(CONF_REFERRER),
35+
language_code=hass.config.language,
36+
)
37+
subentries_runtime_data: dict[str, GoogleWeatherSubEntryRuntimeData] = {}
38+
for subentry in entry.subentries.values():
39+
subentry_runtime_data = GoogleWeatherSubEntryRuntimeData(
40+
coordinator_observation=GoogleWeatherCurrentConditionsCoordinator(
41+
hass, entry, subentry, api
42+
),
43+
coordinator_daily_forecast=GoogleWeatherDailyForecastCoordinator(
44+
hass, entry, subentry, api
45+
),
46+
coordinator_hourly_forecast=GoogleWeatherHourlyForecastCoordinator(
47+
hass, entry, subentry, api
48+
),
49+
)
50+
subentries_runtime_data[subentry.subentry_id] = subentry_runtime_data
51+
tasks = [
52+
coro
53+
for subentry_runtime_data in subentries_runtime_data.values()
54+
for coro in (
55+
subentry_runtime_data.coordinator_observation.async_config_entry_first_refresh(),
56+
subentry_runtime_data.coordinator_daily_forecast.async_config_entry_first_refresh(),
57+
subentry_runtime_data.coordinator_hourly_forecast.async_config_entry_first_refresh(),
58+
)
59+
]
60+
await asyncio.gather(*tasks)
61+
entry.runtime_data = GoogleWeatherRuntimeData(
62+
api=api,
63+
subentries_runtime_data=subentries_runtime_data,
64+
)
65+
66+
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
67+
68+
entry.async_on_unload(entry.add_update_listener(async_update_options))
69+
70+
return True
71+
72+
73+
async def async_unload_entry(
74+
hass: HomeAssistant, entry: GoogleWeatherConfigEntry
75+
) -> bool:
76+
"""Unload a config entry."""
77+
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
78+
79+
80+
async def async_update_options(
81+
hass: HomeAssistant, entry: GoogleWeatherConfigEntry
82+
) -> None:
83+
"""Update options."""
84+
await hass.config_entries.async_reload(entry.entry_id)
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
"""Config flow for the Google Weather integration."""
2+
3+
from __future__ import annotations
4+
5+
import logging
6+
from typing import Any
7+
8+
from google_weather_api import GoogleWeatherApi, GoogleWeatherApiError
9+
import voluptuous as vol
10+
11+
from homeassistant.config_entries import (
12+
ConfigEntry,
13+
ConfigEntryState,
14+
ConfigFlow,
15+
ConfigFlowResult,
16+
ConfigSubentryFlow,
17+
SubentryFlowResult,
18+
)
19+
from homeassistant.const import (
20+
CONF_API_KEY,
21+
CONF_LATITUDE,
22+
CONF_LOCATION,
23+
CONF_LONGITUDE,
24+
CONF_NAME,
25+
)
26+
from homeassistant.core import HomeAssistant, callback
27+
from homeassistant.data_entry_flow import section
28+
from homeassistant.helpers.aiohttp_client import async_get_clientsession
29+
from homeassistant.helpers.selector import LocationSelector, LocationSelectorConfig
30+
31+
from .const import CONF_REFERRER, DOMAIN, SECTION_API_KEY_OPTIONS
32+
33+
_LOGGER = logging.getLogger(__name__)
34+
35+
STEP_USER_DATA_SCHEMA = vol.Schema(
36+
{
37+
vol.Required(CONF_API_KEY): str,
38+
vol.Optional(SECTION_API_KEY_OPTIONS): section(
39+
vol.Schema({vol.Optional(CONF_REFERRER): str}), {"collapsed": True}
40+
),
41+
}
42+
)
43+
44+
45+
async def _validate_input(
46+
user_input: dict[str, Any],
47+
api: GoogleWeatherApi,
48+
errors: dict[str, str],
49+
description_placeholders: dict[str, str],
50+
) -> bool:
51+
try:
52+
await api.async_get_current_conditions(
53+
latitude=user_input[CONF_LOCATION][CONF_LATITUDE],
54+
longitude=user_input[CONF_LOCATION][CONF_LONGITUDE],
55+
)
56+
except GoogleWeatherApiError as err:
57+
errors["base"] = "cannot_connect"
58+
description_placeholders["error_message"] = str(err)
59+
except Exception:
60+
_LOGGER.exception("Unexpected exception")
61+
errors["base"] = "unknown"
62+
else:
63+
return True
64+
return False
65+
66+
67+
def _get_location_schema(hass: HomeAssistant) -> vol.Schema:
68+
"""Return the schema for a location with default values from the hass config."""
69+
return vol.Schema(
70+
{
71+
vol.Required(CONF_NAME, default=hass.config.location_name): str,
72+
vol.Required(
73+
CONF_LOCATION,
74+
default={
75+
CONF_LATITUDE: hass.config.latitude,
76+
CONF_LONGITUDE: hass.config.longitude,
77+
},
78+
): LocationSelector(LocationSelectorConfig(radius=False)),
79+
}
80+
)
81+
82+
83+
def _is_location_already_configured(
84+
hass: HomeAssistant, new_data: dict[str, float], epsilon: float = 1e-4
85+
) -> bool:
86+
"""Check if the location is already configured."""
87+
for entry in hass.config_entries.async_entries(DOMAIN):
88+
for subentry in entry.subentries.values():
89+
# A more accurate way is to use the haversine formula, but for simplicity
90+
# we use a simple distance check. The epsilon value is small anyway.
91+
# This is mostly to capture cases where the user has slightly moved the location pin.
92+
if (
93+
abs(subentry.data[CONF_LATITUDE] - new_data[CONF_LATITUDE]) <= epsilon
94+
and abs(subentry.data[CONF_LONGITUDE] - new_data[CONF_LONGITUDE])
95+
<= epsilon
96+
):
97+
return True
98+
return False
99+
100+
101+
class GoogleWeatherConfigFlow(ConfigFlow, domain=DOMAIN):
102+
"""Handle a config flow for Google Weather."""
103+
104+
VERSION = 1
105+
106+
async def async_step_user(
107+
self, user_input: dict[str, Any] | None = None
108+
) -> ConfigFlowResult:
109+
"""Handle the initial step."""
110+
errors: dict[str, str] = {}
111+
description_placeholders: dict[str, str] = {
112+
"api_key_url": "https://developers.google.com/maps/documentation/weather/get-api-key",
113+
"restricting_api_keys_url": "https://developers.google.com/maps/api-security-best-practices#restricting-api-keys",
114+
}
115+
if user_input is not None:
116+
api_key = user_input[CONF_API_KEY]
117+
referrer = user_input.get(SECTION_API_KEY_OPTIONS, {}).get(CONF_REFERRER)
118+
self._async_abort_entries_match({CONF_API_KEY: api_key})
119+
if _is_location_already_configured(self.hass, user_input[CONF_LOCATION]):
120+
return self.async_abort(reason="already_configured")
121+
api = GoogleWeatherApi(
122+
session=async_get_clientsession(self.hass),
123+
api_key=api_key,
124+
referrer=referrer,
125+
language_code=self.hass.config.language,
126+
)
127+
if await _validate_input(user_input, api, errors, description_placeholders):
128+
return self.async_create_entry(
129+
title="Google Weather",
130+
data={
131+
CONF_API_KEY: api_key,
132+
CONF_REFERRER: referrer,
133+
},
134+
subentries=[
135+
{
136+
"subentry_type": "location",
137+
"data": user_input[CONF_LOCATION],
138+
"title": user_input[CONF_NAME],
139+
"unique_id": None,
140+
},
141+
],
142+
)
143+
else:
144+
user_input = {}
145+
schema = STEP_USER_DATA_SCHEMA.schema.copy()
146+
schema.update(_get_location_schema(self.hass).schema)
147+
return self.async_show_form(
148+
step_id="user",
149+
data_schema=self.add_suggested_values_to_schema(
150+
vol.Schema(schema), user_input
151+
),
152+
errors=errors,
153+
description_placeholders=description_placeholders,
154+
)
155+
156+
@classmethod
157+
@callback
158+
def async_get_supported_subentry_types(
159+
cls, config_entry: ConfigEntry
160+
) -> dict[str, type[ConfigSubentryFlow]]:
161+
"""Return subentries supported by this integration."""
162+
return {"location": LocationSubentryFlowHandler}
163+
164+
165+
class LocationSubentryFlowHandler(ConfigSubentryFlow):
166+
"""Handle a subentry flow for location."""
167+
168+
async def async_step_location(
169+
self,
170+
user_input: dict[str, Any] | None = None,
171+
) -> SubentryFlowResult:
172+
"""Handle the location step."""
173+
if self._get_entry().state != ConfigEntryState.LOADED:
174+
return self.async_abort(reason="entry_not_loaded")
175+
176+
errors: dict[str, str] = {}
177+
description_placeholders: dict[str, str] = {}
178+
if user_input is not None:
179+
if _is_location_already_configured(self.hass, user_input[CONF_LOCATION]):
180+
return self.async_abort(reason="already_configured")
181+
api: GoogleWeatherApi = self._get_entry().runtime_data.api
182+
if await _validate_input(user_input, api, errors, description_placeholders):
183+
return self.async_create_entry(
184+
title=user_input[CONF_NAME],
185+
data=user_input[CONF_LOCATION],
186+
)
187+
else:
188+
user_input = {}
189+
return self.async_show_form(
190+
step_id="location",
191+
data_schema=self.add_suggested_values_to_schema(
192+
_get_location_schema(self.hass), user_input
193+
),
194+
errors=errors,
195+
description_placeholders=description_placeholders,
196+
)
197+
198+
async_step_user = async_step_location
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Constants for the Google Weather integration."""
2+
3+
from typing import Final
4+
5+
DOMAIN = "google_weather"
6+
7+
SECTION_API_KEY_OPTIONS: Final = "api_key_options"
8+
CONF_REFERRER: Final = "referrer"

0 commit comments

Comments
 (0)