mirror of
https://github.com/home-assistant/core.git
synced 2025-11-09 19:09:32 +00:00
232 lines
8.1 KiB
Python
232 lines
8.1 KiB
Python
"""Config flow for growatt server integration."""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
import growattServer
|
|
import requests
|
|
import voluptuous as vol
|
|
|
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
|
from homeassistant.const import (
|
|
CONF_NAME,
|
|
CONF_PASSWORD,
|
|
CONF_TOKEN,
|
|
CONF_URL,
|
|
CONF_USERNAME,
|
|
)
|
|
from homeassistant.core import callback
|
|
|
|
from .const import (
|
|
ABORT_NO_PLANTS,
|
|
AUTH_API_TOKEN,
|
|
AUTH_PASSWORD,
|
|
CONF_AUTH_TYPE,
|
|
CONF_PLANT_ID,
|
|
DEFAULT_URL,
|
|
DOMAIN,
|
|
ERROR_CANNOT_CONNECT,
|
|
ERROR_INVALID_AUTH,
|
|
LOGIN_INVALID_AUTH_CODE,
|
|
SERVER_URLS,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class GrowattServerConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
"""Config flow class."""
|
|
|
|
VERSION = 1
|
|
|
|
api: growattServer.GrowattApi
|
|
|
|
def __init__(self) -> None:
|
|
"""Initialise growatt server flow."""
|
|
self.user_id: str | None = None
|
|
self.data: dict[str, Any] = {}
|
|
self.auth_type: str | None = None
|
|
self.plants: list[dict[str, Any]] = []
|
|
|
|
async def async_step_user(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Handle the start of the config flow."""
|
|
return self.async_show_menu(
|
|
step_id="user",
|
|
menu_options=["password_auth", "token_auth"],
|
|
)
|
|
|
|
async def async_step_password_auth(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Handle username/password authentication."""
|
|
if user_input is None:
|
|
return self._async_show_password_form()
|
|
|
|
self.auth_type = AUTH_PASSWORD
|
|
|
|
# Traditional username/password authentication
|
|
self.api = growattServer.GrowattApi(
|
|
add_random_user_id=True, agent_identifier=user_input[CONF_USERNAME]
|
|
)
|
|
self.api.server_url = user_input[CONF_URL]
|
|
|
|
try:
|
|
login_response = await self.hass.async_add_executor_job(
|
|
self.api.login, user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
|
|
)
|
|
except requests.exceptions.RequestException as ex:
|
|
_LOGGER.error("Network error during Growatt API login: %s", ex)
|
|
return self._async_show_password_form({"base": ERROR_CANNOT_CONNECT})
|
|
except (ValueError, KeyError, TypeError, AttributeError) as ex:
|
|
_LOGGER.error("Invalid response format during login: %s", ex)
|
|
return self._async_show_password_form({"base": ERROR_CANNOT_CONNECT})
|
|
|
|
if (
|
|
not login_response["success"]
|
|
and login_response["msg"] == LOGIN_INVALID_AUTH_CODE
|
|
):
|
|
return self._async_show_password_form({"base": ERROR_INVALID_AUTH})
|
|
|
|
self.user_id = login_response["user"]["id"]
|
|
self.data = user_input
|
|
self.data[CONF_AUTH_TYPE] = self.auth_type
|
|
return await self.async_step_plant()
|
|
|
|
async def async_step_token_auth(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Handle API token authentication."""
|
|
if user_input is None:
|
|
return self._async_show_token_form()
|
|
|
|
self.auth_type = AUTH_API_TOKEN
|
|
|
|
# Using token authentication
|
|
token = user_input[CONF_TOKEN]
|
|
self.api = growattServer.OpenApiV1(token=token)
|
|
|
|
# Verify token by fetching plant list
|
|
try:
|
|
plant_response = await self.hass.async_add_executor_job(self.api.plant_list)
|
|
self.plants = plant_response.get("plants", [])
|
|
except requests.exceptions.RequestException as ex:
|
|
_LOGGER.error("Network error during Growatt V1 API plant list: %s", ex)
|
|
return self._async_show_token_form({"base": ERROR_CANNOT_CONNECT})
|
|
except growattServer.GrowattV1ApiError as e:
|
|
_LOGGER.error(
|
|
"Growatt V1 API error: %s (Code: %s)",
|
|
e.error_msg or str(e),
|
|
getattr(e, "error_code", None),
|
|
)
|
|
return self._async_show_token_form({"base": ERROR_INVALID_AUTH})
|
|
except (ValueError, KeyError, TypeError, AttributeError) as ex:
|
|
_LOGGER.error(
|
|
"Invalid response format during Growatt V1 API plant list: %s", ex
|
|
)
|
|
return self._async_show_token_form({"base": ERROR_CANNOT_CONNECT})
|
|
self.data = user_input
|
|
self.data[CONF_AUTH_TYPE] = self.auth_type
|
|
return await self.async_step_plant()
|
|
|
|
@callback
|
|
def _async_show_password_form(
|
|
self, errors: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Show the username/password form to the user."""
|
|
data_schema = vol.Schema(
|
|
{
|
|
vol.Required(CONF_USERNAME): str,
|
|
vol.Required(CONF_PASSWORD): str,
|
|
vol.Required(CONF_URL, default=DEFAULT_URL): vol.In(SERVER_URLS),
|
|
}
|
|
)
|
|
|
|
return self.async_show_form(
|
|
step_id="password_auth", data_schema=data_schema, errors=errors
|
|
)
|
|
|
|
@callback
|
|
def _async_show_token_form(
|
|
self, errors: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Show the API token form to the user."""
|
|
data_schema = vol.Schema(
|
|
{
|
|
vol.Required(CONF_TOKEN): str,
|
|
}
|
|
)
|
|
|
|
return self.async_show_form(
|
|
step_id="token_auth",
|
|
data_schema=data_schema,
|
|
errors=errors,
|
|
)
|
|
|
|
async def async_step_plant(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> ConfigFlowResult:
|
|
"""Handle adding a "plant" to Home Assistant."""
|
|
if self.auth_type == AUTH_API_TOKEN:
|
|
# Using V1 API with token
|
|
if not self.plants:
|
|
return self.async_abort(reason=ABORT_NO_PLANTS)
|
|
|
|
# Create dictionary of plant_id -> name
|
|
plant_dict = {
|
|
str(plant["plant_id"]): plant.get("name", "Unknown Plant")
|
|
for plant in self.plants
|
|
}
|
|
|
|
if user_input is None and len(plant_dict) > 1:
|
|
data_schema = vol.Schema(
|
|
{vol.Required(CONF_PLANT_ID): vol.In(plant_dict)}
|
|
)
|
|
return self.async_show_form(step_id="plant", data_schema=data_schema)
|
|
|
|
if user_input is None:
|
|
# Single plant => mark it as selected
|
|
user_input = {CONF_PLANT_ID: list(plant_dict.keys())[0]}
|
|
|
|
user_input[CONF_NAME] = plant_dict[user_input[CONF_PLANT_ID]]
|
|
|
|
else:
|
|
# Traditional API
|
|
try:
|
|
plant_info = await self.hass.async_add_executor_job(
|
|
self.api.plant_list, self.user_id
|
|
)
|
|
except requests.exceptions.RequestException as ex:
|
|
_LOGGER.error("Network error during Growatt API plant list: %s", ex)
|
|
return self.async_abort(reason=ERROR_CANNOT_CONNECT)
|
|
|
|
# Access plant_info["data"] - validate response structure
|
|
if not isinstance(plant_info, dict) or "data" not in plant_info:
|
|
_LOGGER.error(
|
|
"Invalid response format during plant list: missing 'data' key"
|
|
)
|
|
return self.async_abort(reason=ERROR_CANNOT_CONNECT)
|
|
|
|
plant_data = plant_info["data"]
|
|
|
|
if not plant_data:
|
|
return self.async_abort(reason=ABORT_NO_PLANTS)
|
|
|
|
plants = {plant["plantId"]: plant["plantName"] for plant in plant_data}
|
|
|
|
if user_input is None and len(plant_data) > 1:
|
|
data_schema = vol.Schema({vol.Required(CONF_PLANT_ID): vol.In(plants)})
|
|
return self.async_show_form(step_id="plant", data_schema=data_schema)
|
|
|
|
if user_input is None:
|
|
# single plant => mark it as selected
|
|
user_input = {CONF_PLANT_ID: plant_data[0]["plantId"]}
|
|
|
|
user_input[CONF_NAME] = plants[user_input[CONF_PLANT_ID]]
|
|
|
|
await self.async_set_unique_id(user_input[CONF_PLANT_ID])
|
|
self._abort_if_unique_id_configured()
|
|
self.data.update(user_input)
|
|
return self.async_create_entry(title=self.data[CONF_NAME], data=self.data)
|