Enforce strict typing for RainMachine (#53414)

This commit is contained in:
Aaron Bach 2021-07-27 02:45:44 -06:00 committed by GitHub
parent 5483ab0cda
commit a6b34924be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 83 additions and 61 deletions

View File

@ -74,6 +74,7 @@ homeassistant.components.openuv.*
homeassistant.components.persistent_notification.*
homeassistant.components.pi_hole.*
homeassistant.components.proximity.*
homeassistant.components.rainmachine.*
homeassistant.components.recorder.purge
homeassistant.components.recorder.repack
homeassistant.components.recorder.statistics

View File

@ -1,7 +1,10 @@
"""Support for RainMachine devices."""
from __future__ import annotations
import asyncio
from datetime import timedelta
from functools import partial
from typing import Any
from regenmaschine import Client
from regenmaschine.controller import Controller
@ -93,7 +96,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.entry_id
] = get_client_controller(client)
entry_updates = {}
entry_updates: dict[str, Any] = {}
if not entry.unique_id or is_ip_address(entry.unique_id):
# If the config entry doesn't already have a unique ID, set one:
entry_updates["unique_id"] = controller.mac
@ -111,23 +114,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_update(api_category: str) -> dict:
"""Update the appropriate API data based on a category."""
data: dict = {}
try:
if api_category == DATA_PROGRAMS:
return await controller.programs.all(include_inactive=True)
if api_category == DATA_PROVISION_SETTINGS:
return await controller.provisioning.settings()
if api_category == DATA_RESTRICTIONS_CURRENT:
return await controller.restrictions.current()
if api_category == DATA_RESTRICTIONS_UNIVERSAL:
return await controller.restrictions.universal()
return await controller.zones.all(details=True, include_inactive=True)
data = await controller.programs.all(include_inactive=True)
elif api_category == DATA_PROVISION_SETTINGS:
data = await controller.provisioning.settings()
elif api_category == DATA_RESTRICTIONS_CURRENT:
data = await controller.restrictions.current()
elif api_category == DATA_RESTRICTIONS_UNIVERSAL:
data = await controller.restrictions.universal()
else:
data = await controller.zones.all(details=True, include_inactive=True)
except RainMachineError as err:
raise UpdateFailed(err) from err
return data
controller_init_tasks = []
for api_category in (
DATA_PROGRAMS,
@ -201,12 +205,12 @@ class RainMachineEntity(CoordinatorEntity):
self._entity_type = entity_type
@callback
def _handle_coordinator_update(self):
def _handle_coordinator_update(self) -> None:
"""Respond to a DataUpdateCoordinator update."""
self.update_from_latest_data()
self.async_write_ha_state()
async def async_added_to_hass(self):
async def async_added_to_hass(self) -> None:
"""Handle entity which will be added."""
await super().async_added_to_hass()
self.update_from_latest_data()

View File

@ -1,31 +1,33 @@
"""Config flow to configure the RainMachine component."""
from __future__ import annotations
from typing import Any
from regenmaschine import Client
from regenmaschine.controller import Controller
from regenmaschine.errors import RainMachineError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.typing import DiscoveryInfoType
from .const import CONF_ZONE_RUN_TIME, DEFAULT_PORT, DEFAULT_ZONE_RUN, DOMAIN
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_IP_ADDRESS): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
}
)
def get_client_controller(client):
@callback
def get_client_controller(client: Client) -> Controller:
"""Return the first local controller."""
return next(iter(client.controllers.values()))
async def async_get_controller(hass, ip_address, password, port, ssl):
async def async_get_controller(
hass: HomeAssistant, ip_address: str, password: str, port: int, ssl: bool
) -> Controller | None:
"""Auth and fetch the mac address from the controller."""
websession = aiohttp_client.async_get_clientsession(hass)
client = Client(session=websession)
@ -42,21 +44,23 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
def __init__(self):
"""Initialize config flow."""
self.discovered_ip_address = None
discovered_ip_address: str | None = None
@staticmethod
@callback
def async_get_options_flow(config_entry):
def async_get_options_flow(
config_entry: ConfigEntry,
) -> RainMachineOptionsFlowHandler:
"""Define the config flow to handle options."""
return RainMachineOptionsFlowHandler(config_entry)
async def async_step_homekit(self, discovery_info):
async def async_step_homekit(self, discovery_info: DiscoveryInfoType) -> FlowResult:
"""Handle a flow initialized by homekit discovery."""
return await self.async_step_zeroconf(discovery_info)
async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType):
async def async_step_zeroconf(
self, discovery_info: DiscoveryInfoType
) -> FlowResult:
"""Handle discovery via zeroconf."""
ip_address = discovery_info["host"]
@ -86,7 +90,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_user()
@callback
def _async_generate_schema(self):
def _async_generate_schema(self) -> vol.Schema:
"""Generate schema."""
return vol.Schema(
{
@ -96,7 +100,9 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
}
)
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."""
errors = {}
if user_input:
@ -134,6 +140,7 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if self.discovered_ip_address:
self.context["title_placeholders"] = {"ip": self.discovered_ip_address}
return self.async_show_form(
step_id="user", data_schema=self._async_generate_schema(), errors=errors
)
@ -142,11 +149,13 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
class RainMachineOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle a RainMachine options flow."""
def __init__(self, config_entry):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize."""
self.config_entry = config_entry
async def async_step_init(self, user_input=None):
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)

View File

@ -3,7 +3,7 @@
"name": "RainMachine",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rainmachine",
"requirements": ["regenmaschine==3.0.0"],
"requirements": ["regenmaschine==3.1.5"],
"codeowners": ["@bachya"],
"iot_class": "local_polling",
"homekit": {

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import Coroutine
from datetime import datetime
from typing import Any
from regenmaschine.controller import Controller
from regenmaschine.errors import RequestError
@ -165,7 +166,8 @@ async def async_setup_entry(
]
zones_coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][DATA_ZONES]
entities = []
entities: list[RainMachineProgram | RainMachineZone] = []
for uid, program in programs_coordinator.data.items():
entities.append(
RainMachineProgram(
@ -241,57 +243,57 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity):
async_update_programs_and_zones(self.hass, self._entry)
)
async def async_disable_program(self, *, program_id):
async def async_disable_program(self, *, program_id: int) -> None:
"""Disable a program."""
await self._controller.programs.disable(program_id)
await async_update_programs_and_zones(self.hass, self._entry)
async def async_disable_zone(self, *, zone_id):
async def async_disable_zone(self, *, zone_id: int) -> None:
"""Disable a zone."""
await self._controller.zones.disable(zone_id)
await async_update_programs_and_zones(self.hass, self._entry)
async def async_enable_program(self, *, program_id):
async def async_enable_program(self, *, program_id: int) -> None:
"""Enable a program."""
await self._controller.programs.enable(program_id)
await async_update_programs_and_zones(self.hass, self._entry)
async def async_enable_zone(self, *, zone_id):
async def async_enable_zone(self, *, zone_id: int) -> None:
"""Enable a zone."""
await self._controller.zones.enable(zone_id)
await async_update_programs_and_zones(self.hass, self._entry)
async def async_pause_watering(self, *, seconds):
async def async_pause_watering(self, *, seconds: int) -> None:
"""Pause watering for a set number of seconds."""
await self._controller.watering.pause_all(seconds)
await async_update_programs_and_zones(self.hass, self._entry)
async def async_start_program(self, *, program_id):
async def async_start_program(self, *, program_id: int) -> None:
"""Start a particular program."""
await self._controller.programs.start(program_id)
await async_update_programs_and_zones(self.hass, self._entry)
async def async_start_zone(self, *, zone_id, zone_run_time):
async def async_start_zone(self, *, zone_id: int, zone_run_time: int) -> None:
"""Start a particular zone for a certain amount of time."""
await self._controller.zones.start(zone_id, zone_run_time)
await async_update_programs_and_zones(self.hass, self._entry)
async def async_stop_all(self):
async def async_stop_all(self) -> None:
"""Stop all watering."""
await self._controller.watering.stop_all()
await async_update_programs_and_zones(self.hass, self._entry)
async def async_stop_program(self, *, program_id):
async def async_stop_program(self, *, program_id: int) -> None:
"""Stop a program."""
await self._controller.programs.stop(program_id)
await async_update_programs_and_zones(self.hass, self._entry)
async def async_stop_zone(self, *, zone_id):
async def async_stop_zone(self, *, zone_id: int) -> None:
"""Stop a zone."""
await self._controller.zones.stop(zone_id)
await async_update_programs_and_zones(self.hass, self._entry)
async def async_unpause_watering(self):
async def async_unpause_watering(self) -> None:
"""Unpause watering."""
await self._controller.watering.unpause_all()
await async_update_programs_and_zones(self.hass, self._entry)
@ -311,13 +313,13 @@ class RainMachineProgram(RainMachineSwitch):
"""Return a list of active zones associated with this program."""
return [z for z in self._data["wateringTimes"] if z["active"]]
async def async_turn_off(self, **kwargs) -> None:
async def async_turn_off(self, **kwargs: dict[str, Any]) -> None:
"""Turn the program off."""
await self._async_run_switch_coroutine(
self._controller.programs.stop(self._uid)
)
async def async_turn_on(self, **kwargs) -> None:
async def async_turn_on(self, **kwargs: dict[str, Any]) -> None:
"""Turn the program on."""
await self._async_run_switch_coroutine(
self._controller.programs.start(self._uid)
@ -330,13 +332,12 @@ class RainMachineProgram(RainMachineSwitch):
self._attr_is_on = bool(self._data["status"])
next_run: str | None = None
if self._data.get("nextRun") is not None:
next_run = datetime.strptime(
f"{self._data['nextRun']} {self._data['startTime']}",
"%Y-%m-%d %H:%M",
).isoformat()
else:
next_run = None
self._attr_extra_state_attributes.update(
{
@ -352,11 +353,11 @@ class RainMachineProgram(RainMachineSwitch):
class RainMachineZone(RainMachineSwitch):
"""A RainMachine zone."""
async def async_turn_off(self, **kwargs) -> None:
async def async_turn_off(self, **kwargs: dict[str, Any]) -> None:
"""Turn the zone off."""
await self._async_run_switch_coroutine(self._controller.zones.stop(self._uid))
async def async_turn_on(self, **kwargs) -> None:
async def async_turn_on(self, **kwargs: dict[str, Any]) -> None:
"""Turn the zone on."""
await self._async_run_switch_coroutine(
self._controller.zones.start(

View File

@ -825,6 +825,17 @@ no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.rainmachine.*]
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.recorder.purge]
check_untyped_defs = true
disallow_incomplete_defs = true
@ -1535,9 +1546,6 @@ ignore_errors = true
[mypy-homeassistant.components.rachio.*]
ignore_errors = true
[mypy-homeassistant.components.rainmachine.*]
ignore_errors = true
[mypy-homeassistant.components.recollect_waste.*]
ignore_errors = true

View File

@ -2010,7 +2010,7 @@ raincloudy==0.0.7
raspyrfm-client==1.2.8
# homeassistant.components.rainmachine
regenmaschine==3.0.0
regenmaschine==3.1.5
# homeassistant.components.python_script
restrictedpython==5.1

View File

@ -1107,7 +1107,7 @@ pyzerproc==0.4.8
rachiopy==1.0.3
# homeassistant.components.rainmachine
regenmaschine==3.0.0
regenmaschine==3.1.5
# homeassistant.components.python_script
restrictedpython==5.1

View File

@ -137,7 +137,6 @@ IGNORED_MODULES: Final[list[str]] = [
"homeassistant.components.profiler.*",
"homeassistant.components.proxmoxve.*",
"homeassistant.components.rachio.*",
"homeassistant.components.rainmachine.*",
"homeassistant.components.recollect_waste.*",
"homeassistant.components.reddit.*",
"homeassistant.components.ring.*",