Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update metoffice to use DataHub API #131425

Open
wants to merge 16 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 35 additions & 65 deletions homeassistant/components/metoffice/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

import asyncio
import logging
import re
from typing import Any

import datapoint
import datapoint.Forecast
import datapoint.Manager

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
Expand All @@ -17,9 +17,8 @@
CONF_NAME,
Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator

Expand All @@ -30,11 +29,9 @@
METOFFICE_DAILY_COORDINATOR,
METOFFICE_HOURLY_COORDINATOR,
METOFFICE_NAME,
MODE_3HOURLY,
MODE_DAILY,
METOFFICE_TWICE_DAILY_COORDINATOR,
)
from .data import MetOfficeData
from .helpers import fetch_data, fetch_site
from .helpers import fetch_data

_LOGGER = logging.getLogger(__name__)

Expand All @@ -51,91 +48,64 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

coordinates = f"{latitude}_{longitude}"

@callback
def update_unique_id(
entity_entry: er.RegistryEntry,
) -> dict[str, Any] | None:
"""Update unique ID of entity entry."""

if entity_entry.domain != Platform.SENSOR:
return None

name_to_key = {
"Station Name": "name",
"Weather": "weather",
"Temperature": "temperature",
"Feels Like Temperature": "feels_like_temperature",
"Wind Speed": "wind_speed",
"Wind Direction": "wind_direction",
"Wind Gust": "wind_gust",
"Visibility": "visibility",
"Visibility Distance": "visibility_distance",
"UV Index": "uv",
"Probability of Precipitation": "precipitation",
"Humidity": "humidity",
}

match = re.search(f"(?P<name>.*)_{coordinates}.*", entity_entry.unique_id)

if match is None:
return None

if (name := match.group("name")) in name_to_key:
return {
"new_unique_id": entity_entry.unique_id.replace(name, name_to_key[name])
}
return None

await er.async_migrate_entries(hass, entry.entry_id, update_unique_id)

connection = datapoint.connection(api_key=api_key)

site = await hass.async_add_executor_job(
fetch_site, connection, latitude, longitude
)
if site is None:
raise ConfigEntryNotReady
connection = datapoint.Manager.Manager(api_key=api_key)

async def async_update_3hourly() -> MetOfficeData:
async def async_update_daily() -> datapoint.Forecast:
return await hass.async_add_executor_job(
fetch_data, connection, site, MODE_3HOURLY
fetch_data, connection, latitude, longitude, "daily"
)

async def async_update_daily() -> MetOfficeData:
async def async_update_twice_daily() -> datapoint.Forecast:
return await hass.async_add_executor_job(
fetch_data, connection, site, MODE_DAILY
fetch_data, connection, latitude, longitude, "twice-daily"
)

metoffice_hourly_coordinator = TimestampDataUpdateCoordinator(
async def async_update_hourly() -> datapoint.Forecast:
return await hass.async_add_executor_job(
fetch_data, connection, latitude, longitude, "hourly"
)

metoffice_daily_coordinator = TimestampDataUpdateCoordinator(
avee87 marked this conversation as resolved.
Show resolved Hide resolved
hass,
_LOGGER,
config_entry=entry,
name=f"MetOffice Hourly Coordinator for {site_name}",
update_method=async_update_3hourly,
name=f"MetOffice Daily Coordinator for {site_name}",
update_method=async_update_daily,
update_interval=DEFAULT_SCAN_INTERVAL,
)

metoffice_daily_coordinator = TimestampDataUpdateCoordinator(
metoffice_twice_daily_coordinator = TimestampDataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=f"MetOffice Daily Coordinator for {site_name}",
update_method=async_update_daily,
name=f"MetOffice Twice Daily Coordinator for {site_name}",
update_method=async_update_twice_daily,
update_interval=DEFAULT_SCAN_INTERVAL,
)

metoffice_hourly_coordinator = TimestampDataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=f"MetOffice Hourly Coordinator for {site_name}",
update_method=async_update_hourly,
update_interval=DEFAULT_SCAN_INTERVAL,
)

