Enforce strict typing for OpenUV (#53409)

* Enforce strict typing for OpenUV

* Linting

* Fix tests
This commit is contained in:
Aaron Bach 2021-07-24 06:50:01 -06:00 committed by GitHub
parent 5c86cc502f
commit 54ace4cdd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 92 additions and 66 deletions

View file

@ -68,6 +68,7 @@ homeassistant.components.notify.*
homeassistant.components.notion.*
homeassistant.components.number.*
homeassistant.components.onewire.*
homeassistant.components.openuv.*
homeassistant.components.persistent_notification.*
homeassistant.components.pi_hole.*
homeassistant.components.proximity.*

View file

@ -1,9 +1,14 @@
"""Support for UV data from openuv.io."""
from __future__ import annotations
import asyncio
from collections.abc import MutableMapping
from typing import Any
from pyopenuv import Client
from pyopenuv.errors import OpenUvError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_API_KEY,
@ -13,7 +18,7 @@ from homeassistant.const import (
CONF_LONGITUDE,
CONF_SENSORS,
)
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.dispatcher import (
@ -42,14 +47,10 @@ TOPIC_UPDATE = f"{DOMAIN}_data_update"
PLATFORMS = ["binary_sensor", "sensor"]
async def async_setup(hass, config):
"""Set up the OpenUV component."""
hass.data[DOMAIN] = {DATA_CLIENT: {}, DATA_LISTENER: {}}
return True
async def async_setup_entry(hass, config_entry):
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up OpenUV as config entry."""
hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}, DATA_LISTENER: {}})
_verify_domain_control = verify_domain_control(hass, DOMAIN)
try:
@ -72,21 +73,21 @@ async def async_setup_entry(hass, config_entry):
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
@_verify_domain_control
async def update_data(service):
async def update_data(_: ServiceCall) -> None:
"""Refresh all OpenUV data."""
LOGGER.debug("Refreshing all OpenUV data")
await openuv.async_update()
async_dispatcher_send(hass, TOPIC_UPDATE)
@_verify_domain_control
async def update_uv_index_data(service):
async def update_uv_index_data(_: ServiceCall) -> None:
"""Refresh OpenUV UV index data."""
LOGGER.debug("Refreshing OpenUV UV index data")
await openuv.async_update_uv_index_data()
async_dispatcher_send(hass, TOPIC_UPDATE)
@_verify_domain_control
async def update_protection_data(service):
async def update_protection_data(_: ServiceCall) -> None:
"""Refresh OpenUV protection window data."""
LOGGER.debug("Refreshing OpenUV protection window data")
await openuv.async_update_protection_data()
@ -102,7 +103,7 @@ async def async_setup_entry(hass, config_entry):
return True
async def async_unload_entry(hass, config_entry):
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload an OpenUV config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
@ -113,7 +114,7 @@ async def async_unload_entry(hass, config_entry):
return unload_ok
async def async_migrate_entry(hass, config_entry):
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate the config entry upon new versions."""
version = config_entry.version
data = {**config_entry.data}
@ -134,12 +135,12 @@ async def async_migrate_entry(hass, config_entry):
class OpenUV:
"""Define a generic OpenUV object."""
def __init__(self, client):
def __init__(self, client: Client) -> None:
"""Initialize."""
self.client = client
self.data = {}
self.data: dict[str, Any] = {}
async def async_update_protection_data(self):
async def async_update_protection_data(self) -> None:
"""Update binary sensor (protection window) data."""
try:
resp = await self.client.uv_protection_window()
@ -148,7 +149,7 @@ class OpenUV:
LOGGER.error("Error during protection data update: %s", err)
self.data[DATA_PROTECTION_WINDOW] = {}
async def async_update_uv_index_data(self):
async def async_update_uv_index_data(self) -> None:
"""Update sensor (uv index, etc) data."""
try:
data = await self.client.uv_index()
@ -157,7 +158,7 @@ class OpenUV:
LOGGER.error("Error during uv index data update: %s", err)
self.data[DATA_UV] = {}
async def async_update(self):
async def async_update(self) -> None:
"""Update sensor/binary sensor data."""
tasks = [self.async_update_protection_data(), self.async_update_uv_index_data()]
await asyncio.gather(*tasks)
@ -166,9 +167,11 @@ class OpenUV:
class OpenUvEntity(Entity):
"""Define a generic OpenUV entity."""
def __init__(self, openuv, sensor_type):
def __init__(self, openuv: OpenUV, sensor_type: str) -> None:
"""Initialize."""
self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
self._attr_extra_state_attributes: MutableMapping[str, Any] = {
ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION
}
self._attr_should_poll = False
self._attr_unique_id = (
f"{openuv.client.latitude}_{openuv.client.longitude}_{sensor_type}"
@ -176,11 +179,11 @@ class OpenUvEntity(Entity):
self._sensor_type = sensor_type
self.openuv = openuv
async def async_added_to_hass(self):
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@callback
def update():
def update() -> None:
"""Update the state."""
self.update_from_latest_data()
self.async_write_ha_state()
@ -189,6 +192,6 @@ class OpenUvEntity(Entity):
self.update_from_latest_data()
def update_from_latest_data(self):
def update_from_latest_data(self) -> None:
"""Update the sensor using the latest data."""
raise NotImplementedError

View file

@ -1,9 +1,11 @@
"""Support for OpenUV binary sensors."""
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.core import callback
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import as_local, parse_datetime, utcnow
from . import OpenUvEntity
from . import OpenUV, OpenUvEntity
from .const import (
DATA_CLIENT,
DATA_PROTECTION_WINDOW,
@ -20,7 +22,9 @@ ATTR_PROTECTION_WINDOW_STARTING_UV = "start_uv"
BINARY_SENSORS = {TYPE_PROTECTION_WINDOW: ("Protection Window", "mdi:sunglasses")}
async def async_setup_entry(hass, entry, async_add_entities):
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up an OpenUV sensor based on a config entry."""
openuv = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
@ -35,7 +39,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity):
"""Define a binary sensor for OpenUV."""
def __init__(self, openuv, sensor_type, name, icon):
def __init__(self, openuv: OpenUV, sensor_type: str, name: str, icon: str) -> None:
"""Initialize the sensor."""
super().__init__(openuv, sensor_type)
@ -43,7 +47,7 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity):
self._attr_name = name
@callback
def update_from_latest_data(self):
def update_from_latest_data(self) -> None:
"""Update the state."""
data = self.openuv.data[DATA_PROTECTION_WINDOW]
@ -59,20 +63,24 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity):
return
if self._sensor_type == TYPE_PROTECTION_WINDOW:
self._attr_is_on = (
parse_datetime(data["from_time"])
<= utcnow()
<= parse_datetime(data["to_time"])
from_dt = parse_datetime(data["from_time"])
to_dt = parse_datetime(data["to_time"])
if not from_dt or not to_dt:
LOGGER.warning(
"Unable to parse protection window datetimes: %s, %s",
data["from_time"],
data["to_time"],
)
self._attr_is_on = False
return
self._attr_is_on = from_dt <= utcnow() <= to_dt
self._attr_extra_state_attributes.update(
{
ATTR_PROTECTION_WINDOW_ENDING_TIME: as_local(
parse_datetime(data["to_time"])
),
ATTR_PROTECTION_WINDOW_ENDING_TIME: as_local(to_dt),
ATTR_PROTECTION_WINDOW_ENDING_UV: data["to_uv"],
ATTR_PROTECTION_WINDOW_STARTING_UV: data["from_uv"],
ATTR_PROTECTION_WINDOW_STARTING_TIME: as_local(
parse_datetime(data["from_time"])
),
ATTR_PROTECTION_WINDOW_STARTING_TIME: as_local(from_dt),
}
)

View file

@ -1,4 +1,8 @@
"""Config flow to configure the OpenUV component."""
from __future__ import annotations
from typing import Any
from pyopenuv import Client
from pyopenuv.errors import OpenUvError
import voluptuous as vol
@ -10,6 +14,7 @@ from homeassistant.const import (
CONF_LATITUDE,
CONF_LONGITUDE,
)
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client, config_validation as cv
from .const import DOMAIN
@ -21,7 +26,7 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 2
@property
def config_schema(self):
def config_schema(self) -> vol.Schema:
"""Return the config schema."""
return vol.Schema(
{
@ -38,7 +43,7 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
}
)
async def _show_form(self, errors=None):
async def _show_form(self, errors: dict[str, Any] | None = None) -> FlowResult:
"""Show the form to the user."""
return self.async_show_form(
step_id="user",
@ -46,11 +51,13 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
errors=errors if errors else {},
)
async def async_step_import(self, import_config):
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
"""Import a config entry from configuration.yaml."""
return await self.async_step_user(import_config)
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 start of the config flow."""
if not user_input:
return await self._show_form()

View file

@ -1,10 +1,14 @@
"""Support for OpenUV sensors."""
from __future__ import annotations
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import TIME_MINUTES, UV_INDEX
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import as_local, parse_datetime
from . import OpenUvEntity
from . import OpenUV, OpenUvEntity
from .const import (
DATA_CLIENT,
DATA_UV,
@ -76,7 +80,9 @@ SENSORS = {
}
async def async_setup_entry(hass, entry, async_add_entities):
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up a OpenUV sensor based on a config entry."""
openuv = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
@ -91,7 +97,9 @@ async def async_setup_entry(hass, entry, async_add_entities):
class OpenUvSensor(OpenUvEntity, SensorEntity):
"""Define a binary sensor for OpenUV."""
def __init__(self, openuv, sensor_type, name, icon, unit):
def __init__(
self, openuv: OpenUV, sensor_type: str, name: str, icon: str, unit: str | None
) -> None:
"""Initialize the sensor."""
super().__init__(openuv, sensor_type)
@ -100,7 +108,7 @@ class OpenUvSensor(OpenUvEntity, SensorEntity):
self._attr_unit_of_measurement = unit
@callback
def update_from_latest_data(self):
def update_from_latest_data(self) -> None:
"""Update the state."""
data = self.openuv.data[DATA_UV].get("result")
@ -127,8 +135,10 @@ class OpenUvSensor(OpenUvEntity, SensorEntity):
self._attr_state = UV_LEVEL_LOW
elif self._sensor_type == TYPE_MAX_UV_INDEX:
self._attr_state = data["uv_max"]
uv_max_time = parse_datetime(data["uv_max_time"])
if uv_max_time:
self._attr_extra_state_attributes.update(
{ATTR_MAX_UV_TIME: as_local(parse_datetime(data["uv_max_time"]))}
{ATTR_MAX_UV_TIME: as_local(uv_max_time)}
)
elif self._sensor_type in (
TYPE_SAFE_EXPOSURE_TIME_1,

View file

@ -759,6 +759,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.openuv.*]
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.persistent_notification.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View file

@ -2,7 +2,6 @@
from unittest.mock import patch
from pyopenuv.errors import InvalidApiKeyError
import pytest
from homeassistant import data_entry_flow
from homeassistant.components.openuv import DOMAIN
@ -17,19 +16,6 @@ from homeassistant.const import (
from tests.common import MockConfigEntry
@pytest.fixture(autouse=True)
def mock_setup():
"""Prevent setup."""
with patch(
"homeassistant.components.openuv.async_setup",
return_value=True,
), patch(
"homeassistant.components.openuv.async_setup_entry",
return_value=True,
):
yield
async def test_duplicate_error(hass):
"""Test that errors are shown when duplicates are added."""
conf = {
@ -81,7 +67,7 @@ async def test_step_user(hass):
}
with patch(
"homeassistant.components.airvisual.async_setup_entry", return_value=True
"homeassistant.components.openuv.async_setup_entry", return_value=True
), patch("pyopenuv.client.Client.uv_index"):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}