diff --git a/.strict-typing b/.strict-typing index 1832a83641a..ebfed5dfa5b 100644 --- a/.strict-typing +++ b/.strict-typing @@ -153,6 +153,7 @@ homeassistant.components.luftdaten.* homeassistant.components.mailbox.* homeassistant.components.media_player.* homeassistant.components.media_source.* +homeassistant.components.metoffice.* homeassistant.components.mjpeg.* homeassistant.components.modbus.* homeassistant.components.modem_callerid.* diff --git a/CODEOWNERS b/CODEOWNERS index 6ae1c9605db..9f53aeab34e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -630,8 +630,8 @@ build.json @home-assistant/supervisor /homeassistant/components/meteoalarm/ @rolfberkenbosch /homeassistant/components/meteoclimatic/ @adrianmo /tests/components/meteoclimatic/ @adrianmo -/homeassistant/components/metoffice/ @MrHarcombe -/tests/components/metoffice/ @MrHarcombe +/homeassistant/components/metoffice/ @MrHarcombe @avee87 +/tests/components/metoffice/ @MrHarcombe @avee87 /homeassistant/components/miflora/ @danielhiversen @basnijholt /homeassistant/components/mikrotik/ @engrbm87 /tests/components/mikrotik/ @engrbm87 diff --git a/homeassistant/components/metoffice/config_flow.py b/homeassistant/components/metoffice/config_flow.py index 959680a90ec..3cf3b0fcda0 100644 --- a/homeassistant/components/metoffice/config_flow.py +++ b/homeassistant/components/metoffice/config_flow.py @@ -1,11 +1,15 @@ """Config flow for Met Office integration.""" +from __future__ import annotations + import logging +from typing import Any import datapoint import voluptuous as vol from homeassistant import config_entries, core, exceptions 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 .const import DOMAIN @@ -14,7 +18,9 @@ from .helpers import fetch_site _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. 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 - 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.""" errors = {} if user_input is not None: diff --git a/homeassistant/components/metoffice/const.py b/homeassistant/components/metoffice/const.py index e413b102898..12f88cc6d56 100644 --- a/homeassistant/components/metoffice/const.py +++ b/homeassistant/components/metoffice/const.py @@ -37,7 +37,7 @@ MODE_3HOURLY_LABEL = "3-Hourly" MODE_DAILY = "daily" MODE_DAILY_LABEL = "Daily" -CONDITION_CLASSES = { +CONDITION_CLASSES: dict[str, list[str]] = { ATTR_CONDITION_CLEAR_NIGHT: ["0"], ATTR_CONDITION_CLOUDY: ["7", "8"], ATTR_CONDITION_FOG: ["5", "6"], diff --git a/homeassistant/components/metoffice/data.py b/homeassistant/components/metoffice/data.py index 607c09e90b6..4b2741ce0fb 100644 --- a/homeassistant/components/metoffice/data.py +++ b/homeassistant/components/metoffice/data.py @@ -1,11 +1,17 @@ """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: """Data structure for MetOffice weather and forecast.""" - def __init__(self, now, forecast, site): - """Initialize the data object.""" - self.now = now - self.forecast = forecast - self.site = site + now: Forecast + forecast: list[Timestep] + site: Site diff --git a/homeassistant/components/metoffice/helpers.py b/homeassistant/components/metoffice/helpers.py index 00d5e73501d..ecef7e5ddcb 100644 --- a/homeassistant/components/metoffice/helpers.py +++ b/homeassistant/components/metoffice/helpers.py @@ -1,8 +1,10 @@ """Helpers used for Met Office integration.""" +from __future__ import annotations import logging import datapoint +from datapoint.Site import Site from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.util.dt import utcnow @@ -13,7 +15,9 @@ from .data import MetOfficeData _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.""" try: return connection.get_nearest_forecast_site( @@ -24,7 +28,7 @@ def fetch_site(connection: datapoint.Manager, latitude, longitude): 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.""" try: forecast = connection.get_forecast_for_site(site.id, mode) @@ -34,8 +38,8 @@ def fetch_data(connection: datapoint.Manager, site, mode) -> MetOfficeData: else: time_now = utcnow() return MetOfficeData( - forecast.now(), - [ + now=forecast.now(), + forecast=[ timestep for day in forecast.days 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 ) # ensures only one result per day in MODE_DAILY ], - site, + site=site, ) diff --git a/homeassistant/components/metoffice/manifest.json b/homeassistant/components/metoffice/manifest.json index d38d2d8cffe..887ecb3578d 100644 --- a/homeassistant/components/metoffice/manifest.json +++ b/homeassistant/components/metoffice/manifest.json @@ -3,7 +3,7 @@ "name": "Met Office", "documentation": "https://www.home-assistant.io/integrations/metoffice", "requirements": ["datapoint==0.9.8"], - "codeowners": ["@MrHarcombe"], + "codeowners": ["@MrHarcombe", "@avee87"], "config_flow": true, "iot_class": "cloud_polling", "loggers": ["datapoint"] diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 699b137c55f..e24e2299be4 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -1,6 +1,10 @@ """Support for UK Met Office weather service.""" from __future__ import annotations +from typing import Any + +from datapoint.Element import Element + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -17,7 +21,10 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant 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 .const import ( @@ -34,6 +41,7 @@ from .const import ( VISIBILITY_CLASSES, VISIBILITY_DISTANCE_CLASSES, ) +from .data import MetOfficeData ATTR_LAST_UPDATE = "last_update" 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.""" def __init__( self, - coordinator, - hass_data, - use_3hourly, + coordinator: DataUpdateCoordinator[MetOfficeData], + hass_data: dict[str, Any], + use_3hourly: bool, description: SensorEntityDescription, - ): + ) -> None: """Initialize the sensor.""" super().__init__(coordinator) self.entity_description = description mode_label = MODE_3HOURLY_LABEL if use_3hourly else MODE_DAILY_LABEL + self._attr_device_info = get_device_info( 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]}" if not use_3hourly: self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" - - self.use_3hourly = use_3hourly + self._attr_entity_registry_enabled_default = ( + self.entity_description.entity_registry_enabled_default and use_3hourly + ) @property - def native_value(self): + def native_value(self) -> Any | None: """Return the state of the sensor.""" value = None @@ -224,13 +236,13 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): elif hasattr(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 return value @property - def icon(self): + def icon(self) -> str | None: """Return the icon for the entity card.""" value = self.entity_description.icon if self.entity_description.key == "weather": @@ -244,7 +256,7 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): return value @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes of the device.""" return { ATTR_ATTRIBUTION: ATTRIBUTION, @@ -253,10 +265,3 @@ class MetOfficeCurrentSensor(CoordinatorEntity, SensorEntity): ATTR_SITE_ID: self.coordinator.data.site.id, 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 - ) diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index f4e0bf61d30..184782d4c12 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -1,18 +1,27 @@ """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 ( ATTR_FORECAST_CONDITION, ATTR_FORECAST_NATIVE_TEMP, ATTR_FORECAST_NATIVE_WIND_SPEED, ATTR_FORECAST_PRECIPITATION_PROBABILITY, - ATTR_FORECAST_TIME, ATTR_FORECAST_WIND_BEARING, + Forecast, WeatherEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import PRESSURE_HPA, SPEED_MILES_PER_HOUR, TEMP_CELSIUS from homeassistant.core import HomeAssistant 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 .const import ( @@ -28,6 +37,7 @@ from .const import ( MODE_DAILY, MODE_DAILY_LABEL, ) +from .data import MetOfficeData async def async_setup_entry( @@ -45,9 +55,8 @@ async def async_setup_entry( ) -def _build_forecast_data(timestep): - data = {} - data[ATTR_FORECAST_TIME] = timestep.date.isoformat() +def _build_forecast_data(timestep: Timestep) -> Forecast: + data = Forecast(datetime=timestep.date.isoformat()) if timestep.weather: data[ATTR_FORECAST_CONDITION] = _get_weather_condition(timestep.weather.value) if timestep.precipitation: @@ -61,21 +70,30 @@ def _build_forecast_data(timestep): 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(): if metoffice_code in metoffice_codes: return hass_name return None -class MetOfficeWeather(CoordinatorEntity, WeatherEntity): +class MetOfficeWeather( + CoordinatorEntity[DataUpdateCoordinator[MetOfficeData]], WeatherEntity +): """Implementation of a Met Office weather condition.""" + _attr_attribution = ATTRIBUTION + _attr_native_temperature_unit = TEMP_CELSIUS _attr_native_pressure_unit = PRESSURE_HPA _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.""" super().__init__(coordinator) @@ -89,62 +107,61 @@ class MetOfficeWeather(CoordinatorEntity, WeatherEntity): self._attr_unique_id = f"{self._attr_unique_id}_{MODE_DAILY}" @property - def condition(self): + def condition(self) -> str | None: """Return the current condition.""" if self.coordinator.data.now: return _get_weather_condition(self.coordinator.data.now.weather.value) return None @property - def native_temperature(self): + def native_temperature(self) -> float | None: """Return the platform temperature.""" - if self.coordinator.data.now.temperature: - return self.coordinator.data.now.temperature.value + weather_now = self.coordinator.data.now + if weather_now.temperature: + value = weather_now.temperature.value + return float(value) if value is not None else None return None @property - def native_pressure(self): + def native_pressure(self) -> float | None: """Return the mean sea-level pressure.""" weather_now = self.coordinator.data.now 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 @property - def humidity(self): + def humidity(self) -> float | None: """Return the relative humidity.""" weather_now = self.coordinator.data.now 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 @property - def native_wind_speed(self): + def native_wind_speed(self) -> float | None: """Return the wind speed.""" weather_now = self.coordinator.data.now 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 @property - def wind_bearing(self): + def wind_bearing(self) -> str | None: """Return the wind bearing.""" weather_now = self.coordinator.data.now 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 @property - def forecast(self): + def forecast(self) -> list[Forecast] | None: """Return the forecast array.""" - if self.coordinator.data.forecast is None: - return None return [ _build_forecast_data(timestep) for timestep in self.coordinator.data.forecast ] - - @property - def attribution(self): - """Return the attribution.""" - return ATTRIBUTION diff --git a/mypy.ini b/mypy.ini index 22a6fd801c3..77830664696 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1446,6 +1446,17 @@ no_implicit_optional = true warn_return_any = 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.*] check_untyped_defs = true disallow_incomplete_defs = true