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.persistent_notification.*
homeassistant.components.pi_hole.* homeassistant.components.pi_hole.*
homeassistant.components.proximity.* homeassistant.components.proximity.*
homeassistant.components.rainmachine.*
homeassistant.components.recorder.purge homeassistant.components.recorder.purge
homeassistant.components.recorder.repack homeassistant.components.recorder.repack
homeassistant.components.recorder.statistics homeassistant.components.recorder.statistics

View file

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

View file

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

View file

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

View file

@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import Coroutine from collections.abc import Coroutine
from datetime import datetime from datetime import datetime
from typing import Any
from regenmaschine.controller import Controller from regenmaschine.controller import Controller
from regenmaschine.errors import RequestError 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] 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(): for uid, program in programs_coordinator.data.items():
entities.append( entities.append(
RainMachineProgram( RainMachineProgram(
@ -241,57 +243,57 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity):
async_update_programs_and_zones(self.hass, self._entry) 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.""" """Disable a program."""
await self._controller.programs.disable(program_id) await self._controller.programs.disable(program_id)
await async_update_programs_and_zones(self.hass, self._entry) 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.""" """Disable a zone."""
await self._controller.zones.disable(zone_id) await self._controller.zones.disable(zone_id)
await async_update_programs_and_zones(self.hass, self._entry) 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.""" """Enable a program."""
await self._controller.programs.enable(program_id) await self._controller.programs.enable(program_id)
await async_update_programs_and_zones(self.hass, self._entry) 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.""" """Enable a zone."""
await self._controller.zones.enable(zone_id) await self._controller.zones.enable(zone_id)
await async_update_programs_and_zones(self.hass, self._entry) 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.""" """Pause watering for a set number of seconds."""
await self._controller.watering.pause_all(seconds) await self._controller.watering.pause_all(seconds)
await async_update_programs_and_zones(self.hass, self._entry) 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.""" """Start a particular program."""
await self._controller.programs.start(program_id) await self._controller.programs.start(program_id)
await async_update_programs_and_zones(self.hass, self._entry) 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.""" """Start a particular zone for a certain amount of time."""
await self._controller.zones.start(zone_id, zone_run_time) await self._controller.zones.start(zone_id, zone_run_time)
await async_update_programs_and_zones(self.hass, self._entry) 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.""" """Stop all watering."""
await self._controller.watering.stop_all() await self._controller.watering.stop_all()
await async_update_programs_and_zones(self.hass, self._entry) 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.""" """Stop a program."""
await self._controller.programs.stop(program_id) await self._controller.programs.stop(program_id)
await async_update_programs_and_zones(self.hass, self._entry) 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.""" """Stop a zone."""
await self._controller.zones.stop(zone_id) await self._controller.zones.stop(zone_id)
await async_update_programs_and_zones(self.hass, self._entry) 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.""" """Unpause watering."""
await self._controller.watering.unpause_all() await self._controller.watering.unpause_all()
await async_update_programs_and_zones(self.hass, self._entry) 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 a list of active zones associated with this program."""
return [z for z in self._data["wateringTimes"] if z["active"]] 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.""" """Turn the program off."""
await self._async_run_switch_coroutine( await self._async_run_switch_coroutine(
self._controller.programs.stop(self._uid) 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.""" """Turn the program on."""
await self._async_run_switch_coroutine( await self._async_run_switch_coroutine(
self._controller.programs.start(self._uid) self._controller.programs.start(self._uid)
@ -330,13 +332,12 @@ class RainMachineProgram(RainMachineSwitch):
self._attr_is_on = bool(self._data["status"]) self._attr_is_on = bool(self._data["status"])
next_run: str | None = None
if self._data.get("nextRun") is not None: if self._data.get("nextRun") is not None:
next_run = datetime.strptime( next_run = datetime.strptime(
f"{self._data['nextRun']} {self._data['startTime']}", f"{self._data['nextRun']} {self._data['startTime']}",
"%Y-%m-%d %H:%M", "%Y-%m-%d %H:%M",
).isoformat() ).isoformat()
else:
next_run = None
self._attr_extra_state_attributes.update( self._attr_extra_state_attributes.update(
{ {
@ -352,11 +353,11 @@ class RainMachineProgram(RainMachineSwitch):
class RainMachineZone(RainMachineSwitch): class RainMachineZone(RainMachineSwitch):
"""A RainMachine zone.""" """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.""" """Turn the zone off."""
await self._async_run_switch_coroutine(self._controller.zones.stop(self._uid)) 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.""" """Turn the zone on."""
await self._async_run_switch_coroutine( await self._async_run_switch_coroutine(
self._controller.zones.start( self._controller.zones.start(

View file

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

View file

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

View file

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

View file

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