Code quality improvements smhi (#64312)

This commit is contained in:
G Johansson 2022-01-17 20:27:23 +01:00 committed by GitHub
parent 7e40707288
commit bbb29ab455
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 63 additions and 131 deletions

View File

@ -154,6 +154,7 @@ homeassistant.components.senseme.*
homeassistant.components.shelly.* homeassistant.components.shelly.*
homeassistant.components.simplisafe.* homeassistant.components.simplisafe.*
homeassistant.components.slack.* homeassistant.components.slack.*
homeassistant.components.smhi.*
homeassistant.components.sonos.media_player homeassistant.components.sonos.media_player
homeassistant.components.ssdp.* homeassistant.components.ssdp.*
homeassistant.components.stookalert.* homeassistant.components.stookalert.*

View File

@ -847,6 +847,8 @@ tests/components/smartthings/* @andrewsayre
homeassistant/components/smarttub/* @mdz homeassistant/components/smarttub/* @mdz
tests/components/smarttub/* @mdz tests/components/smarttub/* @mdz
homeassistant/components/smarty/* @z0mbieprocess homeassistant/components/smarty/* @z0mbieprocess
homeassistant/components/smhi/* @gjohansson-ST
tests/components/smhi/* @gjohansson-ST
homeassistant/components/sms/* @ocalvo homeassistant/components/sms/* @ocalvo
homeassistant/components/smtp/* @fabaff homeassistant/components/smtp/* @fabaff
tests/components/smtp/* @fabaff tests/components/smtp/* @fabaff

View File

@ -4,6 +4,6 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/smhi", "documentation": "https://www.home-assistant.io/integrations/smhi",
"requirements": ["smhi-pkg==1.0.15"], "requirements": ["smhi-pkg==1.0.15"],
"codeowners": [], "codeowners": ["@gjohansson-ST"],
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
} }

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio import asyncio
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
from typing import Final, TypedDict from typing import Final
import aiohttp import aiohttp
import async_timeout import async_timeout
@ -35,7 +35,14 @@ from homeassistant.components.weather import (
WeatherEntity, WeatherEntity,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS from homeassistant.const import (
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
LENGTH_KILOMETERS,
LENGTH_MILLIMETERS,
TEMP_CELSIUS,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.device_registry import DeviceEntryType
@ -72,6 +79,7 @@ CONDITION_CLASSES: Final[dict[str, list[int]]] = {
ATTR_CONDITION_EXCEPTIONAL: [], ATTR_CONDITION_EXCEPTIONAL: [],
} }
TIMEOUT = 10
# 5 minutes between retrying connect to API again # 5 minutes between retrying connect to API again
RETRY_TIMEOUT = 5 * 60 RETRY_TIMEOUT = 5 * 60
@ -103,6 +111,11 @@ async def async_setup_entry(
class SmhiWeather(WeatherEntity): class SmhiWeather(WeatherEntity):
"""Representation of a weather entity.""" """Representation of a weather entity."""
_attr_attribution = "Swedish weather institute (SMHI)"
_attr_temperature_unit = TEMP_CELSIUS
_attr_visibility_unit = LENGTH_KILOMETERS
_attr_precipitation_unit = LENGTH_MILLIMETERS
def __init__( def __init__(
self, self,
name: str, name: str,
@ -112,39 +125,58 @@ class SmhiWeather(WeatherEntity):
) -> None: ) -> None:
"""Initialize the SMHI weather entity.""" """Initialize the SMHI weather entity."""
self._name = name self._attr_name = name
self._latitude = latitude self._attr_unique_id = f"{latitude}, {longitude}"
self._longitude = longitude
self._forecasts: list[SmhiForecast] | None = None self._forecasts: list[SmhiForecast] | None = None
self._fail_count = 0 self._fail_count = 0
self._smhi_api = Smhi(self._longitude, self._latitude, session=session) self._smhi_api = Smhi(longitude, latitude, session=session)
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE, entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, f"{self._latitude}, {self._longitude}")}, identifiers={(DOMAIN, f"{latitude}, {longitude}")},
manufacturer="SMHI", manufacturer="SMHI",
model="v2", model="v2",
name=self._name, name=name,
configuration_url="http://opendata.smhi.se/apidocs/metfcst/parameters.html", configuration_url="http://opendata.smhi.se/apidocs/metfcst/parameters.html",
) )
self._attr_condition = None
@property self._attr_temperature = None
def unique_id(self) -> str:
"""Return a unique id."""
return f"{self._latitude}, {self._longitude}"
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
async def async_update(self) -> None: async def async_update(self) -> None:
"""Refresh the forecast data from SMHI weather API.""" """Refresh the forecast data from SMHI weather API."""
try: try:
async with async_timeout.timeout(10): async with async_timeout.timeout(TIMEOUT):
self._forecasts = await self.get_weather_forecast() self._forecasts = await self._smhi_api.async_get_forecast()
self._fail_count = 0 self._fail_count = 0
except (asyncio.TimeoutError, SmhiForecastException): except (asyncio.TimeoutError, SmhiForecastException):
_LOGGER.error("Failed to connect to SMHI API, retry in 5 minutes") _LOGGER.error("Failed to connect to SMHI API, retry in 5 minutes")
self._fail_count += 1 self._fail_count += 1
if self._fail_count < 3: if self._fail_count < 3:
async_call_later(self.hass, RETRY_TIMEOUT, self.retry_update) async_call_later(self.hass, RETRY_TIMEOUT, self.retry_update)
return
if self._forecasts:
self._attr_temperature = self._forecasts[0].temperature
self._attr_humidity = self._forecasts[0].humidity
# Convert from m/s to km/h
self._attr_wind_speed = round(self._forecasts[0].wind_speed * 18 / 5)
self._attr_wind_bearing = self._forecasts[0].wind_direction
self._attr_visibility = self._forecasts[0].horizontal_visibility
self._attr_pressure = self._forecasts[0].pressure
self._attr_condition = next(
(
k
for k, v in CONDITION_CLASSES.items()
if self._forecasts[0].symbol in v
),
None,
)
self._attr_extra_state_attributes = {
ATTR_SMHI_CLOUDINESS: self._forecasts[0].cloudiness,
# Convert from m/s to km/h
ATTR_SMHI_WIND_GUST_SPEED: round(self._forecasts[0].wind_gust * 18 / 5),
ATTR_SMHI_THUNDER_PROBABILITY: self._forecasts[0].thunder,
}
async def retry_update(self, _: datetime) -> None: async def retry_update(self, _: datetime) -> None:
"""Retry refresh weather forecast.""" """Retry refresh weather forecast."""
@ -152,100 +184,6 @@ class SmhiWeather(WeatherEntity):
no_throttle=True no_throttle=True
) )
async def get_weather_forecast(self) -> list[SmhiForecast]:
"""Return the current forecasts from SMHI API."""
return await self._smhi_api.async_get_forecast()
@property
def name(self) -> str:
"""Return the name of the sensor."""
return self._name
@property
def temperature(self) -> int | None:
"""Return the temperature."""
if self._forecasts is not None:
return self._forecasts[0].temperature
return None
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
return TEMP_CELSIUS
@property
def humidity(self) -> int | None:
"""Return the humidity."""
if self._forecasts is not None:
return self._forecasts[0].humidity
return None
@property
def wind_speed(self) -> float | None:
"""Return the wind speed."""
if self._forecasts is not None:
# Convert from m/s to km/h
return round(self._forecasts[0].wind_speed * 18 / 5)
return None
@property
def wind_gust_speed(self) -> float | None:
"""Return the wind gust speed."""
if self._forecasts is not None:
# Convert from m/s to km/h
return round(self._forecasts[0].wind_gust * 18 / 5)
return None
@property
def wind_bearing(self) -> int | None:
"""Return the wind bearing."""
if self._forecasts is not None:
return self._forecasts[0].wind_direction
return None
@property
def visibility(self) -> float | None:
"""Return the visibility."""
if self._forecasts is not None:
return self._forecasts[0].horizontal_visibility
return None
@property
def pressure(self) -> int | None:
"""Return the pressure."""
if self._forecasts is not None:
return self._forecasts[0].pressure
return None
@property
def cloudiness(self) -> int | None:
"""Return the cloudiness."""
if self._forecasts is not None:
return self._forecasts[0].cloudiness
return None
@property
def thunder_probability(self) -> int | None:
"""Return the chance of thunder, unit Percent."""
if self._forecasts is not None:
return self._forecasts[0].thunder
return None
@property
def condition(self) -> str | None:
"""Return the weather condition."""
if self._forecasts is None:
return None
return next(
(k for k, v in CONDITION_CLASSES.items() if self._forecasts[0].symbol in v),
None,
)
@property
def attribution(self) -> str:
"""Return the attribution."""
return "Swedish weather institute (SMHI)"
@property @property
def forecast(self) -> list[Forecast] | None: def forecast(self) -> list[Forecast] | None:
"""Return the forecast.""" """Return the forecast."""
@ -270,23 +208,3 @@ class SmhiWeather(WeatherEntity):
) )
return data return data
@property
def extra_state_attributes(self) -> ExtraAttributes:
"""Return SMHI specific attributes."""
extra_attributes: ExtraAttributes = {}
if self.cloudiness is not None:
extra_attributes[ATTR_SMHI_CLOUDINESS] = self.cloudiness
if self.wind_gust_speed is not None:
extra_attributes[ATTR_SMHI_WIND_GUST_SPEED] = self.wind_gust_speed
if self.thunder_probability is not None:
extra_attributes[ATTR_SMHI_THUNDER_PROBABILITY] = self.thunder_probability
return extra_attributes
class ExtraAttributes(TypedDict, total=False):
"""Represent the extra state attribute types."""
cloudiness: int
thunder_probability: int
wind_gust_speed: float

View File

@ -1506,6 +1506,17 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.smhi.*]
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.sonos.media_player] [mypy-homeassistant.components.sonos.media_player]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true