metoffice_hass_data = hass.data.setdefault(DOMAIN, {})
metoffice_hass_data[entry.entry_id] = {
METOFFICE_HOURLY_COORDINATOR: metoffice_hourly_coordinator,
METOFFICE_DAILY_COORDINATOR: metoffice_daily_coordinator,
METOFFICE_TWICE_DAILY_COORDINATOR: metoffice_twice_daily_coordinator,
gjohansson-ST marked this conversation as resolved.
Show resolved Hide resolved
METOFFICE_HOURLY_COORDINATOR: metoffice_hourly_coordinator,
METOFFICE_NAME: site_name,
METOFFICE_COORDINATES: coordinates,
}

# Fetch initial data so we have data when entities subscribe
await asyncio.gather(
metoffice_hourly_coordinator.async_config_entry_first_refresh(),
metoffice_daily_coordinator.async_config_entry_first_refresh(),
metoffice_twice_daily_coordinator.async_config_entry_first_refresh(),
metoffice_hourly_coordinator.async_config_entry_first_refresh(),
)

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
Expand Down
103 changes: 81 additions & 22 deletions homeassistant/components/metoffice/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

from __future__ import annotations

from collections.abc import Mapping
import logging
from typing import Any

import datapoint
from datapoint.exceptions import APIException
import datapoint.Manager
from requests import HTTPError
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
Expand All @@ -15,30 +19,44 @@
from homeassistant.helpers import config_validation as cv

from .const import DOMAIN
from .helpers import fetch_site

_LOGGER = logging.getLogger(__name__)


async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]:
async def validate_input(
hass: HomeAssistant, latitude: float, longitude: float, api_key: str
) -> dict[str, Any]:
"""Validate that the user input allows us to connect to DataPoint.

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

connection = datapoint.connection(api_key=api_key)
errors = {}
connection = datapoint.Manager.Manager(api_key=api_key)

try:
forecast = await hass.async_add_executor_job(
connection.get_forecast,
latitude,
longitude,
"daily",
False,
)

site = await hass.async_add_executor_job(
fetch_site, connection, latitude, longitude
)
if forecast is None:
errors["base"] = "cannot_connect"

if site is None:
raise CannotConnect
except (HTTPError, APIException) as err:
if isinstance(err, HTTPError) and err.response.status_code == 401:
errors["base"] = "invalid_auth"
else:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return {"site_name": forecast.name, "errors": errors}

return {"site_name": site.name}
return {"errors": errors}
avee87 marked this conversation as resolved.
Show resolved Hide resolved


class MetOfficeConfigFlow(ConfigFlow, domain=DOMAIN):
Expand All @@ -57,15 +75,17 @@ async def async_step_user(
)
self._abort_if_unique_id_configured()

try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
user_input[CONF_NAME] = info["site_name"]
result = await validate_input(
self.hass,
latitude=user_input[CONF_LATITUDE],
longitude=user_input[CONF_LONGITUDE],
api_key=user_input[CONF_API_KEY],
)

errors = result["errors"]
avee87 marked this conversation as resolved.
Show resolved Hide resolved

if not errors:
user_input[CONF_NAME] = result["site_name"]
return self.async_create_entry(
title=user_input[CONF_NAME], data=user_input
)
Expand All @@ -86,6 +106,45 @@ async def async_step_user(
step_id="user", data_schema=data_schema, errors=errors
)

async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm()

async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
errors = {}

entry = self._get_reauth_entry()
if user_input is not None:
result = await validate_input(
self.hass,
latitude=entry.data[CONF_LATITUDE],
longitude=entry.data[CONF_LONGITUDE],
api_key=user_input[CONF_API_KEY],
)

errors = result["errors"]

if not errors:
return self.async_update_reload_and_abort(
self._get_reauth_entry(),
data_updates=user_input,
)

return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_API_KEY): str,
}
),
errors=errors,
)
gjohansson-ST marked this conversation as resolved.
Show resolved Hide resolved


class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
Loading
Loading