Typehints and cleanup for metoffice (#74338)

* Typehints and cleanup for metoffice

* add myself as owner
This commit is contained in:
avee87 2022-07-04 17:12:41 +01:00 committed by GitHub
parent b082764e30
commit b3fec4c401
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 115 additions and 63 deletions

View File

@ -153,6 +153,7 @@ homeassistant.components.luftdaten.*
homeassistant.components.mailbox.* homeassistant.components.mailbox.*
homeassistant.components.media_player.* homeassistant.components.media_player.*
homeassistant.components.media_source.* homeassistant.components.media_source.*
homeassistant.components.metoffice.*
homeassistant.components.mjpeg.* homeassistant.components.mjpeg.*
homeassistant.components.modbus.* homeassistant.components.modbus.*
homeassistant.components.modem_callerid.* homeassistant.components.modem_callerid.*

View File

@ -630,8 +630,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/meteoalarm/ @rolfberkenbosch /homeassistant/components/meteoalarm/ @rolfberkenbosch
/homeassistant/components/meteoclimatic/ @adrianmo /homeassistant/components/meteoclimatic/ @adrianmo
/tests/components/meteoclimatic/ @adrianmo /tests/components/meteoclimatic/ @adrianmo
/homeassistant/components/metoffice/ @MrHarcombe /homeassistant/components/metoffice/ @MrHarcombe @avee87
/tests/components/metoffice/ @MrHarcombe /tests/components/metoffice/ @MrHarcombe @avee87
/homeassistant/components/miflora/ @danielhiversen @basnijholt /homeassistant/components/miflora/ @danielhiversen @basnijholt
/homeassistant/components/mikrotik/ @engrbm87 /homeassistant/components/mikrotik/ @engrbm87
/tests/components/mikrotik/ @engrbm87 /tests/components/mikrotik/ @engrbm87

View File

@ -1,11 +1,15 @@
"""Config flow for Met Office integration.""" """Config flow for Met Office integration."""
from __future__ import annotations
import logging import logging
from typing import Any
import datapoint import datapoint
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, core, exceptions from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from .const import DOMAIN from .const import DOMAIN
@ -14,7 +18,9 @@ from .helpers import fetch_site
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def validate_input(hass: core.HomeAssistant, data): async def validate_input(
hass: core.HomeAssistant, data: dict[str, Any]
) -> dict[str, str]:
"""Validate that the user input allows us to connect to DataPoint. """Validate that the user input allows us to connect to DataPoint.
Data has the keys from DATA_SCHEMA with values provided by the user. Data has the keys from DATA_SCHEMA with values provided by the user.
@ -40,7 +46,9 @@ class MetOfficeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1 VERSION = 1
async def async_step_user(self, user_input=None): async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step.""" """Handle the initial step."""
errors = {} errors = {}
if user_input is not None: if user_input is not None:

View File

@ -37,7 +37,7 @@ MODE_3HOURLY_LABEL = "3-Hourly"
MODE_DAILY = "daily" MODE_DAILY = "daily"
MODE_DAILY_LABEL = "Daily" MODE_DAILY_LABEL = "Daily"
CONDITION_CLASSES = { CONDITION_CLASSES: dict[str, list[str]] = {
ATTR_CONDITION_CLEAR_NIGHT: ["0"], ATTR_CONDITION_CLEAR_NIGHT: ["0"],
ATTR_CONDITION_CLOUDY: ["7", "8"], ATTR_CONDITION_CLOUDY: ["7", "8"],
ATTR_CONDITION_FOG: ["5", "6"], ATTR_CONDITION_FOG: ["5", "6"],

View File

@ -1,11 +1,17 @@
"""Common Met Office Data class used by both sensor and entity.""" """Common Met Office Data class used by both sensor and entity."""
from dataclasses import dataclass
from datapoint.Forecast import Forecast
from datapoint.Site import Site
from datapoint.Timestep import Timestep
@dataclass
class MetOfficeData: class MetOfficeData:
"""Data structure for MetOffice weather and forecast.""" """Data structure for MetOffice weather and forecast."""
def __init__(self, now, forecast, site): now: Forecast
"""Initialize the data object.""" forecast: list[Timestep]
self.now = now site: Site
self.forecast = forecast
self.site = site

View File

@ -1,8 +1,10 @@
"""Helpers used for Met Office integration.""" """Helpers used for Met Office integration."""
from __future__ import annotations
import logging import logging
import datapoint import datapoint
from datapoint.Site import Site
from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.helpers.update_coordinator import UpdateFailed
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
@ -13,7 +15,9 @@ from .data import MetOfficeData
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def fetch_site(connection: datapoint.Manager, latitude, longitude): def fetch_site(
connection: datapoint.Manager, latitude: float, longitude: float
) -> Site | None:
"""Fetch site information from Datapoint API.""" """Fetch site information from Datapoint API."""
try: try:
return connection.get_nearest_forecast_site( return connection.get_nearest_forecast_site(
@ -24,7 +28,7 @@ def fetch_site(connection: datapoint.Manager, latitude, longitude):
return None return None
def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData: def fetch_data(connection: datapoint.Manager, site: Site, mode: str) -> MetOfficeData:
"""Fetch weather and forecast from Datapoint API.""" """Fetch weather and forecast from Datapoint API."""
try: try:
forecast = connection.get_forecast_for_site(site.id, mode) forecast = connection.get_forecast_for_site(site.id, mode)
@ -34,8 +38,8 @@ def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData:
else: else:
time_now = utcnow() time_now = utcnow()
return MetOfficeData( return MetOfficeData(
forecast.now(), now=forecast.now(),
[ forecast=[
timestep timestep
for day in forecast.days for day in forecast.days
for timestep in day.timesteps for timestep in day.timesteps
@ -44,5 +48,5 @@ def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData:
mode == MODE_3HOURLY or timestep.date.hour > 6 mode == MODE_3HOURLY or timestep.date.hour > 6
) # ensures only one result per day in MODE_DAILY ) # ensures only one result per day in MODE_DAILY
], ],
site, site=site,
) )

View File

@ -3,7 +3,7 @@
"name": "Met Office", "name": "Met Office",
"documentation": "https://www.home-assistant.io/integrations/metoffice", "documentation": "https://www.home-assistant.io/integrations/metoffice",
"requirements": ["datapoint==0.9.8"], "requirements": ["datapoint==0.9.8"],
"codeowners": ["@MrHarcombe"], "codeowners": ["@MrHarcombe", "@avee87"],
"config_flow": true, "config_flow": true,
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["datapoint"] "loggers": ["datapoint"]

View File

@ -1,6 +1,10 @@
"""Support for UK Met Office weather service.""" """Support for UK Met Office weather service."""
from __future__ import annotations from __future__ import annotations
from typing import Any
from datapoint.Element import Element
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
SensorEntity, SensorEntity,
@ -17,7 +21,10 @@ from homeassistant.const import (
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from . import get_device_info from . import get_device_info
from .const import ( from .const import (
@ -34,6 +41,7 @@ from .const import (
VISIBILITY_CLASSES, VISIBILITY_CLASSES,
VISIBILITY_DISTANCE_CLASSES, VISIBILITY_DISTANCE_CLASSES,
) )
from .data import MetOfficeData
ATTR_LAST_UPDATE = "last_update" ATTR_LAST_UPDATE = "last_update"
ATTR_SENSOR_ID = "sensor_id" ATTR_SENSOR_ID = "sensor_id"
@ -170,21 +178,24 @@ async def async_setup_entry(
) )
class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): class MetOfficeCurrentSensor(
CoordinatorEntity[DataUpdateCoordinator[MetOfficeData]], SensorEntity
):
"""Implementation of a Met Office current weather condition sensor.""" """Implementation of a Met Office current weather condition sensor."""
def __init__( def __init__(
self, self,
coordinator, coordinator: DataUpdateCoordinator[MetOfficeData],
hass_data, hass_data: dict[str, Any],
use_3hourly, use_3hourly: bool,
description: SensorEntityDescription, description: SensorEntityDescription,
): ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator) super().__init__(coordinator)
self.entity_description = description self.entity_description = description
mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL
self._attr_device_info = get_device_info( self._attr_device_info = get_device_info(
coordinates=hass_data[METOFFICE_COORDINATES], name=hass_data[METOFFICE_NAME] coordinates=hass_data[METOFFICE_COORDINATES], name=hass_data[METOFFICE_NAME]
) )
@ -192,11 +203,12 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity):
self._attr_unique_id = f"{description.name}_{hass_data[METOFFICE_COORDINATES]}" self._attr_unique_id = f"{description.name}_{hass_data[METOFFICE_COORDINATES]}"
if not use_3hourly: if not use_3hourly:
self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}"
self._attr_entity_registry_enabled_default = (
self.use_3hourly = use_3hourly self.entity_description.entity_registry_enabled_default and use_3hourly
)
@property @property
def native_value(self): def native_value(self) -> Any | None:
"""Return the state of the sensor.""" """Return the state of the sensor."""
value = None value = None
@ -224,13 +236,13 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity):
elif hasattr(self.coordinator.data.now, self.entity_description.key): elif hasattr(self.coordinator.data.now, self.entity_description.key):
value = getattr(self.coordinator.data.now, self.entity_description.key) value = getattr(self.coordinator.data.now, self.entity_description.key)
if hasattr(value, "value"): if isinstance(value, Element):
value = value.value value = value.value
return value return value
@property @property
def icon(self): def icon(self) -> str | None:
"""Return the icon for the entity card.""" """Return the icon for the entity card."""
value = self.entity_description.icon value = self.entity_description.icon
if self.entity_description.key == "weather": if self.entity_description.key == "weather":
@ -244,7 +256,7 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity):
return value return value
@property @property
def extra_state_attributes(self): def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the device.""" """Return the state attributes of the device."""
return { return {
ATTR_ATTRIBUTION: ATTRIBUTION, ATTR_ATTRIBUTION: ATTRIBUTION,
@ -253,10 +265,3 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity):
ATTR_SITE_ID: self.coordinator.data.site.id, ATTR_SITE_ID: self.coordinator.data.site.id,
ATTR_SITE_NAME: self.coordinator.data.site.name, ATTR_SITE_NAME: self.coordinator.data.site.name,
} }
@property
def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry."""
return (
self.entity_description.entity_registry_enabled_default and self.use_3hourly
)

View File

@ -1,18 +1,27 @@
"""Support for UK Met Office weather service.""" """Support for UK Met Office weather service."""
from __future__ import annotations
from typing import Any
from datapoint.Timestep import Timestep
from homeassistant.components.weather import ( from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION, ATTR_FORECAST_CONDITION,
ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_TEMP,
ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_NATIVE_WIND_SPEED,
ATTR_FORECAST_PRECIPITATION_PROBABILITY, ATTR_FORECAST_PRECIPITATION_PROBABILITY,
ATTR_FORECAST_TIME,
ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_BEARING,
Forecast,
WeatherEntity, WeatherEntity,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PRESSURE_HPA, SPEED_MILES_PER_HOUR, TEMP_CELSIUS from homeassistant.const import PRESSURE_HPA, SPEED_MILES_PER_HOUR, TEMP_CELSIUS
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from . import get_device_info from . import get_device_info
from .const import ( from .const import (
@ -28,6 +37,7 @@ from .const import (
MODE_DAILY, MODE_DAILY,
MODE_DAILY_LABEL, MODE_DAILY_LABEL,
) )
from .data import MetOfficeData
async def async_setup_entry( async def async_setup_entry(
@ -45,9 +55,8 @@ async def async_setup_entry(
) )
def _build_forecast_data(timestep): def _build_forecast_data(timestep: Timestep) -> Forecast:
data = {} data = Forecast(datetime=timestep.date.isoformat())
data[ATTR_FORECAST_TIME] = timestep.date.isoformat()
if timestep.weather: if timestep.weather:
data[ATTR_FORECAST_CONDITION] = _get_weather_condition(timestep.weather.value) data[ATTR_FORECAST_CONDITION] = _get_weather_condition(timestep.weather.value)
if timestep.precipitation: if timestep.precipitation:
@ -61,21 +70,30 @@ def _build_forecast_data(timestep):
return data return data
def _get_weather_condition(metoffice_code): def _get_weather_condition(metoffice_code: str) -> str | None:
for hass_name, metoffice_codes in CONDITION_CLASSES.items(): for hass_name, metoffice_codes in CONDITION_CLASSES.items():
if metoffice_code in metoffice_codes: if metoffice_code in metoffice_codes:
return hass_name return hass_name
return None return None
class MetOfficeWeather(CoordinatorEntity, WeatherEntity): class MetOfficeWeather(
CoordinatorEntity[DataUpdateCoordinator[MetOfficeData]], WeatherEntity
):
"""Implementation of a Met Office weather condition.""" """Implementation of a Met Office weather condition."""
_attr_attribution = ATTRIBUTION
_attr_native_temperature_unit = TEMP_CELSIUS _attr_native_temperature_unit = TEMP_CELSIUS
_attr_native_pressure_unit = PRESSURE_HPA _attr_native_pressure_unit = PRESSURE_HPA
_attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR _attr_native_wind_speed_unit = SPEED_MILES_PER_HOUR
def __init__(self, coordinator, hass_data, use_3hourly): def __init__(
self,
coordinator: DataUpdateCoordinator[MetOfficeData],
hass_data: dict[str, Any],
use_3hourly: bool,
) -> None:
"""Initialise the platform with a data instance.""" """Initialise the platform with a data instance."""
super().__init__(coordinator) super().__init__(coordinator)
@ -89,62 +107,61 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity):
self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}"
@property @property
def condition(self): def condition(self) -> str | None:
"""Return the current condition.""" """Return the current condition."""
if self.coordinator.data.now: if self.coordinator.data.now:
return _get_weather_condition(self.coordinator.data.now.weather.value) return _get_weather_condition(self.coordinator.data.now.weather.value)
return None return None
@property @property
def native_temperature(self): def native_temperature(self) -> float | None:
"""Return the platform temperature.""" """Return the platform temperature."""
if self.coordinator.data.now.temperature: weather_now = self.coordinator.data.now
return self.coordinator.data.now.temperature.value if weather_now.temperature:
value = weather_now.temperature.value
return float(value) if value is not None else None
return None return None
@property @property
def native_pressure(self): def native_pressure(self) -> float | None:
"""Return the mean sea-level pressure.""" """Return the mean sea-level pressure."""
weather_now = self.coordinator.data.now weather_now = self.coordinator.data.now
if weather_now and weather_now.pressure: if weather_now and weather_now.pressure:
return weather_now.pressure.value value = weather_now.pressure.value
return float(value) if value is not None else None
return None return None
@property @property
def humidity(self): def humidity(self) -> float | None:
"""Return the relative humidity.""" """Return the relative humidity."""
weather_now = self.coordinator.data.now weather_now = self.coordinator.data.now
if weather_now and weather_now.humidity: if weather_now and weather_now.humidity:
return weather_now.humidity.value value = weather_now.humidity.value
return float(value) if value is not None else None
return None return None
@property @property
def native_wind_speed(self): def native_wind_speed(self) -> float | None:
"""Return the wind speed.""" """Return the wind speed."""
weather_now = self.coordinator.data.now weather_now = self.coordinator.data.now
if weather_now and weather_now.wind_speed: if weather_now and weather_now.wind_speed:
return weather_now.wind_speed.value value = weather_now.wind_speed.value
return float(value) if value is not None else None
return None return None
@property @property
def wind_bearing(self): def wind_bearing(self) -> str | None:
"""Return the wind bearing.""" """Return the wind bearing."""
weather_now = self.coordinator.data.now weather_now = self.coordinator.data.now
if weather_now and weather_now.wind_direction: if weather_now and weather_now.wind_direction:
return weather_now.wind_direction.value value = weather_now.wind_direction.value
return str(value) if value is not None else None
return None return None
@property @property
def forecast(self): def forecast(self) -> list[Forecast] | None:
"""Return the forecast array.""" """Return the forecast array."""
if self.coordinator.data.forecast is None:
return None
return [ return [
_build_forecast_data(timestep) _build_forecast_data(timestep)
for timestep in self.coordinator.data.forecast for timestep in self.coordinator.data.forecast
] ]
@property
def attribution(self):
"""Return the attribution."""
return ATTRIBUTION

View File

@ -1446,6 +1446,17 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.metoffice.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.mjpeg.*] [mypy-homeassistant.components.mjpeg.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true