mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Add config flow to Hydrawise (#95589)
* Add config flow to Hydrawise * Raise an issue when a YAML config is detected * Add a test for YAML import * Add missing __init__.py * Update CODEOWNERS * Update requirements_test_all.txt * Add config flow data to strings.json * Hande scan_interval not being in YAML on import * Fix requirements * Update deprecation dates * Update requirements_test_all.txt * Changes from review * Update homeassistant/components/hydrawise/__init__.py Co-authored-by: G Johansson <goran.johansson@shiftit.se> * Add already_configured to strings.json * Add back setup_platform functions * Apply suggestions from code review Co-authored-by: G Johansson <goran.johansson@shiftit.se> * Add back setup_platform * Update requirements_test_all.txt * Run black on hydrawise/*.py * Add missing import of HOMEASSISTANT_DOMAIN * Use more specific errors in config flow * Add additional tests * Update config flow to use pydrawise.legacy * Re-work YAML deprecation issues * Revert some changes to binary_sensor, as requested in review * Changes requested during review * Apply suggestions from code review Co-authored-by: G Johansson <goran.johansson@shiftit.se> * Remove unused STE_USER_DATA_SCHEMA Co-authored-by: G Johansson <goran.johansson@shiftit.se> * Update comment in setup_platform * Re-work the config flow again * Apply suggestions from code review Co-authored-by: G Johansson <goran.johansson@shiftit.se> * Update tests * Add back the _default_watering_timer attribute * Bump deprecation dates * Update requirements_test_all.txt * Update CODEOWNERS --------- Co-authored-by: G Johansson <goran.johansson@shiftit.se> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
28dc17c0b3
commit
f8a8fe760d
@ -537,7 +537,12 @@ omit =
|
||||
homeassistant/components/hvv_departures/__init__.py
|
||||
homeassistant/components/hvv_departures/binary_sensor.py
|
||||
homeassistant/components/hvv_departures/sensor.py
|
||||
homeassistant/components/hydrawise/*
|
||||
homeassistant/components/hydrawise/__init__.py
|
||||
homeassistant/components/hydrawise/binary_sensor.py
|
||||
homeassistant/components/hydrawise/const.py
|
||||
homeassistant/components/hydrawise/coordinator.py
|
||||
homeassistant/components/hydrawise/sensor.py
|
||||
homeassistant/components/hydrawise/switch.py
|
||||
homeassistant/components/ialarm/alarm_control_panel.py
|
||||
homeassistant/components/iammeter/sensor.py
|
||||
homeassistant/components/iaqualink/binary_sensor.py
|
||||
|
@ -562,6 +562,7 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/hvv_departures/ @vigonotion
|
||||
/tests/components/hvv_departures/ @vigonotion
|
||||
/homeassistant/components/hydrawise/ @dknowles2 @ptcryan
|
||||
/tests/components/hydrawise/ @dknowles2 @ptcryan
|
||||
/homeassistant/components/hyperion/ @dermotduffy
|
||||
/tests/components/hyperion/ @dermotduffy
|
||||
/homeassistant/components/ialarm/ @RyuzakiKK
|
||||
|
@ -5,13 +5,19 @@ from pydrawise.legacy import LegacyHydrawise
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_SCAN_INTERVAL
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
CONF_API_KEY,
|
||||
CONF_SCAN_INTERVAL,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, LOGGER, NOTIFICATION_ID, NOTIFICATION_TITLE, SCAN_INTERVAL
|
||||
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
|
||||
from .coordinator import HydrawiseDataUpdateCoordinator
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
@ -26,37 +32,49 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Hunter Hydrawise component."""
|
||||
conf = config[DOMAIN]
|
||||
access_token = conf[CONF_ACCESS_TOKEN]
|
||||
scan_interval = conf.get(CONF_SCAN_INTERVAL)
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={CONF_API_KEY: config[DOMAIN][CONF_ACCESS_TOKEN]},
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up Hydrawise from a config entry."""
|
||||
access_token = config_entry.data[CONF_API_KEY]
|
||||
try:
|
||||
hydrawise = await hass.async_add_executor_job(LegacyHydrawise, access_token)
|
||||
except (ConnectTimeout, HTTPError) as ex:
|
||||
LOGGER.error("Unable to connect to Hydrawise cloud service: %s", str(ex))
|
||||
_show_failure_notification(hass, str(ex))
|
||||
return False
|
||||
raise ConfigEntryNotReady(
|
||||
f"Unable to connect to Hydrawise cloud service: {ex}"
|
||||
) from ex
|
||||
|
||||
if not hydrawise.current_controller:
|
||||
LOGGER.error("Failed to fetch Hydrawise data")
|
||||
_show_failure_notification(hass, "Failed to fetch Hydrawise data.")
|
||||
return False
|
||||
|
||||
hass.data[DOMAIN] = HydrawiseDataUpdateCoordinator(hass, hydrawise, scan_interval)
|
||||
hass.data.setdefault(DOMAIN, {})[
|
||||
config_entry.entry_id
|
||||
] = HydrawiseDataUpdateCoordinator(hass, hydrawise, SCAN_INTERVAL)
|
||||
if not hydrawise.controller_info or not hydrawise.controller_status:
|
||||
raise ConfigEntryNotReady("Hydrawise data not loaded")
|
||||
|
||||
# NOTE: We don't need to call async_config_entry_first_refresh() because
|
||||
# data is fetched when the Hydrawiser object is instantiated.
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
def _show_failure_notification(hass: HomeAssistant, error: str) -> None:
|
||||
persistent_notification.create(
|
||||
hass,
|
||||
f"Error: {error}<br />You will need to restart hass after fixing.",
|
||||
title=NOTIFICATION_TITLE,
|
||||
notification_id=NOTIFICATION_ID,
|
||||
)
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
|
@ -10,6 +10,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -38,6 +39,8 @@ BINARY_SENSOR_KEYS: list[str] = [
|
||||
desc.key for desc in (BINARY_SENSOR_STATUS, *BINARY_SENSOR_TYPES)
|
||||
]
|
||||
|
||||
# Deprecated since Home Assistant 2023.10.0
|
||||
# Can be removed completely in 2024.4.0
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=BINARY_SENSOR_KEYS): vol.All(
|
||||
@ -54,32 +57,39 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up a sensor for a Hydrawise device."""
|
||||
coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN]
|
||||
hydrawise: LegacyHydrawise = coordinator.api
|
||||
monitored_conditions = config[CONF_MONITORED_CONDITIONS]
|
||||
# We don't need to trigger import flow from here as it's triggered from `__init__.py`
|
||||
return
|
||||
|
||||
entities = []
|
||||
if BINARY_SENSOR_STATUS.key in monitored_conditions:
|
||||
entities.append(
|
||||
HydrawiseBinarySensor(
|
||||
data=hydrawise.current_controller,
|
||||
coordinator=coordinator,
|
||||
description=BINARY_SENSOR_STATUS,
|
||||
)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Hydrawise binary_sensor platform."""
|
||||
coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
hydrawise: LegacyHydrawise = coordinator.api
|
||||
|
||||
entities = [
|
||||
HydrawiseBinarySensor(
|
||||
data=hydrawise.current_controller,
|
||||
coordinator=coordinator,
|
||||
description=BINARY_SENSOR_STATUS,
|
||||
)
|
||||
]
|
||||
|
||||
# create a sensor for each zone
|
||||
for zone in hydrawise.relays:
|
||||
for description in BINARY_SENSOR_TYPES:
|
||||
if description.key not in monitored_conditions:
|
||||
continue
|
||||
entities.append(
|
||||
HydrawiseBinarySensor(
|
||||
data=zone, coordinator=coordinator, description=description
|
||||
)
|
||||
)
|
||||
|
||||
add_entities(entities, True)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class HydrawiseBinarySensor(HydrawiseEntity, BinarySensorEntity):
|
||||
|
111
homeassistant/components/hydrawise/config_flow.py
Normal file
111
homeassistant/components/hydrawise/config_flow.py
Normal file
@ -0,0 +1,111 @@
|
||||
"""Config flow for the Hydrawise integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from pydrawise import legacy
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
|
||||
from homeassistant.data_entry_flow import AbortFlow, FlowResult, FlowResultType
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Hydrawise."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def _create_entry(
|
||||
self, api_key: str, *, on_failure: Callable[[str], FlowResult]
|
||||
) -> FlowResult:
|
||||
"""Create the config entry."""
|
||||
try:
|
||||
api = await self.hass.async_add_executor_job(
|
||||
legacy.LegacyHydrawise, api_key
|
||||
)
|
||||
except ConnectTimeout:
|
||||
return on_failure("timeout_connect")
|
||||
except HTTPError as ex:
|
||||
LOGGER.error("Unable to connect to Hydrawise cloud service: %s", ex)
|
||||
return on_failure("cannot_connect")
|
||||
|
||||
if not api.status:
|
||||
return on_failure("unknown")
|
||||
|
||||
await self.async_set_unique_id(f"hydrawise-{api.customer_id}")
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(title="Hydrawise", data={CONF_API_KEY: api_key})
|
||||
|
||||
def _import_issue(self, error_type: str) -> FlowResult:
|
||||
"""Create an issue about a YAML import failure."""
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_import_issue_{error_type}",
|
||||
breaks_in_ha_version="2024.4.0",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.ERROR,
|
||||
translation_key="deprecated_yaml_import_issue",
|
||||
translation_placeholders={"error_type": error_type},
|
||||
)
|
||||
return self.async_abort(reason=error_type)
|
||||
|
||||
def _deprecated_yaml_issue(self) -> None:
|
||||
"""Create an issue about YAML deprecation."""
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
breaks_in_ha_version="2024.4.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Hydrawise",
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial setup."""
|
||||
if user_input is not None:
|
||||
api_key = user_input[CONF_API_KEY]
|
||||
return await self._create_entry(api_key, on_failure=self._show_form)
|
||||
return self._show_form()
|
||||
|
||||
def _show_form(self, error_type: str | None = None) -> FlowResult:
|
||||
errors = {}
|
||||
if error_type is not None:
|
||||
errors["base"] = error_type
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data: dict[str, Any]) -> FlowResult:
|
||||
"""Import data from YAML."""
|
||||
try:
|
||||
result = await self._create_entry(
|
||||
import_data.get(CONF_API_KEY, ""),
|
||||
on_failure=self._import_issue,
|
||||
)
|
||||
except AbortFlow:
|
||||
self._deprecated_yaml_issue()
|
||||
raise
|
||||
|
||||
if result["type"] == FlowResultType.CREATE_ENTRY:
|
||||
self._deprecated_yaml_issue()
|
||||
return result
|
@ -8,9 +8,6 @@ LOGGER = logging.getLogger(__package__)
|
||||
ALLOWED_WATERING_TIME = [5, 10, 15, 30, 45, 60]
|
||||
CONF_WATERING_TIME = "watering_minutes"
|
||||
|
||||
NOTIFICATION_ID = "hydrawise_notification"
|
||||
NOTIFICATION_TITLE = "Hydrawise Setup"
|
||||
|
||||
DOMAIN = "hydrawise"
|
||||
DEFAULT_WATERING_TIME = 15
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
"domain": "hydrawise",
|
||||
"name": "Hunter Hydrawise",
|
||||
"codeowners": ["@dknowles2", "@ptcryan"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pydrawise"],
|
||||
|
@ -1,7 +1,6 @@
|
||||
"""Support for Hydrawise sprinkler sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pydrawise.legacy import LegacyHydrawise
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@ -10,6 +9,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -37,6 +37,8 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
|
||||
SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
|
||||
|
||||
# Deprecated since Home Assistant 2023.10.0
|
||||
# Can be removed completely in 2024.4.0
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=SENSOR_KEYS): vol.All(
|
||||
@ -56,18 +58,25 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up a sensor for a Hydrawise device."""
|
||||
coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN]
|
||||
hydrawise: LegacyHydrawise = coordinator.api
|
||||
monitored_conditions = config[CONF_MONITORED_CONDITIONS]
|
||||
# We don't need to trigger import flow from here as it's triggered from `__init__.py`
|
||||
return
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Hydrawise sensor platform."""
|
||||
coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
entities = [
|
||||
HydrawiseSensor(data=zone, coordinator=coordinator, description=description)
|
||||
for zone in hydrawise.relays
|
||||
for zone in coordinator.api.relays
|
||||
for description in SENSOR_TYPES
|
||||
if description.key in monitored_conditions
|
||||
]
|
||||
|
||||
add_entities(entities, True)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class HydrawiseSensor(HydrawiseEntity, SensorEntity):
|
||||
|
25
homeassistant/components/hydrawise/strings.json
Normal file
25
homeassistant/components/hydrawise/strings.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue": {
|
||||
"title": "The Hydrawise YAML configuration import failed",
|
||||
"description": "Configuring Hydrawise using YAML is being removed but there was an {error_type} error importing your YAML configuration.\n\nEnsure connection to Hydrawise works and restart Home Assistant to try again or remove the Hydrawise YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydrawise.legacy import LegacyHydrawise
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
@ -12,6 +11,7 @@ from homeassistant.components.switch import (
|
||||
SwitchEntity,
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MONITORED_CONDITIONS
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@ -43,6 +43,8 @@ SWITCH_TYPES: tuple[SwitchEntityDescription, ...] = (
|
||||
|
||||
SWITCH_KEYS: list[str] = [desc.key for desc in SWITCH_TYPES]
|
||||
|
||||
# Deprecated since Home Assistant 2023.10.0
|
||||
# Can be removed completely in 2024.4.0
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS, default=SWITCH_KEYS): vol.All(
|
||||
@ -62,10 +64,20 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up a sensor for a Hydrawise device."""
|
||||
coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN]
|
||||
hydrawise: LegacyHydrawise = coordinator.api
|
||||
monitored_conditions: list[str] = config[CONF_MONITORED_CONDITIONS]
|
||||
default_watering_timer: int = config[CONF_WATERING_TIME]
|
||||
# We don't need to trigger import flow from here as it's triggered from `__init__.py`
|
||||
return
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Hydrawise switch platform."""
|
||||
coordinator: HydrawiseDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
default_watering_timer = DEFAULT_WATERING_TIME
|
||||
|
||||
entities = [
|
||||
HydrawiseSwitch(
|
||||
@ -74,12 +86,11 @@ def setup_platform(
|
||||
description=description,
|
||||
default_watering_timer=default_watering_timer,
|
||||
)
|
||||
for zone in hydrawise.relays
|
||||
for zone in coordinator.api.relays
|
||||
for description in SWITCH_TYPES
|
||||
if description.key in monitored_conditions
|
||||
]
|
||||
|
||||
add_entities(entities, True)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class HydrawiseSwitch(HydrawiseEntity, SwitchEntity):
|
||||
|
@ -206,6 +206,7 @@ FLOWS = {
|
||||
"huisbaasje",
|
||||
"hunterdouglas_powerview",
|
||||
"hvv_departures",
|
||||
"hydrawise",
|
||||
"hyperion",
|
||||
"ialarm",
|
||||
"iaqualink",
|
||||
|
@ -2501,7 +2501,7 @@
|
||||
"hydrawise": {
|
||||
"name": "Hunter Hydrawise",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"hyperion": {
|
||||
|
@ -1244,6 +1244,9 @@ pydexcom==0.2.3
|
||||
# homeassistant.components.discovergy
|
||||
pydiscovergy==2.0.3
|
||||
|
||||
# homeassistant.components.hydrawise
|
||||
pydrawise==2023.8.0
|
||||
|
||||
# homeassistant.components.android_ip_webcam
|
||||
pydroid-ipcam==2.0.0
|
||||
|
||||
|
1
tests/components/hydrawise/__init__.py
Normal file
1
tests/components/hydrawise/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Hydrawise integration."""
|
15
tests/components/hydrawise/conftest.py
Normal file
15
tests/components/hydrawise/conftest.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""Common fixtures for the Hydrawise tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.hydrawise.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
213
tests/components/hydrawise/test_config_flow.py
Normal file
213
tests/components/hydrawise/test_config_flow.py
Normal file
@ -0,0 +1,213 @@
|
||||
"""Test the Hydrawise config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.hydrawise.const import DOMAIN
|
||||
from homeassistant.const import CONF_API_KEY, CONF_SCAN_INTERVAL
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
import homeassistant.helpers.issue_registry as ir
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
||||
|
||||
|
||||
@patch("pydrawise.legacy.LegacyHydrawise")
|
||||
async def test_form(
|
||||
mock_api: MagicMock, hass: HomeAssistant, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {"api_key": "abc123"}
|
||||
)
|
||||
mock_api.return_value.customer_id = 12345
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "Hydrawise"
|
||||
assert result2["data"] == {"api_key": "abc123"}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@patch("pydrawise.legacy.LegacyHydrawise")
|
||||
async def test_form_api_error(mock_api: MagicMock, hass: HomeAssistant) -> None:
|
||||
"""Test we handle API errors."""
|
||||
mock_api.side_effect = HTTPError
|
||||
init_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
data = {"api_key": "abc123"}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
init_result["flow_id"], data
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
mock_api.side_effect = None
|
||||
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data)
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
@patch("pydrawise.legacy.LegacyHydrawise")
|
||||
async def test_form_connect_timeout(mock_api: MagicMock, hass: HomeAssistant) -> None:
|
||||
"""Test we handle API errors."""
|
||||
mock_api.side_effect = ConnectTimeout
|
||||
init_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
data = {"api_key": "abc123"}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
init_result["flow_id"], data
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "timeout_connect"}
|
||||
|
||||
mock_api.side_effect = None
|
||||
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], data)
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
@patch("pydrawise.legacy.LegacyHydrawise")
|
||||
async def test_flow_import_success(mock_api: MagicMock, hass: HomeAssistant) -> None:
|
||||
"""Test that we can import a YAML config."""
|
||||
mock_api.return_value.status = "All good!"
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_API_KEY: "__api_key__",
|
||||
CONF_SCAN_INTERVAL: 120,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "Hydrawise"
|
||||
assert result["data"] == {
|
||||
CONF_API_KEY: "__api_key__",
|
||||
}
|
||||
|
||||
issue_registry = ir.async_get(hass)
|
||||
issue = issue_registry.async_get_issue(
|
||||
HOMEASSISTANT_DOMAIN, "deprecated_yaml_hydrawise"
|
||||
)
|
||||
assert issue.translation_key == "deprecated_yaml"
|
||||
|
||||
|
||||
@patch("pydrawise.legacy.LegacyHydrawise", side_effect=HTTPError)
|
||||
async def test_flow_import_api_error(mock_api: MagicMock, hass: HomeAssistant) -> None:
|
||||
"""Test that we handle API errors on YAML import."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_API_KEY: "__api_key__",
|
||||
CONF_SCAN_INTERVAL: 120,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
issue_registry = ir.async_get(hass)
|
||||
issue = issue_registry.async_get_issue(
|
||||
DOMAIN, "deprecated_yaml_import_issue_cannot_connect"
|
||||
)
|
||||
assert issue.translation_key == "deprecated_yaml_import_issue"
|
||||
|
||||
|
||||
@patch("pydrawise.legacy.LegacyHydrawise", side_effect=ConnectTimeout)
|
||||
async def test_flow_import_connect_timeout(
|
||||
mock_api: MagicMock, hass: HomeAssistant
|
||||
) -> None:
|
||||
"""Test that we handle connection timeouts on YAML import."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_API_KEY: "__api_key__",
|
||||
CONF_SCAN_INTERVAL: 120,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "timeout_connect"
|
||||
|
||||
issue_registry = ir.async_get(hass)
|
||||
issue = issue_registry.async_get_issue(
|
||||
DOMAIN, "deprecated_yaml_import_issue_timeout_connect"
|
||||
)
|
||||
assert issue.translation_key == "deprecated_yaml_import_issue"
|
||||
|
||||
|
||||
@patch("pydrawise.legacy.LegacyHydrawise")
|
||||
async def test_flow_import_no_status(mock_api: MagicMock, hass: HomeAssistant) -> None:
|
||||
"""Test we handle a lack of API status on YAML import."""
|
||||
mock_api.return_value.status = None
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_API_KEY: "__api_key__",
|
||||
CONF_SCAN_INTERVAL: 120,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "unknown"
|
||||
|
||||
issue_registry = ir.async_get(hass)
|
||||
issue = issue_registry.async_get_issue(
|
||||
DOMAIN, "deprecated_yaml_import_issue_unknown"
|
||||
)
|
||||
assert issue.translation_key == "deprecated_yaml_import_issue"
|
||||
|
||||
|
||||
@patch("pydrawise.legacy.LegacyHydrawise")
|
||||
async def test_flow_import_already_imported(
|
||||
mock_api: MagicMock, hass: HomeAssistant
|
||||
) -> None:
|
||||
"""Test that we can handle a YAML config already imported."""
|
||||
mock_config_entry = MockConfigEntry(
|
||||
title="Hydrawise",
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_API_KEY: "__api_key__",
|
||||
},
|
||||
unique_id="hydrawise-CUSTOMER_ID",
|
||||
)
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
mock_api.return_value.customer_id = "CUSTOMER_ID"
|
||||
mock_api.return_value.status = "All good!"
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_API_KEY: "__api_key__",
|
||||
CONF_SCAN_INTERVAL: 120,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result.get("reason") == "already_configured"
|
||||
|
||||
issue_registry = ir.async_get(hass)
|
||||
issue = issue_registry.async_get_issue(
|
||||
HOMEASSISTANT_DOMAIN, "deprecated_yaml_hydrawise"
|
||||
)
|
||||
assert issue.translation_key == "deprecated_yaml"
|
Loading…
x
Reference in New Issue
Block a user