mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 09:17:53 +00:00
Add Growatt Server Config flow (#41303)
* Growatt Server Config flow * Use reference strings Co-authored-by: SNoof85 <snoof85@gmail.com> * Remove configuration.yaml import logic * Removed import test * Re-added PLATFORM_SCHEMA validation * Import yaml from old yaml configuration * Apply suggestions from code review Co-authored-by: Martin Hjelmare <marhje52@gmail.com> * Feedback * Use Executor for IO only * Fix imports * update requirements * Fix flake8 * Run every section of fetching devices in single executor * Config flow feedback * Clean up * Fix plan step * Fix config flow test * Remove duplicate test * Test import step * Test already configured entry * Clean up tests * Add asserts * Mock out entry setup * Add warning if set up via yaml Co-authored-by: SNoof85 <snoof85@gmail.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
parent
12342437e2
commit
85f758380a
@ -374,6 +374,7 @@ omit =
|
||||
homeassistant/components/greenwave/light.py
|
||||
homeassistant/components/group/notify.py
|
||||
homeassistant/components/growatt_server/sensor.py
|
||||
homeassistant/components/growatt_server/__init__.py
|
||||
homeassistant/components/gstreamer/media_player.py
|
||||
homeassistant/components/gtfs/sensor.py
|
||||
homeassistant/components/guardian/__init__.py
|
||||
|
@ -1 +1,19 @@
|
||||
"""The Growatt server PV inverter sensor integration."""
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import PLATFORMS
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: config_entries.ConfigEntry
|
||||
) -> bool:
|
||||
"""Load the saved entities."""
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
78
homeassistant/components/growatt_server/config_flow.py
Normal file
78
homeassistant/components/growatt_server/config_flow.py
Normal file
@ -0,0 +1,78 @@
|
||||
"""Config flow for growatt server integration."""
|
||||
import growattServer
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
|
||||
from .const import CONF_PLANT_ID, DOMAIN
|
||||
|
||||
|
||||
class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow class."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialise growatt server flow."""
|
||||
self.api = growattServer.GrowattApi()
|
||||
self.user_id = None
|
||||
self.data = {}
|
||||
|
||||
@callback
|
||||
def _async_show_user_form(self, errors=None):
|
||||
"""Show the form to the user."""
|
||||
data_schema = vol.Schema(
|
||||
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=data_schema, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the start of the config flow."""
|
||||
if not user_input:
|
||||
return self._async_show_user_form()
|
||||
|
||||
login_response = await self.hass.async_add_executor_job(
|
||||
self.api.login, user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
|
||||
)
|
||||
|
||||
if not login_response["success"] and login_response["errCode"] == "102":
|
||||
return self._async_show_user_form({"base": "invalid_auth"})
|
||||
self.user_id = login_response["userId"]
|
||||
|
||||
self.data = user_input
|
||||
return await self.async_step_plant()
|
||||
|
||||
async def async_step_plant(self, user_input=None):
|
||||
"""Handle adding a "plant" to Home Assistant."""
|
||||
plant_info = await self.hass.async_add_executor_job(
|
||||
self.api.plant_list, self.user_id
|
||||
)
|
||||
|
||||
if not plant_info["data"]:
|
||||
return self.async_abort(reason="no_plants")
|
||||
|
||||
plants = {plant["plantId"]: plant["plantName"] for plant in plant_info["data"]}
|
||||
|
||||
if user_input is None and len(plant_info["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 and len(plant_info["data"]) == 1:
|
||||
user_input = {CONF_PLANT_ID: plant_info["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)
|
||||
|
||||
async def async_step_import(self, import_data):
|
||||
"""Migrate old yaml config to config flow."""
|
||||
return await self.async_step_user(import_data)
|
10
homeassistant/components/growatt_server/const.py
Normal file
10
homeassistant/components/growatt_server/const.py
Normal file
@ -0,0 +1,10 @@
|
||||
"""Define constants for the Growatt Server component."""
|
||||
CONF_PLANT_ID = "plant_id"
|
||||
|
||||
DEFAULT_PLANT_ID = "0"
|
||||
|
||||
DEFAULT_NAME = "Growatt"
|
||||
|
||||
DOMAIN = "growatt_server"
|
||||
|
||||
PLATFORMS = ["sensor"]
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"domain": "growatt_server",
|
||||
"name": "Growatt",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/growatt_server/",
|
||||
"requirements": ["growattServer==1.0.0"],
|
||||
"codeowners": ["@indykoning", "@muppet3000"],
|
||||
|
@ -8,6 +8,7 @@ import growattServer
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
@ -32,11 +33,10 @@ from homeassistant.const import (
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.util import Throttle, dt
|
||||
|
||||
from .const import CONF_PLANT_ID, DEFAULT_NAME, DEFAULT_PLANT_ID, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_PLANT_ID = "plant_id"
|
||||
DEFAULT_PLANT_ID = "0"
|
||||
DEFAULT_NAME = "Growatt"
|
||||
SCAN_INTERVAL = datetime.timedelta(minutes=1)
|
||||
|
||||
# Sensor type order is: Sensor name, Unit of measurement, api data name, additional options
|
||||
@ -558,17 +558,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Growatt sensor."""
|
||||
username = config[CONF_USERNAME]
|
||||
password = config[CONF_PASSWORD]
|
||||
plant_id = config[CONF_PLANT_ID]
|
||||
name = config[CONF_NAME]
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up growatt server from yaml."""
|
||||
if not hass.config_entries.async_entries(DOMAIN):
|
||||
_LOGGER.warning(
|
||||
"Loading Growatt via platform setup is deprecated."
|
||||
"Please remove it from your configuration"
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||
)
|
||||
)
|
||||
|
||||
api = growattServer.GrowattApi()
|
||||
|
||||
def get_device_list(api, config):
|
||||
"""Retrieve the device list for the selected plant."""
|
||||
plant_id = config[CONF_PLANT_ID]
|
||||
|
||||
# Log in to api and fetch first plant if no plant id is defined.
|
||||
login_response = api.login(username, password)
|
||||
login_response = api.login(config[CONF_USERNAME], config[CONF_PASSWORD])
|
||||
if not login_response["success"] and login_response["errCode"] == "102":
|
||||
_LOGGER.error("Username or Password may be incorrect!")
|
||||
return
|
||||
@ -579,6 +588,20 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
|
||||
# Get a list of devices for specified plant to add sensors for.
|
||||
devices = api.device_list(plant_id)
|
||||
return [devices, plant_id]
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up the Growatt sensor."""
|
||||
config = config_entry.data
|
||||
username = config[CONF_USERNAME]
|
||||
password = config[CONF_PASSWORD]
|
||||
name = config[CONF_NAME]
|
||||
|
||||
api = growattServer.GrowattApi()
|
||||
|
||||
devices, plant_id = await hass.async_add_executor_job(get_device_list, api, config)
|
||||
|
||||
entities = []
|
||||
probe = GrowattData(api, username, password, plant_id, "total")
|
||||
for sensor in TOTAL_SENSOR_TYPES:
|
||||
@ -616,7 +639,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
)
|
||||
)
|
||||
|
||||
add_entities(entities, True)
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class GrowattInverter(SensorEntity):
|
||||
|
27
homeassistant/components/growatt_server/strings.json
Normal file
27
homeassistant/components/growatt_server/strings.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_plants": "No plants have been found on this account"
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
},
|
||||
"step": {
|
||||
"plant": {
|
||||
"data": {
|
||||
"plant_id": "Plant"
|
||||
},
|
||||
"title": "Select your plant"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"password": "[%key:common::config_flow::data::name%]",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"title": "Enter your Growatt information"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Growatt Server"
|
||||
}
|
27
homeassistant/components/growatt_server/translations/en.json
Normal file
27
homeassistant/components/growatt_server/translations/en.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_plants": "No plants have been found on this account"
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Invalid authentication"
|
||||
},
|
||||
"step": {
|
||||
"plant": {
|
||||
"data": {
|
||||
"plant_id": "Plant"
|
||||
},
|
||||
"title": "Select your plant"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"password": "Password",
|
||||
"username": "Username"
|
||||
},
|
||||
"title": "Enter your Growatt information"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Growatt Server"
|
||||
}
|
@ -92,6 +92,7 @@ FLOWS = [
|
||||
"google_travel_time",
|
||||
"gpslogger",
|
||||
"gree",
|
||||
"growatt_server",
|
||||
"guardian",
|
||||
"habitica",
|
||||
"hangouts",
|
||||
|
@ -386,6 +386,9 @@ googlemaps==2.5.1
|
||||
# homeassistant.components.gree
|
||||
greeclimate==0.11.4
|
||||
|
||||
# homeassistant.components.growatt_server
|
||||
growattServer==1.0.0
|
||||
|
||||
# homeassistant.components.profiler
|
||||
guppy3==3.1.0
|
||||
|
||||
|
1
tests/components/growatt_server/__init__.py
Normal file
1
tests/components/growatt_server/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the growatt_server component."""
|
188
tests/components/growatt_server/test_config_flow.py
Normal file
188
tests/components/growatt_server/test_config_flow.py
Normal file
@ -0,0 +1,188 @@
|
||||
"""Tests for the Growatt server config flow."""
|
||||
from copy import deepcopy
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components.growatt_server.const import CONF_PLANT_ID, DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
FIXTURE_USER_INPUT = {CONF_USERNAME: "username", CONF_PASSWORD: "password"}
|
||||
|
||||
GROWATT_PLANT_LIST_RESPONSE = {
|
||||
"data": [
|
||||
{
|
||||
"plantMoneyText": "474.9 (€)",
|
||||
"plantName": "Plant name",
|
||||
"plantId": "123456",
|
||||
"isHaveStorage": "false",
|
||||
"todayEnergy": "2.6 kWh",
|
||||
"totalEnergy": "2.37 MWh",
|
||||
"currentPower": "628.8 W",
|
||||
}
|
||||
],
|
||||
"totalData": {
|
||||
"currentPowerSum": "628.8 W",
|
||||
"CO2Sum": "2.37 KT",
|
||||
"isHaveStorage": "false",
|
||||
"eTotalMoneyText": "474.9 (€)",
|
||||
"todayEnergySum": "2.6 kWh",
|
||||
"totalEnergySum": "2.37 MWh",
|
||||
},
|
||||
"success": True,
|
||||
}
|
||||
GROWATT_LOGIN_RESPONSE = {"userId": 123456, "userLevel": 1, "success": True}
|
||||
|
||||
|
||||
async def test_show_authenticate_form(hass):
|
||||
"""Test that the setup form is served."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
|
||||
async def test_incorrect_username(hass):
|
||||
"""Test that it shows the appropriate error when an incorrect username is entered."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"growattServer.GrowattApi.login",
|
||||
return_value={"errCode": "102", "success": False},
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], FIXTURE_USER_INPUT
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_no_plants_on_account(hass):
|
||||
"""Test registering an integration and finishing flow with an entered plant_id."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
user_input = FIXTURE_USER_INPUT.copy()
|
||||
plant_list = deepcopy(GROWATT_PLANT_LIST_RESPONSE)
|
||||
plant_list["data"] = []
|
||||
|
||||
with patch(
|
||||
"growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE
|
||||
), patch("growattServer.GrowattApi.plant_list", return_value=plant_list):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "no_plants"
|
||||
|
||||
|
||||
async def test_multiple_plant_ids(hass):
|
||||
"""Test registering an integration and finishing flow with an entered plant_id."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
user_input = FIXTURE_USER_INPUT.copy()
|
||||
plant_list = deepcopy(GROWATT_PLANT_LIST_RESPONSE)
|
||||
plant_list["data"].append(plant_list["data"][0])
|
||||
|
||||
with patch(
|
||||
"growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE
|
||||
), patch("growattServer.GrowattApi.plant_list", return_value=plant_list), patch(
|
||||
"homeassistant.components.growatt_server.async_setup_entry", return_value=True
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "plant"
|
||||
|
||||
user_input = {CONF_PLANT_ID: "123456"}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME]
|
||||
assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD]
|
||||
assert result["data"][CONF_PLANT_ID] == "123456"
|
||||
|
||||
|
||||
async def test_one_plant_on_account(hass):
|
||||
"""Test registering an integration and finishing flow with an entered plant_id."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
user_input = FIXTURE_USER_INPUT.copy()
|
||||
|
||||
with patch(
|
||||
"growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE
|
||||
), patch(
|
||||
"growattServer.GrowattApi.plant_list",
|
||||
return_value=GROWATT_PLANT_LIST_RESPONSE,
|
||||
), patch(
|
||||
"homeassistant.components.growatt_server.async_setup_entry", return_value=True
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME]
|
||||
assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD]
|
||||
assert result["data"][CONF_PLANT_ID] == "123456"
|
||||
|
||||
|
||||
async def test_import_one_plant(hass):
|
||||
"""Test import step with a single plant."""
|
||||
import_data = FIXTURE_USER_INPUT.copy()
|
||||
|
||||
with patch(
|
||||
"growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE
|
||||
), patch(
|
||||
"growattServer.GrowattApi.plant_list",
|
||||
return_value=GROWATT_PLANT_LIST_RESPONSE,
|
||||
), patch(
|
||||
"homeassistant.components.growatt_server.async_setup_entry", return_value=True
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data=import_data,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME]
|
||||
assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD]
|
||||
assert result["data"][CONF_PLANT_ID] == "123456"
|
||||
|
||||
|
||||
async def test_existing_plant_configured(hass):
|
||||
"""Test entering an existing plant_id."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, unique_id="123456")
|
||||
entry.add_to_hass(hass)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
user_input = FIXTURE_USER_INPUT.copy()
|
||||
|
||||
with patch(
|
||||
"growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE
|
||||
), patch(
|
||||
"growattServer.GrowattApi.plant_list",
|
||||
return_value=GROWATT_PLANT_LIST_RESPONSE,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "already_configured"
|
Loading…
x
Reference in New Issue
Block a user