mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 09:17:10 +00:00
Use the new hybrid Hydrawise client (#136522)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
04d1d80917
commit
b73203fdf6
@ -1,9 +1,9 @@
|
||||
"""Support for Hydrawise cloud."""
|
||||
|
||||
from pydrawise import auth, client
|
||||
from pydrawise import auth, hybrid
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
|
||||
@ -21,16 +21,21 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.VALVE,
|
||||
]
|
||||
|
||||
_REQUIRED_AUTH_KEYS = (CONF_USERNAME, CONF_PASSWORD, CONF_API_KEY)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Set up Hydrawise from a config entry."""
|
||||
if CONF_USERNAME not in config_entry.data or CONF_PASSWORD not in config_entry.data:
|
||||
# The GraphQL API requires username and password to authenticate. If either is
|
||||
# missing, reauth is required.
|
||||
if any(k not in config_entry.data for k in _REQUIRED_AUTH_KEYS):
|
||||
# If we are missing any required authentication keys, trigger a reauth flow.
|
||||
raise ConfigEntryAuthFailed
|
||||
|
||||
hydrawise = client.Hydrawise(
|
||||
auth.Auth(config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD]),
|
||||
hydrawise = hybrid.HybridClient(
|
||||
auth.HybridAuth(
|
||||
config_entry.data[CONF_USERNAME],
|
||||
config_entry.data[CONF_PASSWORD],
|
||||
config_entry.data[CONF_API_KEY],
|
||||
),
|
||||
app_id=APP_ID,
|
||||
)
|
||||
|
||||
|
@ -6,25 +6,32 @@ from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError
|
||||
from pydrawise import auth as pydrawise_auth, client
|
||||
from pydrawise import auth as pydrawise_auth, hybrid
|
||||
from pydrawise.exceptions import NotAuthorizedError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
from .const import APP_ID, DOMAIN, LOGGER
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
}
|
||||
)
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_PASSWORD): str, vol.Required(CONF_API_KEY): str}
|
||||
)
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
|
||||
|
||||
|
||||
class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Hydrawise."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@ -34,14 +41,19 @@ class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self._show_user_form({})
|
||||
username = user_input[CONF_USERNAME]
|
||||
password = user_input[CONF_PASSWORD]
|
||||
unique_id, errors = await _authenticate(username, password)
|
||||
api_key = user_input[CONF_API_KEY]
|
||||
unique_id, errors = await _authenticate(username, password, api_key)
|
||||
if errors:
|
||||
return self._show_user_form(errors)
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=username,
|
||||
data={CONF_USERNAME: username, CONF_PASSWORD: password},
|
||||
data={
|
||||
CONF_USERNAME: username,
|
||||
CONF_PASSWORD: password,
|
||||
CONF_API_KEY: api_key,
|
||||
},
|
||||
)
|
||||
|
||||
def _show_user_form(self, errors: dict[str, str]) -> ConfigFlowResult:
|
||||
@ -65,14 +77,20 @@ class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
username = reauth_entry.data[CONF_USERNAME]
|
||||
password = user_input[CONF_PASSWORD]
|
||||
user_id, errors = await _authenticate(username, password)
|
||||
api_key = user_input[CONF_API_KEY]
|
||||
user_id, errors = await _authenticate(username, password, api_key)
|
||||
if user_id is None:
|
||||
return self._show_reauth_form(errors)
|
||||
|
||||
await self.async_set_unique_id(user_id)
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_account")
|
||||
return self.async_update_reload_and_abort(
|
||||
reauth_entry, data={CONF_USERNAME: username, CONF_PASSWORD: password}
|
||||
reauth_entry,
|
||||
data={
|
||||
CONF_USERNAME: username,
|
||||
CONF_PASSWORD: password,
|
||||
CONF_API_KEY: api_key,
|
||||
},
|
||||
)
|
||||
|
||||
def _show_reauth_form(self, errors: dict[str, str]) -> ConfigFlowResult:
|
||||
@ -82,14 +100,14 @@ class HydrawiseConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
|
||||
async def _authenticate(
|
||||
username: str, password: str
|
||||
username: str, password: str, api_key: str
|
||||
) -> tuple[str | None, dict[str, str]]:
|
||||
"""Authenticate with the Hydrawise API."""
|
||||
unique_id = None
|
||||
errors: dict[str, str] = {}
|
||||
auth = pydrawise_auth.Auth(username, password)
|
||||
auth = pydrawise_auth.HybridAuth(username, password, api_key)
|
||||
try:
|
||||
await auth.token()
|
||||
await auth.check()
|
||||
except NotAuthorizedError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except TimeoutError:
|
||||
@ -99,7 +117,7 @@ async def _authenticate(
|
||||
return unique_id, errors
|
||||
|
||||
try:
|
||||
api = client.Hydrawise(auth, app_id=APP_ID)
|
||||
api = hybrid.HybridClient(auth, app_id=APP_ID)
|
||||
# Don't fetch zones because we don't need them yet.
|
||||
user = await api.get_user(fetch_zones=False)
|
||||
except TimeoutError:
|
||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from pydrawise import Hydrawise
|
||||
from pydrawise import HydrawiseBase
|
||||
from pydrawise.schema import Controller, ControllerWaterUseSummary, Sensor, User, Zone
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
@ -38,7 +38,7 @@ class HydrawiseUpdateCoordinators:
|
||||
class HydrawiseDataUpdateCoordinator(DataUpdateCoordinator[HydrawiseData]):
|
||||
"""Base class for Hydrawise Data Update Coordinators."""
|
||||
|
||||
api: Hydrawise
|
||||
api: HydrawiseBase
|
||||
|
||||
|
||||
class HydrawiseMainDataUpdateCoordinator(HydrawiseDataUpdateCoordinator):
|
||||
@ -49,7 +49,7 @@ class HydrawiseMainDataUpdateCoordinator(HydrawiseDataUpdateCoordinator):
|
||||
integration are updated in a timely manner.
|
||||
"""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, api: Hydrawise) -> None:
|
||||
def __init__(self, hass: HomeAssistant, api: HydrawiseBase) -> None:
|
||||
"""Initialize HydrawiseDataUpdateCoordinator."""
|
||||
super().__init__(hass, LOGGER, name=DOMAIN, update_interval=MAIN_SCAN_INTERVAL)
|
||||
self.api = api
|
||||
@ -82,7 +82,7 @@ class HydrawiseWaterUseDataUpdateCoordinator(HydrawiseDataUpdateCoordinator):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
api: Hydrawise,
|
||||
api: HydrawiseBase,
|
||||
main_coordinator: HydrawiseMainDataUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialize HydrawiseWaterUseDataUpdateCoordinator."""
|
||||
|
@ -6,14 +6,22 @@
|
||||
"description": "Please provide the username and password for your Hydrawise cloud account:",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "You can generate an API Key in the 'Account Details' section of the Hydrawise app"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "The Hydrawise integration needs to re-authenticate your account",
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "[%key:component::hydrawise::config::step::user::data_description::api_key%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -7,7 +7,7 @@ from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from pydrawise import Hydrawise, Zone
|
||||
from pydrawise import HydrawiseBase, Zone
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
SwitchDeviceClass,
|
||||
@ -28,8 +28,8 @@ from .entity import HydrawiseEntity
|
||||
class HydrawiseSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Describes Hydrawise binary sensor."""
|
||||
|
||||
turn_on_fn: Callable[[Hydrawise, Zone], Coroutine[Any, Any, None]]
|
||||
turn_off_fn: Callable[[Hydrawise, Zone], Coroutine[Any, Any, None]]
|
||||
turn_on_fn: Callable[[HydrawiseBase, Zone], Coroutine[Any, Any, None]]
|
||||
turn_off_fn: Callable[[HydrawiseBase, Zone], Coroutine[Any, Any, None]]
|
||||
value_fn: Callable[[Zone], bool]
|
||||
|
||||
|
||||
|
@ -63,7 +63,7 @@ def mock_pydrawise(
|
||||
controller_water_use_summary: ControllerWaterUseSummary,
|
||||
) -> Generator[AsyncMock]:
|
||||
"""Mock Hydrawise."""
|
||||
with patch("pydrawise.client.Hydrawise", autospec=True) as mock_pydrawise:
|
||||
with patch("pydrawise.hybrid.HybridClient", autospec=True) as mock_pydrawise:
|
||||
user.controllers = [controller]
|
||||
controller.sensors = sensors
|
||||
mock_pydrawise.return_value.get_user.return_value = user
|
||||
@ -76,8 +76,8 @@ def mock_pydrawise(
|
||||
|
||||
@pytest.fixture
|
||||
def mock_auth() -> Generator[AsyncMock]:
|
||||
"""Mock pydrawise Auth."""
|
||||
with patch("pydrawise.auth.Auth", autospec=True) as mock_auth:
|
||||
"""Mock pydrawise HybridAuth."""
|
||||
with patch("pydrawise.auth.HybridAuth", autospec=True) as mock_auth:
|
||||
yield mock_auth.return_value
|
||||
|
||||
|
||||
@ -215,6 +215,7 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
data={
|
||||
CONF_USERNAME: "asfd@asdf.com",
|
||||
CONF_PASSWORD: "__password__",
|
||||
CONF_API_KEY: "abc123",
|
||||
},
|
||||
unique_id="hydrawise-customerid",
|
||||
version=1,
|
||||
|
@ -9,7 +9,7 @@ import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.hydrawise.const import DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import CONF_API_KEY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
@ -35,7 +35,11 @@ async def test_form(
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"},
|
||||
{
|
||||
CONF_USERNAME: "asdf@asdf.com",
|
||||
CONF_PASSWORD: "__password__",
|
||||
CONF_API_KEY: "__api-key__",
|
||||
},
|
||||
)
|
||||
mock_pydrawise.get_user.return_value = user
|
||||
await hass.async_block_till_done()
|
||||
@ -45,9 +49,10 @@ async def test_form(
|
||||
assert result["data"] == {
|
||||
CONF_USERNAME: "asdf@asdf.com",
|
||||
CONF_PASSWORD: "__password__",
|
||||
CONF_API_KEY: "__api-key__",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
mock_auth.token.assert_awaited_once_with()
|
||||
mock_auth.check.assert_awaited_once_with()
|
||||
mock_pydrawise.get_user.assert_awaited_once_with(fetch_zones=False)
|
||||
|
||||
|
||||
@ -60,7 +65,11 @@ async def test_form_api_error(
|
||||
init_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
data = {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"}
|
||||
data = {
|
||||
CONF_USERNAME: "asdf@asdf.com",
|
||||
CONF_PASSWORD: "__password__",
|
||||
CONF_API_KEY: "__api-key__",
|
||||
}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
init_result["flow_id"], data
|
||||
)
|
||||
@ -77,11 +86,18 @@ async def test_form_auth_connect_timeout(
|
||||
hass: HomeAssistant, mock_auth: AsyncMock, mock_pydrawise: AsyncMock
|
||||
) -> None:
|
||||
"""Test we handle connection timeout errors."""
|
||||
mock_auth.token.side_effect = TimeoutError
|
||||
mock_auth.check.side_effect = TimeoutError
|
||||
init_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
DOMAIN,
|
||||
context={
|
||||
"source": config_entries.SOURCE_USER,
|
||||
},
|
||||
)
|
||||
data = {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"}
|
||||
data = {
|
||||
CONF_USERNAME: "asdf@asdf.com",
|
||||
CONF_PASSWORD: "__password__",
|
||||
CONF_API_KEY: "__api-key__",
|
||||
}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
init_result["flow_id"], data
|
||||
)
|
||||
@ -89,7 +105,7 @@ async def test_form_auth_connect_timeout(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "timeout_connect"}
|
||||
|
||||
mock_auth.token.reset_mock(side_effect=True)
|
||||
mock_auth.check.reset_mock(side_effect=True)
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], data)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
@ -102,7 +118,11 @@ async def test_form_client_connect_timeout(
|
||||
init_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
data = {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"}
|
||||
data = {
|
||||
CONF_USERNAME: "asdf@asdf.com",
|
||||
CONF_PASSWORD: "__password__",
|
||||
CONF_API_KEY: "__api-key__",
|
||||
}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
init_result["flow_id"], data
|
||||
)
|
||||
@ -120,19 +140,23 @@ async def test_form_not_authorized_error(
|
||||
hass: HomeAssistant, mock_auth: AsyncMock, mock_pydrawise: AsyncMock
|
||||
) -> None:
|
||||
"""Test we handle API errors."""
|
||||
mock_auth.token.side_effect = NotAuthorizedError
|
||||
mock_auth.check.side_effect = NotAuthorizedError
|
||||
|
||||
init_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
data = {CONF_USERNAME: "asdf@asdf.com", CONF_PASSWORD: "__password__"}
|
||||
data = {
|
||||
CONF_USERNAME: "asdf@asdf.com",
|
||||
CONF_PASSWORD: "__password__",
|
||||
CONF_API_KEY: "__api-key__",
|
||||
}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
init_result["flow_id"], data
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
mock_auth.token.reset_mock(side_effect=True)
|
||||
mock_auth.check.reset_mock(side_effect=True)
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], data)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
@ -150,6 +174,7 @@ async def test_reauth(
|
||||
data={
|
||||
CONF_USERNAME: "asdf@asdf.com",
|
||||
CONF_PASSWORD: "bad-password",
|
||||
CONF_API_KEY: "__api-key__",
|
||||
},
|
||||
unique_id="hydrawise-12345",
|
||||
)
|
||||
@ -165,7 +190,11 @@ async def test_reauth(
|
||||
|
||||
mock_pydrawise.get_user.return_value = user
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_PASSWORD: "__password__"}
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_PASSWORD: "__password__",
|
||||
CONF_API_KEY: "__api-key__",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@ -183,6 +212,7 @@ async def test_reauth_fails(
|
||||
data={
|
||||
CONF_USERNAME: "asdf@asdf.com",
|
||||
CONF_PASSWORD: "bad-password",
|
||||
CONF_API_KEY: "__api-key__",
|
||||
},
|
||||
unique_id="hydrawise-12345",
|
||||
)
|
||||
@ -191,18 +221,26 @@ async def test_reauth_fails(
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
mock_auth.token.side_effect = NotAuthorizedError
|
||||
mock_auth.check.side_effect = NotAuthorizedError
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_PASSWORD: "__password__"}
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_PASSWORD: "__password__",
|
||||
CONF_API_KEY: "__api-key__",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
mock_auth.token.reset_mock(side_effect=True)
|
||||
mock_auth.check.reset_mock(side_effect=True)
|
||||
mock_pydrawise.get_user.return_value = user
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_PASSWORD: "__password__"}
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_PASSWORD: "__password__",
|
||||
CONF_API_KEY: "__api-key__",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
|
Loading…
x
Reference in New Issue
Block a user