diff --git a/.strict-typing b/.strict-typing index 82069f1548a..d3ff7fb42f0 100644 --- a/.strict-typing +++ b/.strict-typing @@ -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 diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 13540c092fa..8d3f9444f08 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -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() diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index 55ff68c5ea0..c392ad1f8ce 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -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) diff --git a/homeassistant/components/rainmachine/manifest.json b/homeassistant/components/rainmachine/manifest.json index b6021d02c39..03fedcf8c57 100644 --- a/homeassistant/components/rainmachine/manifest.json +++ b/homeassistant/components/rainmachine/manifest.json @@ -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": { diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 8338e4c8305..9554b22d783 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -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( diff --git a/mypy.ini b/mypy.ini index 072242f44e7..46a08049bfd 100644 --- a/mypy.ini +++ b/mypy.ini @@ -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 diff --git a/requirements_all.txt b/requirements_all.txt index d9c8162d26d..71e2da64aa2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6704cb73b5c..5f3042643ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 642dd47b732..f07575b61de 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -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.*",