mirror of
https://github.com/home-assistant/core.git
synced 2025-12-20 02:48:57 +00:00
Add Google Weather integration (#147015)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
@@ -231,6 +231,7 @@ homeassistant.components.google_cloud.*
|
||||
homeassistant.components.google_drive.*
|
||||
homeassistant.components.google_photos.*
|
||||
homeassistant.components.google_sheets.*
|
||||
homeassistant.components.google_weather.*
|
||||
homeassistant.components.govee_ble.*
|
||||
homeassistant.components.gpsd.*
|
||||
homeassistant.components.greeneye_monitor.*
|
||||
|
||||
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -607,6 +607,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/google_tasks/ @allenporter
|
||||
/homeassistant/components/google_travel_time/ @eifinger
|
||||
/tests/components/google_travel_time/ @eifinger
|
||||
/homeassistant/components/google_weather/ @tronikos
|
||||
/tests/components/google_weather/ @tronikos
|
||||
/homeassistant/components/govee_ble/ @bdraco
|
||||
/tests/components/govee_ble/ @bdraco
|
||||
/homeassistant/components/govee_light_local/ @Galorhallen
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"google_tasks",
|
||||
"google_translate",
|
||||
"google_travel_time",
|
||||
"google_weather",
|
||||
"google_wifi",
|
||||
"google",
|
||||
"nest",
|
||||
|
||||
84
homeassistant/components/google_weather/__init__.py
Normal file
84
homeassistant/components/google_weather/__init__.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""The Google Weather integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from google_weather_api import GoogleWeatherApi
|
||||
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_REFERRER
|
||||
from .coordinator import (
|
||||
GoogleWeatherConfigEntry,
|
||||
GoogleWeatherCurrentConditionsCoordinator,
|
||||
GoogleWeatherDailyForecastCoordinator,
|
||||
GoogleWeatherHourlyForecastCoordinator,
|
||||
GoogleWeatherRuntimeData,
|
||||
GoogleWeatherSubEntryRuntimeData,
|
||||
)
|
||||
|
||||
_PLATFORMS: list[Platform] = [Platform.WEATHER]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: GoogleWeatherConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Google Weather from a config entry."""
|
||||
|
||||
api = GoogleWeatherApi(
|
||||
session=async_get_clientsession(hass),
|
||||
api_key=entry.data[CONF_API_KEY],
|
||||
referrer=entry.data.get(CONF_REFERRER),
|
||||
language_code=hass.config.language,
|
||||
)
|
||||
subentries_runtime_data: dict[str, GoogleWeatherSubEntryRuntimeData] = {}
|
||||
for subentry in entry.subentries.values():
|
||||
subentry_runtime_data = GoogleWeatherSubEntryRuntimeData(
|
||||
coordinator_observation=GoogleWeatherCurrentConditionsCoordinator(
|
||||
hass, entry, subentry, api
|
||||
),
|
||||
coordinator_daily_forecast=GoogleWeatherDailyForecastCoordinator(
|
||||
hass, entry, subentry, api
|
||||
),
|
||||
coordinator_hourly_forecast=GoogleWeatherHourlyForecastCoordinator(
|
||||
hass, entry, subentry, api
|
||||
),
|
||||
)
|
||||
subentries_runtime_data[subentry.subentry_id] = subentry_runtime_data
|
||||
tasks = [
|
||||
coro
|
||||
for subentry_runtime_data in subentries_runtime_data.values()
|
||||
for coro in (
|
||||
subentry_runtime_data.coordinator_observation.async_config_entry_first_refresh(),
|
||||
subentry_runtime_data.coordinator_daily_forecast.async_config_entry_first_refresh(),
|
||||
subentry_runtime_data.coordinator_hourly_forecast.async_config_entry_first_refresh(),
|
||||
)
|
||||
]
|
||||
await asyncio.gather(*tasks)
|
||||
entry.runtime_data = GoogleWeatherRuntimeData(
|
||||
api=api,
|
||||
subentries_runtime_data=subentries_runtime_data,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(async_update_options))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: GoogleWeatherConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
||||
|
||||
|
||||
async def async_update_options(
|
||||
hass: HomeAssistant, entry: GoogleWeatherConfigEntry
|
||||
) -> None:
|
||||
"""Update options."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
198
homeassistant/components/google_weather/config_flow.py
Normal file
198
homeassistant/components/google_weather/config_flow.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""Config flow for the Google Weather integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from google_weather_api import GoogleWeatherApi, GoogleWeatherApiError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigEntryState,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
ConfigSubentryFlow,
|
||||
SubentryFlowResult,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_LATITUDE,
|
||||
CONF_LOCATION,
|
||||
CONF_LONGITUDE,
|
||||
CONF_NAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import section
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import LocationSelector, LocationSelectorConfig
|
||||
|
||||
from .const import CONF_REFERRER, DOMAIN, SECTION_API_KEY_OPTIONS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Optional(SECTION_API_KEY_OPTIONS): section(
|
||||
vol.Schema({vol.Optional(CONF_REFERRER): str}), {"collapsed": True}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def _validate_input(
|
||||
user_input: dict[str, Any],
|
||||
api: GoogleWeatherApi,
|
||||
errors: dict[str, str],
|
||||
description_placeholders: dict[str, str],
|
||||
) -> bool:
|
||||
try:
|
||||
await api.async_get_current_conditions(
|
||||
latitude=user_input[CONF_LOCATION][CONF_LATITUDE],
|
||||
longitude=user_input[CONF_LOCATION][CONF_LONGITUDE],
|
||||
)
|
||||
except GoogleWeatherApiError as err:
|
||||
errors["base"] = "cannot_connect"
|
||||
description_placeholders["error_message"] = str(err)
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _get_location_schema(hass: HomeAssistant) -> vol.Schema:
|
||||
"""Return the schema for a location with default values from the hass config."""
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME, default=hass.config.location_name): str,
|
||||
vol.Required(
|
||||
CONF_LOCATION,
|
||||
default={
|
||||
CONF_LATITUDE: hass.config.latitude,
|
||||
CONF_LONGITUDE: hass.config.longitude,
|
||||
},
|
||||
): LocationSelector(LocationSelectorConfig(radius=False)),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _is_location_already_configured(
|
||||
hass: HomeAssistant, new_data: dict[str, float], epsilon: float = 1e-4
|
||||
) -> bool:
|
||||
"""Check if the location is already configured."""
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
for subentry in entry.subentries.values():
|
||||
# A more accurate way is to use the haversine formula, but for simplicity
|
||||
# we use a simple distance check. The epsilon value is small anyway.
|
||||
# This is mostly to capture cases where the user has slightly moved the location pin.
|
||||
if (
|
||||
abs(subentry.data[CONF_LATITUDE] - new_data[CONF_LATITUDE]) <= epsilon
|
||||
and abs(subentry.data[CONF_LONGITUDE] - new_data[CONF_LONGITUDE])
|
||||
<= epsilon
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class GoogleWeatherConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Google Weather."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders: dict[str, str] = {
|
||||
"api_key_url": "https://developers.google.com/maps/documentation/weather/get-api-key",
|
||||
"restricting_api_keys_url": "https://developers.google.com/maps/api-security-best-practices#restricting-api-keys",
|
||||
}
|
||||
if user_input is not None:
|
||||
api_key = user_input[CONF_API_KEY]
|
||||
referrer = user_input.get(SECTION_API_KEY_OPTIONS, {}).get(CONF_REFERRER)
|
||||
self._async_abort_entries_match({CONF_API_KEY: api_key})
|
||||
if _is_location_already_configured(self.hass, user_input[CONF_LOCATION]):
|
||||
return self.async_abort(reason="already_configured")
|
||||
api = GoogleWeatherApi(
|
||||
session=async_get_clientsession(self.hass),
|
||||
api_key=api_key,
|
||||
referrer=referrer,
|
||||
language_code=self.hass.config.language,
|
||||
)
|
||||
if await _validate_input(user_input, api, errors, description_placeholders):
|
||||
return self.async_create_entry(
|
||||
title="Google Weather",
|
||||
data={
|
||||
CONF_API_KEY: api_key,
|
||||
CONF_REFERRER: referrer,
|
||||
},
|
||||
subentries=[
|
||||
{
|
||||
"subentry_type": "location",
|
||||
"data": user_input[CONF_LOCATION],
|
||||
"title": user_input[CONF_NAME],
|
||||
"unique_id": None,
|
||||
},
|
||||
],
|
||||
)
|
||||
else:
|
||||
user_input = {}
|
||||
schema = STEP_USER_DATA_SCHEMA.schema.copy()
|
||||
schema.update(_get_location_schema(self.hass).schema)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
vol.Schema(schema), user_input
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@callback
|
||||
def async_get_supported_subentry_types(
|
||||
cls, config_entry: ConfigEntry
|
||||
) -> dict[str, type[ConfigSubentryFlow]]:
|
||||
"""Return subentries supported by this integration."""
|
||||
return {"location": LocationSubentryFlowHandler}
|
||||
|
||||
|
||||
class LocationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
"""Handle a subentry flow for location."""
|
||||
|
||||
async def async_step_location(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> SubentryFlowResult:
|
||||
"""Handle the location step."""
|
||||
if self._get_entry().state != ConfigEntryState.LOADED:
|
||||
return self.async_abort(reason="entry_not_loaded")
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
if _is_location_already_configured(self.hass, user_input[CONF_LOCATION]):
|
||||
return self.async_abort(reason="already_configured")
|
||||
api: GoogleWeatherApi = self._get_entry().runtime_data.api
|
||||
if await _validate_input(user_input, api, errors, description_placeholders):
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_NAME],
|
||||
data=user_input[CONF_LOCATION],
|
||||
)
|
||||
else:
|
||||
user_input = {}
|
||||
return self.async_show_form(
|
||||
step_id="location",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
_get_location_schema(self.hass), user_input
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=description_placeholders,
|
||||
)
|
||||
|
||||
async_step_user = async_step_location
|
||||
8
homeassistant/components/google_weather/const.py
Normal file
8
homeassistant/components/google_weather/const.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""Constants for the Google Weather integration."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
DOMAIN = "google_weather"
|
||||
|
||||
SECTION_API_KEY_OPTIONS: Final = "api_key_options"
|
||||
CONF_REFERRER: Final = "referrer"
|
||||
169
homeassistant/components/google_weather/coordinator.py
Normal file
169
homeassistant/components/google_weather/coordinator.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""The Google Weather coordinator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import TypeVar
|
||||
|
||||
from google_weather_api import (
|
||||
CurrentConditionsResponse,
|
||||
DailyForecastResponse,
|
||||
GoogleWeatherApi,
|
||||
GoogleWeatherApiError,
|
||||
HourlyForecastResponse,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
|
||||
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
TimestampDataUpdateCoordinator,
|
||||
UpdateFailed,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
T = TypeVar(
|
||||
"T",
|
||||
bound=(
|
||||
CurrentConditionsResponse
|
||||
| DailyForecastResponse
|
||||
| HourlyForecastResponse
|
||||
| None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GoogleWeatherSubEntryRuntimeData:
|
||||
"""Runtime data for a Google Weather sub-entry."""
|
||||
|
||||
coordinator_observation: GoogleWeatherCurrentConditionsCoordinator
|
||||
coordinator_daily_forecast: GoogleWeatherDailyForecastCoordinator
|
||||
coordinator_hourly_forecast: GoogleWeatherHourlyForecastCoordinator
|
||||
|
||||
|
||||
@dataclass
|
||||
class GoogleWeatherRuntimeData:
|
||||
"""Runtime data for the Google Weather integration."""
|
||||
|
||||
api: GoogleWeatherApi
|
||||
subentries_runtime_data: dict[str, GoogleWeatherSubEntryRuntimeData]
|
||||
|
||||
|
||||
type GoogleWeatherConfigEntry = ConfigEntry[GoogleWeatherRuntimeData]
|
||||
|
||||
|
||||
class GoogleWeatherBaseCoordinator(TimestampDataUpdateCoordinator[T]):
|
||||
"""Base class for Google Weather coordinators."""
|
||||
|
||||
config_entry: GoogleWeatherConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: GoogleWeatherConfigEntry,
|
||||
subentry: ConfigSubentry,
|
||||
data_type_name: str,
|
||||
update_interval: timedelta,
|
||||
api_method: Callable[..., Awaitable[T]],
|
||||
) -> None:
|
||||
"""Initialize the data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=f"Google Weather {data_type_name} coordinator for {subentry.title}",
|
||||
update_interval=update_interval,
|
||||
)
|
||||
self.subentry = subentry
|
||||
self._data_type_name = data_type_name
|
||||
self._api_method = api_method
|
||||
|
||||
async def _async_update_data(self) -> T:
|
||||
"""Fetch data from API and handle errors."""
|
||||
try:
|
||||
return await self._api_method(
|
||||
self.subentry.data[CONF_LATITUDE],
|
||||
self.subentry.data[CONF_LONGITUDE],
|
||||
)
|
||||
except GoogleWeatherApiError as err:
|
||||
_LOGGER.error(
|
||||
"Error fetching %s for %s: %s",
|
||||
self._data_type_name,
|
||||
self.subentry.title,
|
||||
err,
|
||||
)
|
||||
raise UpdateFailed(f"Error fetching {self._data_type_name}") from err
|
||||
|
||||
|
||||
class GoogleWeatherCurrentConditionsCoordinator(
|
||||
GoogleWeatherBaseCoordinator[CurrentConditionsResponse]
|
||||
):
|
||||
"""Handle fetching current weather conditions."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: GoogleWeatherConfigEntry,
|
||||
subentry: ConfigSubentry,
|
||||
api: GoogleWeatherApi,
|
||||
) -> None:
|
||||
"""Initialize the data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
config_entry,
|
||||
subentry,
|
||||
"current weather conditions",
|
||||
timedelta(minutes=15),
|
||||
api.async_get_current_conditions,
|
||||
)
|
||||
|
||||
|
||||
class GoogleWeatherDailyForecastCoordinator(
|
||||
GoogleWeatherBaseCoordinator[DailyForecastResponse]
|
||||
):
|
||||
"""Handle fetching daily weather forecast."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: GoogleWeatherConfigEntry,
|
||||
subentry: ConfigSubentry,
|
||||
api: GoogleWeatherApi,
|
||||
) -> None:
|
||||
"""Initialize the data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
config_entry,
|
||||
subentry,
|
||||
"daily weather forecast",
|
||||
timedelta(hours=1),
|
||||
api.async_get_daily_forecast,
|
||||
)
|
||||
|
||||
|
||||
class GoogleWeatherHourlyForecastCoordinator(
|
||||
GoogleWeatherBaseCoordinator[HourlyForecastResponse]
|
||||
):
|
||||
"""Handle fetching hourly weather forecast."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: GoogleWeatherConfigEntry,
|
||||
subentry: ConfigSubentry,
|
||||
api: GoogleWeatherApi,
|
||||
) -> None:
|
||||
"""Initialize the data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
config_entry,
|
||||
subentry,
|
||||
"hourly weather forecast",
|
||||
timedelta(hours=1),
|
||||
api.async_get_hourly_forecast,
|
||||
)
|
||||
28
homeassistant/components/google_weather/entity.py
Normal file
28
homeassistant/components/google_weather/entity.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""Base entity for Google Weather."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import GoogleWeatherConfigEntry
|
||||
|
||||
|
||||
class GoogleWeatherBaseEntity(Entity):
|
||||
"""Base entity for all Google Weather entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self, config_entry: GoogleWeatherConfigEntry, subentry: ConfigSubentry
|
||||
) -> None:
|
||||
"""Initialize base entity."""
|
||||
self._attr_unique_id = subentry.subentry_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, subentry.subentry_id)},
|
||||
name=subentry.title,
|
||||
manufacturer="Google",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
12
homeassistant/components/google_weather/manifest.json
Normal file
12
homeassistant/components/google_weather/manifest.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "google_weather",
|
||||
"name": "Google Weather",
|
||||
"codeowners": ["@tronikos"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/google_weather",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["google_weather_api"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["python-google-weather-api==0.0.4"]
|
||||
}
|
||||
82
homeassistant/components/google_weather/quality_scale.yaml
Normal file
82
homeassistant/components/google_weather/quality_scale.yaml
Normal file
@@ -0,0 +1,82 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: No actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: No actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: No events subscribed.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: No actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: No configuration options.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: No discovery.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: No discovery.
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices:
|
||||
status: exempt
|
||||
comment: No physical devices.
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: N/A
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: No repairs.
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: N/A
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
65
homeassistant/components/google_weather/strings.json
Normal file
65
homeassistant/components/google_weather/strings.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Unable to connect to the Google Weather API:\n\n{error_message}",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"location": "[%key:common::config_flow::data::location%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "A unique alphanumeric string that associates your Google billing account with Google Weather API",
|
||||
"location": "Location coordinates",
|
||||
"name": "Location name"
|
||||
},
|
||||
"description": "Get your API key from [here]({api_key_url}).",
|
||||
"sections": {
|
||||
"api_key_options": {
|
||||
"data": {
|
||||
"referrer": "HTTP referrer"
|
||||
},
|
||||
"data_description": {
|
||||
"referrer": "Specify this only if the API key has a [website application restriction]({restricting_api_keys_url})"
|
||||
},
|
||||
"name": "Optional API key options"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"config_subentries": {
|
||||
"location": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
|
||||
"entry_not_loaded": "Cannot add things while the configuration is disabled."
|
||||
},
|
||||
"entry_type": "Location",
|
||||
"error": {
|
||||
"cannot_connect": "[%key:component::google_weather::config::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"initiate_flow": {
|
||||
"user": "Add location"
|
||||
},
|
||||
"step": {
|
||||
"location": {
|
||||
"data": {
|
||||
"location": "[%key:common::config_flow::data::location%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"data_description": {
|
||||
"location": "[%key:component::google_weather::config::step::user::data_description::location%]",
|
||||
"name": "[%key:component::google_weather::config::step::user::data_description::name%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
366
homeassistant/components/google_weather/weather.py
Normal file
366
homeassistant/components/google_weather/weather.py
Normal file
@@ -0,0 +1,366 @@
|
||||
"""Weather entity."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from google_weather_api import (
|
||||
DailyForecastResponse,
|
||||
HourlyForecastResponse,
|
||||
WeatherCondition,
|
||||
)
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
ATTR_CONDITION_CLOUDY,
|
||||
ATTR_CONDITION_HAIL,
|
||||
ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
ATTR_CONDITION_PARTLYCLOUDY,
|
||||
ATTR_CONDITION_POURING,
|
||||
ATTR_CONDITION_RAINY,
|
||||
ATTR_CONDITION_SNOWY,
|
||||
ATTR_CONDITION_SNOWY_RAINY,
|
||||
ATTR_CONDITION_SUNNY,
|
||||
ATTR_CONDITION_WINDY,
|
||||
ATTR_FORECAST_CLOUD_COVERAGE,
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_HUMIDITY,
|
||||
ATTR_FORECAST_IS_DAYTIME,
|
||||
ATTR_FORECAST_NATIVE_APPARENT_TEMP,
|
||||
ATTR_FORECAST_NATIVE_DEW_POINT,
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION,
|
||||
ATTR_FORECAST_NATIVE_PRESSURE,
|
||||
ATTR_FORECAST_NATIVE_TEMP,
|
||||
ATTR_FORECAST_NATIVE_TEMP_LOW,
|
||||
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED,
|
||||
ATTR_FORECAST_NATIVE_WIND_SPEED,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY,
|
||||
ATTR_FORECAST_TIME,
|
||||
ATTR_FORECAST_UV_INDEX,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
CoordinatorWeatherEntity,
|
||||
Forecast,
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.const import (
|
||||
UnitOfLength,
|
||||
UnitOfPrecipitationDepth,
|
||||
UnitOfPressure,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import (
|
||||
GoogleWeatherConfigEntry,
|
||||
GoogleWeatherCurrentConditionsCoordinator,
|
||||
GoogleWeatherDailyForecastCoordinator,
|
||||
GoogleWeatherHourlyForecastCoordinator,
|
||||
)
|
||||
from .entity import GoogleWeatherBaseEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
# Maps https://developers.google.com/maps/documentation/weather/weather-condition-icons
|
||||
# to https://developers.home-assistant.io/docs/core/entity/weather/#recommended-values-for-state-and-condition
|
||||
_CONDITION_MAP: dict[WeatherCondition.Type, str | None] = {
|
||||
WeatherCondition.Type.TYPE_UNSPECIFIED: None,
|
||||
WeatherCondition.Type.CLEAR: ATTR_CONDITION_SUNNY,
|
||||
WeatherCondition.Type.MOSTLY_CLEAR: ATTR_CONDITION_PARTLYCLOUDY,
|
||||
WeatherCondition.Type.PARTLY_CLOUDY: ATTR_CONDITION_PARTLYCLOUDY,
|
||||
WeatherCondition.Type.MOSTLY_CLOUDY: ATTR_CONDITION_CLOUDY,
|
||||
WeatherCondition.Type.CLOUDY: ATTR_CONDITION_CLOUDY,
|
||||
WeatherCondition.Type.WINDY: ATTR_CONDITION_WINDY,
|
||||
WeatherCondition.Type.WIND_AND_RAIN: ATTR_CONDITION_RAINY,
|
||||
WeatherCondition.Type.LIGHT_RAIN_SHOWERS: ATTR_CONDITION_RAINY,
|
||||
WeatherCondition.Type.CHANCE_OF_SHOWERS: ATTR_CONDITION_RAINY,
|
||||
WeatherCondition.Type.SCATTERED_SHOWERS: ATTR_CONDITION_RAINY,
|
||||
WeatherCondition.Type.RAIN_SHOWERS: ATTR_CONDITION_RAINY,
|
||||
WeatherCondition.Type.HEAVY_RAIN_SHOWERS: ATTR_CONDITION_POURING,
|
||||
WeatherCondition.Type.LIGHT_TO_MODERATE_RAIN: ATTR_CONDITION_RAINY,
|
||||
WeatherCondition.Type.MODERATE_TO_HEAVY_RAIN: ATTR_CONDITION_POURING,
|
||||
WeatherCondition.Type.RAIN: ATTR_CONDITION_RAINY,
|
||||
WeatherCondition.Type.LIGHT_RAIN: ATTR_CONDITION_RAINY,
|
||||
WeatherCondition.Type.HEAVY_RAIN: ATTR_CONDITION_POURING,
|
||||
WeatherCondition.Type.RAIN_PERIODICALLY_HEAVY: ATTR_CONDITION_POURING,
|
||||
WeatherCondition.Type.LIGHT_SNOW_SHOWERS: ATTR_CONDITION_SNOWY,
|
||||
WeatherCondition.Type.CHANCE_OF_SNOW_SHOWERS: ATTR_CONDITION_SNOWY,
|
||||
WeatherCondition.Type.SCATTERED_SNOW_SHOWERS: ATTR_CONDITION_SNOWY,
|
||||
WeatherCondition.Type.SNOW_SHOWERS: ATTR_CONDITION_SNOWY,
|
||||
WeatherCondition.Type.HEAVY_SNOW_SHOWERS: ATTR_CONDITION_SNOWY,
|
||||
WeatherCondition.Type.LIGHT_TO_MODERATE_SNOW: ATTR_CONDITION_SNOWY,
|
||||
WeatherCondition.Type.MODERATE_TO_HEAVY_SNOW: ATTR_CONDITION_SNOWY,
|
||||
WeatherCondition.Type.SNOW: ATTR_CONDITION_SNOWY,
|
||||
WeatherCondition.Type.LIGHT_SNOW: ATTR_CONDITION_SNOWY,
|
||||
WeatherCondition.Type.HEAVY_SNOW: ATTR_CONDITION_SNOWY,
|
||||
WeatherCondition.Type.SNOWSTORM: ATTR_CONDITION_SNOWY,
|
||||
WeatherCondition.Type.SNOW_PERIODICALLY_HEAVY: ATTR_CONDITION_SNOWY,
|
||||
WeatherCondition.Type.HEAVY_SNOW_STORM: ATTR_CONDITION_SNOWY,
|
||||
WeatherCondition.Type.BLOWING_SNOW: ATTR_CONDITION_SNOWY,
|
||||
WeatherCondition.Type.RAIN_AND_SNOW: ATTR_CONDITION_SNOWY_RAINY,
|
||||
WeatherCondition.Type.HAIL: ATTR_CONDITION_HAIL,
|
||||
WeatherCondition.Type.HAIL_SHOWERS: ATTR_CONDITION_HAIL,
|
||||
WeatherCondition.Type.THUNDERSTORM: ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
WeatherCondition.Type.THUNDERSHOWER: ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
WeatherCondition.Type.LIGHT_THUNDERSTORM_RAIN: ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
WeatherCondition.Type.SCATTERED_THUNDERSTORMS: ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
WeatherCondition.Type.HEAVY_THUNDERSTORM: ATTR_CONDITION_LIGHTNING_RAINY,
|
||||
}
|
||||
|
||||
|
||||
def _get_condition(
|
||||
api_condition: WeatherCondition.Type, is_daytime: bool
|
||||
) -> str | None:
|
||||
"""Map Google Weather condition to Home Assistant condition."""
|
||||
cond = _CONDITION_MAP[api_condition]
|
||||
if cond == ATTR_CONDITION_SUNNY and not is_daytime:
|
||||
return ATTR_CONDITION_CLEAR_NIGHT
|
||||
return cond
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: GoogleWeatherConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add a weather entity from a config_entry."""
|
||||
for subentry in entry.subentries.values():
|
||||
async_add_entities(
|
||||
[GoogleWeatherEntity(entry, subentry)],
|
||||
config_subentry_id=subentry.subentry_id,
|
||||
)
|
||||
|
||||
|
||||
class GoogleWeatherEntity(
|
||||
CoordinatorWeatherEntity[
|
||||
GoogleWeatherCurrentConditionsCoordinator,
|
||||
GoogleWeatherDailyForecastCoordinator,
|
||||
GoogleWeatherHourlyForecastCoordinator,
|
||||
GoogleWeatherDailyForecastCoordinator,
|
||||
],
|
||||
GoogleWeatherBaseEntity,
|
||||
):
|
||||
"""Representation of a Google Weather entity."""
|
||||
|
||||
_attr_attribution = "Data from Google Weather"
|
||||
|
||||
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_native_pressure_unit = UnitOfPressure.MBAR
|
||||
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
|
||||
_attr_native_visibility_unit = UnitOfLength.KILOMETERS
|
||||
_attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
|
||||
|
||||
_attr_name = None
|
||||
|
||||
_attr_supported_features = (
|
||||
WeatherEntityFeature.FORECAST_DAILY
|
||||
| WeatherEntityFeature.FORECAST_HOURLY
|
||||
| WeatherEntityFeature.FORECAST_TWICE_DAILY
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
entry: GoogleWeatherConfigEntry,
|
||||
subentry: ConfigSubentry,
|
||||
) -> None:
|
||||
"""Initialize the weather entity."""
|
||||
subentry_runtime_data = entry.runtime_data.subentries_runtime_data[
|
||||
subentry.subentry_id
|
||||
]
|
||||
super().__init__(
|
||||
observation_coordinator=subentry_runtime_data.coordinator_observation,
|
||||
daily_coordinator=subentry_runtime_data.coordinator_daily_forecast,
|
||||
hourly_coordinator=subentry_runtime_data.coordinator_hourly_forecast,
|
||||
twice_daily_coordinator=subentry_runtime_data.coordinator_daily_forecast,
|
||||
)
|
||||
GoogleWeatherBaseEntity.__init__(self, entry, subentry)
|
||||
|
||||
@property
|
||||
def condition(self) -> str | None:
|
||||
"""Return the current condition."""
|
||||
return _get_condition(
|
||||
self.coordinator.data.weather_condition.type,
|
||||
self.coordinator.data.is_daytime,
|
||||
)
|
||||
|
||||
@property
|
||||
def native_temperature(self) -> float:
|
||||
"""Return the temperature."""
|
||||
return self.coordinator.data.temperature.degrees
|
||||
|
||||
@property
|
||||
def native_apparent_temperature(self) -> float:
|
||||
"""Return the apparent temperature."""
|
||||
return self.coordinator.data.feels_like_temperature.degrees
|
||||
|
||||
@property
|
||||
def native_dew_point(self) -> float:
|
||||
"""Return the dew point."""
|
||||
return self.coordinator.data.dew_point.degrees
|
||||
|
||||
@property
|
||||
def humidity(self) -> int:
|
||||
"""Return the humidity."""
|
||||
return self.coordinator.data.relative_humidity
|
||||
|
||||
@property
|
||||
def uv_index(self) -> float:
|
||||
"""Return the UV index."""
|
||||
return float(self.coordinator.data.uv_index)
|
||||
|
||||
@property
|
||||
def native_pressure(self) -> float:
|
||||
"""Return the pressure."""
|
||||
return self.coordinator.data.air_pressure.mean_sea_level_millibars
|
||||
|
||||
@property
|
||||
def native_wind_gust_speed(self) -> float:
|
||||
"""Return the wind gust speed."""
|
||||
return self.coordinator.data.wind.gust.value
|
||||
|
||||
@property
|
||||
def native_wind_speed(self) -> float:
|
||||
"""Return the wind speed."""
|
||||
return self.coordinator.data.wind.speed.value
|
||||
|
||||
@property
|
||||
def wind_bearing(self) -> int:
|
||||
"""Return the wind bearing."""
|
||||
return self.coordinator.data.wind.direction.degrees
|
||||
|
||||
@property
|
||||
def native_visibility(self) -> float:
|
||||
"""Return the visibility."""
|
||||
return self.coordinator.data.visibility.distance
|
||||
|
||||
@property
|
||||
def cloud_coverage(self) -> float:
|
||||
"""Return the Cloud coverage in %."""
|
||||
return float(self.coordinator.data.cloud_cover)
|
||||
|
||||
@callback
|
||||
def _async_forecast_daily(self) -> list[Forecast] | None:
|
||||
"""Return the daily forecast in native units."""
|
||||
coordinator = self.forecast_coordinators["daily"]
|
||||
assert coordinator
|
||||
daily_data = coordinator.data
|
||||
assert isinstance(daily_data, DailyForecastResponse)
|
||||
return [
|
||||
{
|
||||
ATTR_FORECAST_CONDITION: _get_condition(
|
||||
item.daytime_forecast.weather_condition.type, is_daytime=True
|
||||
),
|
||||
ATTR_FORECAST_TIME: item.interval.start_time,
|
||||
ATTR_FORECAST_HUMIDITY: item.daytime_forecast.relative_humidity,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: max(
|
||||
item.daytime_forecast.precipitation.probability.percent,
|
||||
item.nighttime_forecast.precipitation.probability.percent,
|
||||
),
|
||||
ATTR_FORECAST_CLOUD_COVERAGE: item.daytime_forecast.cloud_cover,
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION: (
|
||||
item.daytime_forecast.precipitation.qpf.quantity
|
||||
+ item.nighttime_forecast.precipitation.qpf.quantity
|
||||
),
|
||||
ATTR_FORECAST_NATIVE_TEMP: item.max_temperature.degrees,
|
||||
ATTR_FORECAST_NATIVE_TEMP_LOW: item.min_temperature.degrees,
|
||||
ATTR_FORECAST_NATIVE_APPARENT_TEMP: (
|
||||
item.feels_like_max_temperature.degrees
|
||||
),
|
||||
ATTR_FORECAST_WIND_BEARING: item.daytime_forecast.wind.direction.degrees,
|
||||
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: max(
|
||||
item.daytime_forecast.wind.gust.value,
|
||||
item.nighttime_forecast.wind.gust.value,
|
||||
),
|
||||
ATTR_FORECAST_NATIVE_WIND_SPEED: max(
|
||||
item.daytime_forecast.wind.speed.value,
|
||||
item.nighttime_forecast.wind.speed.value,
|
||||
),
|
||||
ATTR_FORECAST_UV_INDEX: item.daytime_forecast.uv_index,
|
||||
}
|
||||
for item in daily_data.forecast_days
|
||||
]
|
||||
|
||||
@callback
|
||||
def _async_forecast_hourly(self) -> list[Forecast] | None:
|
||||
"""Return the hourly forecast in native units."""
|
||||
coordinator = self.forecast_coordinators["hourly"]
|
||||
assert coordinator
|
||||
hourly_data = coordinator.data
|
||||
assert isinstance(hourly_data, HourlyForecastResponse)
|
||||
return [
|
||||
{
|
||||
ATTR_FORECAST_CONDITION: _get_condition(
|
||||
item.weather_condition.type, item.is_daytime
|
||||
),
|
||||
ATTR_FORECAST_TIME: item.interval.start_time,
|
||||
ATTR_FORECAST_HUMIDITY: item.relative_humidity,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: item.precipitation.probability.percent,
|
||||
ATTR_FORECAST_CLOUD_COVERAGE: item.cloud_cover,
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION: item.precipitation.qpf.quantity,
|
||||
ATTR_FORECAST_NATIVE_PRESSURE: item.air_pressure.mean_sea_level_millibars,
|
||||
ATTR_FORECAST_NATIVE_TEMP: item.temperature.degrees,
|
||||
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item.feels_like_temperature.degrees,
|
||||
ATTR_FORECAST_WIND_BEARING: item.wind.direction.degrees,
|
||||
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: item.wind.gust.value,
|
||||
ATTR_FORECAST_NATIVE_WIND_SPEED: item.wind.speed.value,
|
||||
ATTR_FORECAST_NATIVE_DEW_POINT: item.dew_point.degrees,
|
||||
ATTR_FORECAST_UV_INDEX: item.uv_index,
|
||||
ATTR_FORECAST_IS_DAYTIME: item.is_daytime,
|
||||
}
|
||||
for item in hourly_data.forecast_hours
|
||||
]
|
||||
|
||||
@callback
|
||||
def _async_forecast_twice_daily(self) -> list[Forecast] | None:
|
||||
"""Return the twice daily forecast in native units."""
|
||||
coordinator = self.forecast_coordinators["twice_daily"]
|
||||
assert coordinator
|
||||
daily_data = coordinator.data
|
||||
assert isinstance(daily_data, DailyForecastResponse)
|
||||
forecasts: list[Forecast] = []
|
||||
for item in daily_data.forecast_days:
|
||||
# Process daytime forecast
|
||||
day_forecast = item.daytime_forecast
|
||||
forecasts.append(
|
||||
{
|
||||
ATTR_FORECAST_CONDITION: _get_condition(
|
||||
day_forecast.weather_condition.type, is_daytime=True
|
||||
),
|
||||
ATTR_FORECAST_TIME: day_forecast.interval.start_time,
|
||||
ATTR_FORECAST_HUMIDITY: day_forecast.relative_humidity,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: day_forecast.precipitation.probability.percent,
|
||||
ATTR_FORECAST_CLOUD_COVERAGE: day_forecast.cloud_cover,
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION: day_forecast.precipitation.qpf.quantity,
|
||||
ATTR_FORECAST_NATIVE_TEMP: item.max_temperature.degrees,
|
||||
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item.feels_like_max_temperature.degrees,
|
||||
ATTR_FORECAST_WIND_BEARING: day_forecast.wind.direction.degrees,
|
||||
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: day_forecast.wind.gust.value,
|
||||
ATTR_FORECAST_NATIVE_WIND_SPEED: day_forecast.wind.speed.value,
|
||||
ATTR_FORECAST_UV_INDEX: day_forecast.uv_index,
|
||||
ATTR_FORECAST_IS_DAYTIME: True,
|
||||
}
|
||||
)
|
||||
|
||||
# Process nighttime forecast
|
||||
night_forecast = item.nighttime_forecast
|
||||
forecasts.append(
|
||||
{
|
||||
ATTR_FORECAST_CONDITION: _get_condition(
|
||||
night_forecast.weather_condition.type, is_daytime=False
|
||||
),
|
||||
ATTR_FORECAST_TIME: night_forecast.interval.start_time,
|
||||
ATTR_FORECAST_HUMIDITY: night_forecast.relative_humidity,
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: night_forecast.precipitation.probability.percent,
|
||||
ATTR_FORECAST_CLOUD_COVERAGE: night_forecast.cloud_cover,
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION: night_forecast.precipitation.qpf.quantity,
|
||||
ATTR_FORECAST_NATIVE_TEMP: item.min_temperature.degrees,
|
||||
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item.feels_like_min_temperature.degrees,
|
||||
ATTR_FORECAST_WIND_BEARING: night_forecast.wind.direction.degrees,
|
||||
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: night_forecast.wind.gust.value,
|
||||
ATTR_FORECAST_NATIVE_WIND_SPEED: night_forecast.wind.speed.value,
|
||||
ATTR_FORECAST_UV_INDEX: night_forecast.uv_index,
|
||||
ATTR_FORECAST_IS_DAYTIME: False,
|
||||
}
|
||||
)
|
||||
|
||||
return forecasts
|
||||
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -255,6 +255,7 @@ FLOWS = {
|
||||
"google_tasks",
|
||||
"google_translate",
|
||||
"google_travel_time",
|
||||
"google_weather",
|
||||
"govee_ble",
|
||||
"govee_light_local",
|
||||
"gpsd",
|
||||
|
||||
@@ -2451,6 +2451,12 @@
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"google_weather": {
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Google Weather"
|
||||
},
|
||||
"google_wifi": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
|
||||
10
mypy.ini
generated
10
mypy.ini
generated
@@ -2066,6 +2066,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.google_weather.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.govee_ble.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
||||
3
requirements_all.txt
generated
3
requirements_all.txt
generated
@@ -2469,6 +2469,9 @@ python-gitlab==1.6.0
|
||||
# homeassistant.components.google_drive
|
||||
python-google-drive-api==0.1.0
|
||||
|
||||
# homeassistant.components.google_weather
|
||||
python-google-weather-api==0.0.4
|
||||
|
||||
# homeassistant.components.analytics_insights
|
||||
python-homeassistant-analytics==0.9.0
|
||||
|
||||
|
||||
3
requirements_test_all.txt
generated
3
requirements_test_all.txt
generated
@@ -2044,6 +2044,9 @@ python-fullykiosk==0.0.14
|
||||
# homeassistant.components.google_drive
|
||||
python-google-drive-api==0.1.0
|
||||
|
||||
# homeassistant.components.google_weather
|
||||
python-google-weather-api==0.0.4
|
||||
|
||||
# homeassistant.components.analytics_insights
|
||||
python-homeassistant-analytics==0.9.0
|
||||
|
||||
|
||||
1
tests/components/google_weather/__init__.py
Normal file
1
tests/components/google_weather/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Tests for the Google Weather integration."""
|
||||
83
tests/components/google_weather/conftest.py
Normal file
83
tests/components/google_weather/conftest.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""Common fixtures for the Google Weather tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from google_weather_api import (
|
||||
CurrentConditionsResponse,
|
||||
DailyForecastResponse,
|
||||
HourlyForecastResponse,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.google_weather.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigSubentryDataWithId
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.google_weather.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
config_entry = MockConfigEntry(
|
||||
title="Google Weather",
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_API_KEY: "test-api-key",
|
||||
},
|
||||
subentries_data=[
|
||||
ConfigSubentryDataWithId(
|
||||
data={
|
||||
CONF_LATITUDE: 10.1,
|
||||
CONF_LONGITUDE: 20.1,
|
||||
},
|
||||
subentry_type="location",
|
||||
title="Home",
|
||||
subentry_id="home-subentry-id",
|
||||
unique_id=None,
|
||||
)
|
||||
],
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
return config_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_google_weather_api() -> Generator[AsyncMock]:
|
||||
"""Mock Google Weather API."""
|
||||
current_conditions = CurrentConditionsResponse.from_dict(
|
||||
load_json_object_fixture("current_conditions.json", DOMAIN)
|
||||
)
|
||||
daily_forecast = DailyForecastResponse.from_dict(
|
||||
load_json_object_fixture("daily_forecast.json", DOMAIN)
|
||||
)
|
||||
hourly_forecast = HourlyForecastResponse.from_dict(
|
||||
load_json_object_fixture("hourly_forecast.json", DOMAIN)
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.google_weather.GoogleWeatherApi", autospec=True
|
||||
) as mock_api,
|
||||
patch(
|
||||
"homeassistant.components.google_weather.config_flow.GoogleWeatherApi",
|
||||
new=mock_api,
|
||||
),
|
||||
):
|
||||
api = mock_api.return_value
|
||||
api.async_get_current_conditions.return_value = current_conditions
|
||||
api.async_get_daily_forecast.return_value = daily_forecast
|
||||
api.async_get_hourly_forecast.return_value = hourly_forecast
|
||||
|
||||
yield api
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"currentTime": "2025-01-28T22:04:12.025273178Z",
|
||||
"timeZone": {
|
||||
"id": "America/Los_Angeles"
|
||||
},
|
||||
"isDaytime": true,
|
||||
"weatherCondition": {
|
||||
"iconBaseUri": "https://maps.gstatic.com/weather/v1/sunny",
|
||||
"description": {
|
||||
"text": "Sunny",
|
||||
"languageCode": "en"
|
||||
},
|
||||
"type": "CLEAR"
|
||||
},
|
||||
"temperature": {
|
||||
"degrees": 13.7,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"feelsLikeTemperature": {
|
||||
"degrees": 13.1,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"dewPoint": {
|
||||
"degrees": 1.1,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"heatIndex": {
|
||||
"degrees": 13.7,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"windChill": {
|
||||
"degrees": 13.1,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"relativeHumidity": 42,
|
||||
"uvIndex": 1,
|
||||
"precipitation": {
|
||||
"probability": {
|
||||
"percent": 0,
|
||||
"type": "RAIN"
|
||||
},
|
||||
"qpf": {
|
||||
"quantity": 0,
|
||||
"unit": "MILLIMETERS"
|
||||
}
|
||||
},
|
||||
"thunderstormProbability": 0,
|
||||
"airPressure": {
|
||||
"meanSeaLevelMillibars": 1019.16
|
||||
},
|
||||
"wind": {
|
||||
"direction": {
|
||||
"degrees": 335,
|
||||
"cardinal": "NORTH_NORTHWEST"
|
||||
},
|
||||
"speed": {
|
||||
"value": 8,
|
||||
"unit": "KILOMETERS_PER_HOUR"
|
||||
},
|
||||
"gust": {
|
||||
"value": 18,
|
||||
"unit": "KILOMETERS_PER_HOUR"
|
||||
}
|
||||
},
|
||||
"visibility": {
|
||||
"distance": 16,
|
||||
"unit": "KILOMETERS"
|
||||
},
|
||||
"cloudCover": 0,
|
||||
"currentConditionsHistory": {
|
||||
"temperatureChange": {
|
||||
"degrees": -0.6,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"maxTemperature": {
|
||||
"degrees": 14.3,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"minTemperature": {
|
||||
"degrees": 3.7,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"qpf": {
|
||||
"quantity": 0,
|
||||
"unit": "MILLIMETERS"
|
||||
}
|
||||
}
|
||||
}
|
||||
135
tests/components/google_weather/fixtures/daily_forecast.json
Normal file
135
tests/components/google_weather/fixtures/daily_forecast.json
Normal file
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"forecastDays": [
|
||||
{
|
||||
"interval": {
|
||||
"startTime": "2025-02-10T15:00:00Z",
|
||||
"endTime": "2025-02-11T15:00:00Z"
|
||||
},
|
||||
"displayDate": {
|
||||
"year": 2025,
|
||||
"month": 2,
|
||||
"day": 10
|
||||
},
|
||||
"daytimeForecast": {
|
||||
"interval": {
|
||||
"startTime": "2025-02-10T15:00:00Z",
|
||||
"endTime": "2025-02-11T03:00:00Z"
|
||||
},
|
||||
"weatherCondition": {
|
||||
"iconBaseUri": "https://maps.gstatic.com/weather/v1/party_cloudy",
|
||||
"description": {
|
||||
"text": "Partly sunny",
|
||||
"languageCode": "en"
|
||||
},
|
||||
"type": "PARTLY_CLOUDY"
|
||||
},
|
||||
"relativeHumidity": 54,
|
||||
"uvIndex": 3,
|
||||
"precipitation": {
|
||||
"probability": {
|
||||
"percent": 5,
|
||||
"type": "RAIN"
|
||||
},
|
||||
"qpf": {
|
||||
"quantity": 0,
|
||||
"unit": "MILLIMETERS"
|
||||
}
|
||||
},
|
||||
"thunderstormProbability": 0,
|
||||
"wind": {
|
||||
"direction": {
|
||||
"degrees": 280,
|
||||
"cardinal": "WEST"
|
||||
},
|
||||
"speed": {
|
||||
"value": 6,
|
||||
"unit": "KILOMETERS_PER_HOUR"
|
||||
},
|
||||
"gust": {
|
||||
"value": 14,
|
||||
"unit": "KILOMETERS_PER_HOUR"
|
||||
}
|
||||
},
|
||||
"cloudCover": 53
|
||||
},
|
||||
"nighttimeForecast": {
|
||||
"interval": {
|
||||
"startTime": "2025-02-11T03:00:00Z",
|
||||
"endTime": "2025-02-11T15:00:00Z"
|
||||
},
|
||||
"weatherCondition": {
|
||||
"iconBaseUri": "https://maps.gstatic.com/weather/v1/partly_clear",
|
||||
"description": {
|
||||
"text": "Partly cloudy",
|
||||
"languageCode": "en"
|
||||
},
|
||||
"type": "PARTLY_CLOUDY"
|
||||
},
|
||||
"relativeHumidity": 85,
|
||||
"uvIndex": 0,
|
||||
"precipitation": {
|
||||
"probability": {
|
||||
"percent": 10,
|
||||
"type": "RAIN_AND_SNOW"
|
||||
},
|
||||
"qpf": {
|
||||
"quantity": 0,
|
||||
"unit": "MILLIMETERS"
|
||||
}
|
||||
},
|
||||
"thunderstormProbability": 0,
|
||||
"wind": {
|
||||
"direction": {
|
||||
"degrees": 201,
|
||||
"cardinal": "SOUTH_SOUTHWEST"
|
||||
},
|
||||
"speed": {
|
||||
"value": 6,
|
||||
"unit": "KILOMETERS_PER_HOUR"
|
||||
},
|
||||
"gust": {
|
||||
"value": 14,
|
||||
"unit": "KILOMETERS_PER_HOUR"
|
||||
}
|
||||
},
|
||||
"cloudCover": 70
|
||||
},
|
||||
"maxTemperature": {
|
||||
"degrees": 13.3,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"minTemperature": {
|
||||
"degrees": 1.5,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"feelsLikeMaxTemperature": {
|
||||
"degrees": 13.3,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"feelsLikeMinTemperature": {
|
||||
"degrees": 1.5,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"sunEvents": {
|
||||
"sunriseTime": "2025-02-10T15:02:35.703929582Z",
|
||||
"sunsetTime": "2025-02-11T01:43:00.762932858Z"
|
||||
},
|
||||
"moonEvents": {
|
||||
"moonPhase": "WAXING_GIBBOUS",
|
||||
"moonriseTimes": ["2025-02-10T23:54:17.713157984Z"],
|
||||
"moonsetTimes": ["2025-02-10T14:13:58.625181191Z"]
|
||||
},
|
||||
"maxHeatIndex": {
|
||||
"degrees": 13.3,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"iceThickness": {
|
||||
"thickness": 0,
|
||||
"unit": "MILLIMETERS"
|
||||
}
|
||||
}
|
||||
],
|
||||
"timeZone": {
|
||||
"id": "America/Los_Angeles"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"forecastHours": [
|
||||
{
|
||||
"interval": {
|
||||
"startTime": "2025-02-05T23:00:00Z",
|
||||
"endTime": "2025-02-06T00:00:00Z"
|
||||
},
|
||||
"displayDateTime": {
|
||||
"year": 2025,
|
||||
"month": 2,
|
||||
"day": 5,
|
||||
"hours": 15,
|
||||
"utcOffset": "-28800s"
|
||||
},
|
||||
"isDaytime": true,
|
||||
"weatherCondition": {
|
||||
"iconBaseUri": "https://maps.gstatic.com/weather/v1/sunny",
|
||||
"description": {
|
||||
"text": "Sunny",
|
||||
"languageCode": "en"
|
||||
},
|
||||
"type": "CLEAR"
|
||||
},
|
||||
"temperature": {
|
||||
"degrees": 12.7,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"feelsLikeTemperature": {
|
||||
"degrees": 12,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"dewPoint": {
|
||||
"degrees": 2.7,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"heatIndex": {
|
||||
"degrees": 12.7,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"windChill": {
|
||||
"degrees": 12,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"wetBulbTemperature": {
|
||||
"degrees": 7.7,
|
||||
"unit": "CELSIUS"
|
||||
},
|
||||
"relativeHumidity": 51,
|
||||
"uvIndex": 1,
|
||||
"precipitation": {
|
||||
"probability": {
|
||||
"percent": 0,
|
||||
"type": "RAIN"
|
||||
},
|
||||
"qpf": {
|
||||
"quantity": 0,
|
||||
"unit": "MILLIMETERS"
|
||||
}
|
||||
},
|
||||
"thunderstormProbability": 0,
|
||||
"airPressure": {
|
||||
"meanSeaLevelMillibars": 1019.13
|
||||
},
|
||||
"wind": {
|
||||
"direction": {
|
||||
"degrees": 335,
|
||||
"cardinal": "NORTH_NORTHWEST"
|
||||
},
|
||||
"speed": {
|
||||
"value": 10,
|
||||
"unit": "KILOMETERS_PER_HOUR"
|
||||
},
|
||||
"gust": {
|
||||
"value": 19,
|
||||
"unit": "KILOMETERS_PER_HOUR"
|
||||
}
|
||||
},
|
||||
"visibility": {
|
||||
"distance": 16,
|
||||
"unit": "KILOMETERS"
|
||||
},
|
||||
"cloudCover": 0,
|
||||
"iceThickness": {
|
||||
"thickness": 0,
|
||||
"unit": "MILLIMETERS"
|
||||
}
|
||||
}
|
||||
],
|
||||
"timeZone": {
|
||||
"id": "America/Los_Angeles"
|
||||
}
|
||||
}
|
||||
191
tests/components/google_weather/snapshots/test_weather.ambr
Normal file
191
tests/components/google_weather/snapshots/test_weather.ambr
Normal file
@@ -0,0 +1,191 @@
|
||||
# serializer version: 1
|
||||
# name: test_forecast_service[daily]
|
||||
dict({
|
||||
'weather.home': dict({
|
||||
'forecast': list([
|
||||
dict({
|
||||
'apparent_temperature': 13.3,
|
||||
'cloud_coverage': 53,
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-02-10T15:00:00Z',
|
||||
'humidity': 54,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 10,
|
||||
'temperature': 13.3,
|
||||
'templow': 1.5,
|
||||
'uv_index': 3,
|
||||
'wind_bearing': 280,
|
||||
'wind_gust_speed': 14.0,
|
||||
'wind_speed': 6.0,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_forecast_service[hourly]
|
||||
dict({
|
||||
'weather.home': dict({
|
||||
'forecast': list([
|
||||
dict({
|
||||
'apparent_temperature': 12.0,
|
||||
'cloud_coverage': 0,
|
||||
'condition': 'sunny',
|
||||
'datetime': '2025-02-05T23:00:00Z',
|
||||
'dew_point': 2.7,
|
||||
'humidity': 51,
|
||||
'is_daytime': True,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 0,
|
||||
'pressure': 1019.13,
|
||||
'temperature': 12.7,
|
||||
'uv_index': 1,
|
||||
'wind_bearing': 335,
|
||||
'wind_gust_speed': 19.0,
|
||||
'wind_speed': 10.0,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_forecast_service[twice_daily]
|
||||
dict({
|
||||
'weather.home': dict({
|
||||
'forecast': list([
|
||||
dict({
|
||||
'apparent_temperature': 13.3,
|
||||
'cloud_coverage': 53,
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-02-10T15:00:00Z',
|
||||
'humidity': 54,
|
||||
'is_daytime': True,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 5,
|
||||
'temperature': 13.3,
|
||||
'uv_index': 3,
|
||||
'wind_bearing': 280,
|
||||
'wind_gust_speed': 14.0,
|
||||
'wind_speed': 6.0,
|
||||
}),
|
||||
dict({
|
||||
'apparent_temperature': 1.5,
|
||||
'cloud_coverage': 70,
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-02-11T03:00:00Z',
|
||||
'humidity': 85,
|
||||
'is_daytime': False,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 10,
|
||||
'temperature': 1.5,
|
||||
'uv_index': 0,
|
||||
'wind_bearing': 201,
|
||||
'wind_gust_speed': 14.0,
|
||||
'wind_speed': 6.0,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
# name: test_forecast_subscription
|
||||
list([
|
||||
dict({
|
||||
'apparent_temperature': 13.3,
|
||||
'cloud_coverage': 53,
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-02-10T15:00:00Z',
|
||||
'humidity': 54,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 10,
|
||||
'temperature': 13.3,
|
||||
'templow': 1.5,
|
||||
'uv_index': 3,
|
||||
'wind_bearing': 280,
|
||||
'wind_gust_speed': 14.0,
|
||||
'wind_speed': 6.0,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_forecast_subscription.1
|
||||
list([
|
||||
dict({
|
||||
'apparent_temperature': 13.3,
|
||||
'cloud_coverage': 53,
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2025-02-10T15:00:00Z',
|
||||
'humidity': 54,
|
||||
'precipitation': 0.0,
|
||||
'precipitation_probability': 10,
|
||||
'temperature': 13.3,
|
||||
'templow': 1.5,
|
||||
'uv_index': 3,
|
||||
'wind_bearing': 280,
|
||||
'wind_gust_speed': 14.0,
|
||||
'wind_speed': 6.0,
|
||||
}),
|
||||
])
|
||||
# ---
|
||||
# name: test_weather[weather.home-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'weather',
|
||||
'entity_category': None,
|
||||
'entity_id': 'weather.home',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'google_weather',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <WeatherEntityFeature: 7>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'home-subentry-id',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_weather[weather.home-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'apparent_temperature': 13.1,
|
||||
'attribution': 'Data from Google Weather',
|
||||
'cloud_coverage': 0.0,
|
||||
'dew_point': 1.1,
|
||||
'friendly_name': 'Home',
|
||||
'humidity': 42,
|
||||
'precipitation_unit': <UnitOfPrecipitationDepth.MILLIMETERS: 'mm'>,
|
||||
'pressure': 1019.16,
|
||||
'pressure_unit': <UnitOfPressure.HPA: 'hPa'>,
|
||||
'supported_features': <WeatherEntityFeature: 7>,
|
||||
'temperature': 13.7,
|
||||
'temperature_unit': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
'uv_index': 1.0,
|
||||
'visibility': 16.0,
|
||||
'visibility_unit': <UnitOfLength.KILOMETERS: 'km'>,
|
||||
'wind_bearing': 335,
|
||||
'wind_gust_speed': 18.0,
|
||||
'wind_speed': 8.0,
|
||||
'wind_speed_unit': <UnitOfSpeed.KILOMETERS_PER_HOUR: 'km/h'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'weather.home',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'sunny',
|
||||
})
|
||||
# ---
|
||||
375
tests/components/google_weather/test_config_flow.py
Normal file
375
tests/components/google_weather/test_config_flow.py
Normal file
@@ -0,0 +1,375 @@
|
||||
"""Test the Google Weather config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from google_weather_api import GoogleWeatherApiError
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.google_weather.const import (
|
||||
CONF_REFERRER,
|
||||
DOMAIN,
|
||||
SECTION_API_KEY_OPTIONS,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_LATITUDE,
|
||||
CONF_LOCATION,
|
||||
CONF_LONGITUDE,
|
||||
CONF_NAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry, get_schema_suggested_value
|
||||
|
||||
|
||||
def _assert_create_entry_result(
|
||||
result: dict, expected_referrer: str | None = None
|
||||
) -> None:
|
||||
"""Assert that the result is a create entry result."""
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Google Weather"
|
||||
assert result["data"] == {
|
||||
CONF_API_KEY: "test-api-key",
|
||||
CONF_REFERRER: expected_referrer,
|
||||
}
|
||||
assert len(result["subentries"]) == 1
|
||||
subentry = result["subentries"][0]
|
||||
assert subentry["subentry_type"] == "location"
|
||||
assert subentry["title"] == "test-name"
|
||||
assert subentry["data"] == {
|
||||
CONF_LATITUDE: 10.1,
|
||||
CONF_LONGITUDE: 20.1,
|
||||
}
|
||||
|
||||
|
||||
async def test_create_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test creating a config entry."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_NAME: "test-name",
|
||||
CONF_API_KEY: "test-api-key",
|
||||
CONF_LOCATION: {
|
||||
CONF_LATITUDE: 10.1,
|
||||
CONF_LONGITUDE: 20.1,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
mock_google_weather_api.async_get_current_conditions.assert_called_once_with(
|
||||
latitude=10.1, longitude=20.1
|
||||
)
|
||||
|
||||
_assert_create_entry_result(result)
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_with_referrer(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test we get the form and optional referrer is specified."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_NAME: "test-name",
|
||||
CONF_API_KEY: "test-api-key",
|
||||
SECTION_API_KEY_OPTIONS: {
|
||||
CONF_REFERRER: "test-referrer",
|
||||
},
|
||||
CONF_LOCATION: {
|
||||
CONF_LATITUDE: 10.1,
|
||||
CONF_LONGITUDE: 20.1,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
mock_google_weather_api.async_get_current_conditions.assert_called_once_with(
|
||||
latitude=10.1, longitude=20.1
|
||||
)
|
||||
|
||||
_assert_create_entry_result(result, expected_referrer="test-referrer")
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("api_exception", "expected_error"),
|
||||
[
|
||||
(GoogleWeatherApiError(), "cannot_connect"),
|
||||
(ValueError(), "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_form_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
api_exception,
|
||||
expected_error,
|
||||
) -> None:
|
||||
"""Test we handle exceptions."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
mock_google_weather_api.async_get_current_conditions.side_effect = api_exception
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_NAME: "test-name",
|
||||
CONF_API_KEY: "test-api-key",
|
||||
CONF_LOCATION: {
|
||||
CONF_LATITUDE: 10.1,
|
||||
CONF_LONGITUDE: 20.1,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": expected_error}
|
||||
# On error, the form should have the previous user input
|
||||
data_schema = result["data_schema"].schema
|
||||
assert get_schema_suggested_value(data_schema, CONF_NAME) == "test-name"
|
||||
assert get_schema_suggested_value(data_schema, CONF_API_KEY) == "test-api-key"
|
||||
assert get_schema_suggested_value(data_schema, CONF_LOCATION) == {
|
||||
CONF_LATITUDE: 10.1,
|
||||
CONF_LONGITUDE: 20.1,
|
||||
}
|
||||
|
||||
# Make sure the config flow tests finish with either an
|
||||
# FlowResultType.CREATE_ENTRY or FlowResultType.ABORT so
|
||||
# we can show the config flow is able to recover from an error.
|
||||
|
||||
mock_google_weather_api.async_get_current_conditions.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_NAME: "test-name",
|
||||
CONF_API_KEY: "test-api-key",
|
||||
CONF_LOCATION: {
|
||||
CONF_LATITUDE: 10.1,
|
||||
CONF_LONGITUDE: 20.1,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
_assert_create_entry_result(result)
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_api_key_already_configured(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test user input for config_entry with API key that already exists."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_NAME: "test-name",
|
||||
CONF_API_KEY: "test-api-key",
|
||||
CONF_LOCATION: {
|
||||
CONF_LATITUDE: 10.2,
|
||||
CONF_LONGITUDE: 20.2,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert mock_google_weather_api.async_get_current_conditions.call_count == 0
|
||||
|
||||
|
||||
async def test_form_location_already_configured(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test user input for a location that already exists."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_NAME: "test-name",
|
||||
CONF_API_KEY: "another-api-key",
|
||||
CONF_LOCATION: {
|
||||
CONF_LATITUDE: 10.1001,
|
||||
CONF_LONGITUDE: 20.0999,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert mock_google_weather_api.async_get_current_conditions.call_count == 0
|
||||
|
||||
|
||||
async def test_form_not_already_configured(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test user input for config_entry different than the existing one."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_NAME: "new-test-name",
|
||||
CONF_API_KEY: "new-test-api-key",
|
||||
CONF_LOCATION: {
|
||||
CONF_LATITUDE: 10.1002,
|
||||
CONF_LONGITUDE: 20.0998,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
mock_google_weather_api.async_get_current_conditions.assert_called_once_with(
|
||||
latitude=10.1002, longitude=20.0998
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Google Weather"
|
||||
assert result["data"] == {
|
||||
CONF_API_KEY: "new-test-api-key",
|
||||
CONF_REFERRER: None,
|
||||
}
|
||||
assert len(result["subentries"]) == 1
|
||||
subentry = result["subentries"][0]
|
||||
assert subentry["subentry_type"] == "location"
|
||||
assert subentry["title"] == "new-test-name"
|
||||
assert subentry["data"] == {
|
||||
CONF_LATITUDE: 10.1002,
|
||||
CONF_LONGITUDE: 20.0998,
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 2
|
||||
|
||||
|
||||
async def test_subentry_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test creating a location subentry."""
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# After initial setup for 1 subentry, each API is called once
|
||||
assert mock_google_weather_api.async_get_current_conditions.call_count == 1
|
||||
assert mock_google_weather_api.async_get_daily_forecast.call_count == 1
|
||||
assert mock_google_weather_api.async_get_hourly_forecast.call_count == 1
|
||||
|
||||
result = await hass.config_entries.subentries.async_init(
|
||||
(mock_config_entry.entry_id, "location"),
|
||||
context={"source": "user"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "location"
|
||||
|
||||
result2 = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_NAME: "Work",
|
||||
CONF_LOCATION: {
|
||||
CONF_LATITUDE: 30.1,
|
||||
CONF_LONGITUDE: 40.1,
|
||||
},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "Work"
|
||||
assert result2["data"] == {
|
||||
CONF_LATITUDE: 30.1,
|
||||
CONF_LONGITUDE: 40.1,
|
||||
}
|
||||
|
||||
# Initial setup: 1 of each API call
|
||||
# Subentry flow validation: 1 current conditions call
|
||||
# Reload with 2 subentries: 2 of each API call
|
||||
assert mock_google_weather_api.async_get_current_conditions.call_count == 1 + 1 + 2
|
||||
assert mock_google_weather_api.async_get_daily_forecast.call_count == 1 + 2
|
||||
assert mock_google_weather_api.async_get_hourly_forecast.call_count == 1 + 2
|
||||
|
||||
entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
||||
assert len(entry.subentries) == 2
|
||||
|
||||
|
||||
async def test_subentry_flow_location_already_configured(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test user input for a location that already exists."""
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
result = await hass.config_entries.subentries.async_init(
|
||||
(mock_config_entry.entry_id, "location"),
|
||||
context={"source": "user"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "location"
|
||||
|
||||
result2 = await hass.config_entries.subentries.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_NAME: "Work",
|
||||
CONF_LOCATION: {
|
||||
CONF_LATITUDE: 10.1,
|
||||
CONF_LONGITUDE: 20.1,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "already_configured"
|
||||
|
||||
entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
||||
assert len(entry.subentries) == 1
|
||||
|
||||
|
||||
async def test_subentry_flow_entry_not_loaded(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test creating a location subentry when the parent entry is not loaded."""
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
|
||||
result = await hass.config_entries.subentries.async_init(
|
||||
(mock_config_entry.entry_id, "location"),
|
||||
context={"source": "user"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "entry_not_loaded"
|
||||
69
tests/components/google_weather/test_init.py
Normal file
69
tests/components/google_weather/test_init.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Test init of Google Weather integration."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from google_weather_api import GoogleWeatherApiError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.google_weather.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test a successful setup entry."""
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
state = hass.states.get("weather.home")
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
assert state.state == "sunny"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"failing_api_method",
|
||||
[
|
||||
"async_get_current_conditions",
|
||||
"async_get_daily_forecast",
|
||||
"async_get_hourly_forecast",
|
||||
],
|
||||
)
|
||||
async def test_config_not_ready(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
failing_api_method: str,
|
||||
) -> None:
|
||||
"""Test for setup failure if an API call fails."""
|
||||
getattr(
|
||||
mock_google_weather_api, failing_api_method
|
||||
).side_effect = GoogleWeatherApiError()
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_unload_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test successful unload of entry."""
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
214
tests/components/google_weather/test_weather.py
Normal file
214
tests/components/google_weather/test_weather.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""Test weather of Google Weather integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from google_weather_api import GoogleWeatherApiError, WeatherCondition
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.google_weather.weather import _CONDITION_MAP
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
ATTR_CONDITION_PARTLYCLOUDY,
|
||||
ATTR_CONDITION_SUNNY,
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECASTS,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
|
||||
async def test_weather(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test states of the weather."""
|
||||
with patch(
|
||||
"homeassistant.components.google_weather._PLATFORMS", [Platform.WEATHER]
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
async def test_availability(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Ensure that we mark the entities unavailable correctly when service is offline."""
|
||||
entity_id = "weather.home"
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
assert state.state == "sunny"
|
||||
|
||||
mock_google_weather_api.async_get_current_conditions.side_effect = (
|
||||
GoogleWeatherApiError()
|
||||
)
|
||||
|
||||
freezer.tick(timedelta(minutes=15))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
# Reset side effect, return a valid response again
|
||||
mock_google_weather_api.async_get_current_conditions.side_effect = None
|
||||
|
||||
freezer.tick(timedelta(minutes=15))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
assert state.state == "sunny"
|
||||
mock_google_weather_api.async_get_current_conditions.assert_called_with(
|
||||
latitude=10.1, longitude=20.1
|
||||
)
|
||||
|
||||
|
||||
async def test_manual_update_entity(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test manual update entity via service homeassistant/update_entity."""
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
await async_setup_component(hass, "homeassistant", {})
|
||||
|
||||
assert mock_google_weather_api.async_get_current_conditions.call_count == 1
|
||||
mock_google_weather_api.async_get_current_conditions.assert_called_with(
|
||||
latitude=10.1, longitude=20.1
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
"homeassistant",
|
||||
"update_entity",
|
||||
{ATTR_ENTITY_ID: ["weather.home"]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_google_weather_api.async_get_current_conditions.call_count == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("api_condition", "is_daytime", "expected_ha_condition"),
|
||||
[
|
||||
(WeatherCondition.Type.CLEAR, True, ATTR_CONDITION_SUNNY),
|
||||
(WeatherCondition.Type.CLEAR, False, ATTR_CONDITION_CLEAR_NIGHT),
|
||||
(WeatherCondition.Type.PARTLY_CLOUDY, True, ATTR_CONDITION_PARTLYCLOUDY),
|
||||
(WeatherCondition.Type.PARTLY_CLOUDY, False, ATTR_CONDITION_PARTLYCLOUDY),
|
||||
(WeatherCondition.Type.TYPE_UNSPECIFIED, True, "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_condition(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
api_condition: WeatherCondition.Type,
|
||||
is_daytime: bool,
|
||||
expected_ha_condition: str,
|
||||
) -> None:
|
||||
"""Test condition mapping."""
|
||||
mock_google_weather_api.async_get_current_conditions.return_value.weather_condition.type = api_condition
|
||||
mock_google_weather_api.async_get_current_conditions.return_value.is_daytime = (
|
||||
is_daytime
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
state = hass.states.get("weather.home")
|
||||
assert state.state == expected_ha_condition
|
||||
|
||||
|
||||
def test_all_conditions_mapped() -> None:
|
||||
"""Ensure all WeatherCondition.Type enum members are in the _CONDITION_MAP."""
|
||||
for condition_type in WeatherCondition.Type:
|
||||
assert condition_type in _CONDITION_MAP
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("forecast_type"), ["daily", "hourly", "twice_daily"])
|
||||
async def test_forecast_service(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
forecast_type,
|
||||
) -> None:
|
||||
"""Test forecast service."""
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
SERVICE_GET_FORECASTS,
|
||||
{
|
||||
"entity_id": "weather.home",
|
||||
"type": forecast_type,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == snapshot
|
||||
|
||||
|
||||
async def test_forecast_subscription(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_google_weather_api: AsyncMock,
|
||||
) -> None:
|
||||
"""Test multiple forecast."""
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "weather/subscribe_forecast",
|
||||
"forecast_type": "daily",
|
||||
"entity_id": "weather.home",
|
||||
}
|
||||
)
|
||||
msg = await client.receive_json()
|
||||
assert msg["success"]
|
||||
assert msg["result"] is None
|
||||
subscription_id = msg["id"]
|
||||
|
||||
msg = await client.receive_json()
|
||||
assert msg["id"] == subscription_id
|
||||
assert msg["type"] == "event"
|
||||
forecast1 = msg["event"]["forecast"]
|
||||
|
||||
assert forecast1 != []
|
||||
assert forecast1 == snapshot
|
||||
|
||||
freezer.tick(timedelta(hours=1) + timedelta(seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
msg = await client.receive_json()
|
||||
|
||||
assert msg["id"] == subscription_id
|
||||
assert msg["type"] == "event"
|
||||
forecast2 = msg["event"]["forecast"]
|
||||
|
||||
assert forecast2 != []
|
||||
assert forecast2 == snapshot
|
||||
Reference in New Issue
Block a user