Skip to content
Snippets Groups Projects
Unverified Commit 85f75838 authored by indykoning's avatar indykoning Committed by GitHub
Browse files

Add Growatt Server Config flow (#41303)


* Growatt Server Config flow

* Use reference strings

Co-authored-by: default avatarSNoof85 <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: default avatarMartin 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: default avatarSNoof85 <snoof85@gmail.com>
Co-authored-by: default avatarMartin Hjelmare <marhje52@gmail.com>
parent 12342437
No related branches found
No related tags found
No related merge requests found
Showing with 390 additions and 12 deletions
...@@ -374,6 +374,7 @@ omit = ...@@ -374,6 +374,7 @@ omit =
homeassistant/components/greenwave/light.py homeassistant/components/greenwave/light.py
homeassistant/components/group/notify.py homeassistant/components/group/notify.py
homeassistant/components/growatt_server/sensor.py homeassistant/components/growatt_server/sensor.py
homeassistant/components/growatt_server/__init__.py
homeassistant/components/gstreamer/media_player.py homeassistant/components/gstreamer/media_player.py
homeassistant/components/gtfs/sensor.py homeassistant/components/gtfs/sensor.py
homeassistant/components/guardian/__init__.py homeassistant/components/guardian/__init__.py
......
"""The Growatt server PV inverter sensor integration.""" """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)
"""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)
"""Define constants for the Growatt Server component."""
CONF_PLANT_ID = "plant_id"
DEFAULT_PLANT_ID = "0"
DEFAULT_NAME = "Growatt"
DOMAIN = "growatt_server"
PLATFORMS = ["sensor"]
{ {
"domain": "growatt_server", "domain": "growatt_server",
"name": "Growatt", "name": "Growatt",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/growatt_server/", "documentation": "https://www.home-assistant.io/integrations/growatt_server/",
"requirements": ["growattServer==1.0.0"], "requirements": ["growattServer==1.0.0"],
"codeowners": ["@indykoning", "@muppet3000"], "codeowners": ["@indykoning", "@muppet3000"],
......
...@@ -8,6 +8,7 @@ import growattServer ...@@ -8,6 +8,7 @@ import growattServer
import voluptuous as vol import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_PASSWORD, CONF_PASSWORD,
...@@ -32,11 +33,10 @@ from homeassistant.const import ( ...@@ -32,11 +33,10 @@ from homeassistant.const import (
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.util import Throttle, dt from homeassistant.util import Throttle, dt
from .const import CONF_PLANT_ID, DEFAULT_NAME, DEFAULT_PLANT_ID, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_PLANT_ID = "plant_id"
DEFAULT_PLANT_ID = "0"
DEFAULT_NAME = "Growatt"
SCAN_INTERVAL = datetime.timedelta(minutes=1) SCAN_INTERVAL = datetime.timedelta(minutes=1)
# Sensor type order is: Sensor name, Unit of measurement, api data name, additional options # Sensor type order is: Sensor name, Unit of measurement, api data name, additional options
...@@ -558,17 +558,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ...@@ -558,17 +558,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
) )
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Growatt sensor.""" """Set up growatt server from yaml."""
username = config[CONF_USERNAME] if not hass.config_entries.async_entries(DOMAIN):
password = config[CONF_PASSWORD] _LOGGER.warning(
plant_id = config[CONF_PLANT_ID] "Loading Growatt via platform setup is deprecated."
name = config[CONF_NAME] "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. # 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": if not login_response["success"] and login_response["errCode"] == "102":
_LOGGER.error("Username or Password may be incorrect!") _LOGGER.error("Username or Password may be incorrect!")
return return
...@@ -579,6 +588,20 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ...@@ -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. # Get a list of devices for specified plant to add sensors for.
devices = api.device_list(plant_id) 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 = [] entities = []
probe = GrowattData(api, username, password, plant_id, "total") probe = GrowattData(api, username, password, plant_id, "total")
for sensor in TOTAL_SENSOR_TYPES: for sensor in TOTAL_SENSOR_TYPES:
...@@ -616,7 +639,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ...@@ -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): class GrowattInverter(SensorEntity):
......
{
"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"
}
{
"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 = [ ...@@ -92,6 +92,7 @@ FLOWS = [
"google_travel_time", "google_travel_time",
"gpslogger", "gpslogger",
"gree", "gree",
"growatt_server",
"guardian", "guardian",
"habitica", "habitica",
"hangouts", "hangouts",
......
...@@ -386,6 +386,9 @@ googlemaps==2.5.1 ...@@ -386,6 +386,9 @@ googlemaps==2.5.1
# homeassistant.components.gree # homeassistant.components.gree
greeclimate==0.11.4 greeclimate==0.11.4
# homeassistant.components.growatt_server
growattServer==1.0.0
# homeassistant.components.profiler # homeassistant.components.profiler
guppy3==3.1.0 guppy3==3.1.0
......
"""Tests for the growatt_server component."""
"""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"
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment