diff --git a/custom_components/openei/__init__.py b/custom_components/openei/__init__.py index ffde059..2821c96 100644 --- a/custom_components/openei/__init__.py +++ b/custom_components/openei/__init__.py @@ -40,27 +40,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) _LOGGER.info(STARTUP_MESSAGE) - updated_config = entry.data.copy() - - _LOGGER.debug("config_entry: %s", updated_config) - - if CONF_SENSOR in updated_config.keys() and updated_config[CONF_SENSOR] == "(none)": - updated_config.pop(CONF_SENSOR, None) - - if CONF_MANUAL_PLAN not in updated_config.keys(): - updated_config[CONF_MANUAL_PLAN] = "" - - if CONF_PLAN not in updated_config.keys(): - updated_config[CONF_PLAN] = "" - - if not any([updated_config[CONF_MANUAL_PLAN], updated_config[CONF_PLAN]]): + if CONF_MANUAL_PLAN not in entry.data.keys() and CONF_PLAN not in entry.data.keys(): _LOGGER.error("Plan configuration missing.") raise ConfigEntryNotReady - _LOGGER.debug("updated_config: %s", updated_config) - if updated_config != entry.data: - hass.config_entries.async_update_entry(entry, data=updated_config) - entry.add_update_listener(update_listener) coordinator = OpenEIDataUpdateCoordinator(hass, config=entry) diff --git a/custom_components/openei/config_flow.py b/custom_components/openei/config_flow.py index c11ca00..ceeb871 100644 --- a/custom_components/openei/config_flow.py +++ b/custom_components/openei/config_flow.py @@ -35,6 +35,7 @@ def __init__(self): """Initialize.""" self._data = {} self._errors = {} + self._entry = {} async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" @@ -61,6 +62,8 @@ async def async_step_user_3(self, user_input=None): self._errors = {} if user_input is not None: + if user_input[CONF_SENSOR] == "(none)": + user_input.pop(CONF_SENSOR, None) self._data.update(user_input) return self.async_create_entry( title=self._data[CONF_UTILITY], data=self._data @@ -103,77 +106,69 @@ async def _show_config_form_3(self, user_input): # pylint: disable=unused-argum errors=self._errors, ) + async def async_step_reconfigure(self, user_input: dict[str, Any] | None = None): + """Add reconfigure step to allow to reconfigure a config entry.""" + self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + assert self._entry + self._data = dict(self._entry.data) + self._errors = {} + + if user_input is not None: + self._data.update(user_input) + return await self.async_step_reconfig_2() + return await self._show_reconfig_form(user_input) + + async def _show_reconfig_form(self, user_input): + """Show the configuration form to edit configuration data.""" + return self.async_show_form( + step_id="reconfigure", + data_schema=_get_schema_step_1(user_input, self._data), + errors=self._errors, + ) + + async def async_step_reconfig_2(self, user_input: dict[str, Any] | None = None): + """Add reconfigure step to allow to reconfigure a config entry.""" + self._errors = {} + + if user_input is not None: + self._data.update(user_input) + return await self.async_step_reconfig_3() + return await self._show_reconfig_2() + + async def _show_reconfig_2(self): + """Show the configuration form to edit configuration data.""" + defaults = {} + utility_list = await _get_utility_list(self.hass, self._data) + _LOGGER.debug("Utility list: %s", utility_list) + return self.async_show_form( + step_id="reconfig_2", + data_schema=_get_schema_step_2(self._data, defaults, utility_list), + errors=self._errors, + ) + + async def async_step_reconfig_3(self, user_input: dict[str, Any] | None = None): + """Add reconfigure step to allow to reconfigure a config entry.""" + self._errors = {} -# class OpenEIOptionsFlowHandler(config_entries.OptionsFlow): -# """Blueprint config flow options handler.""" - -# def __init__(self, config_entry): -# """Initialize OpenEI options flow.""" -# self.config_entry = config_entry -# self._data = dict(config_entry.data) -# self._errors = {} - -# async def async_step_init(self, user_input=None): # pylint: disable=unused-argument -# """Manage the options.""" -# return await self.async_step_user() - -# async def async_step_user(self, user_input=None): -# """Handle a flow initialized by the user.""" -# if user_input is not None: -# if user_input[CONF_LOCATION] == '""': -# user_input[CONF_LOCATION] = "" -# self._data.update(user_input) -# return await self.async_step_user_2() - -# return await self._show_config_form(user_input) - -# async def async_step_user_2(self, user_input=None): -# """Handle a flow initialized by the user.""" -# _LOGGER.debug("data: %s", self._data) -# if user_input is not None: -# self._data.update(user_input) -# return await self.async_step_user_3() - -# return await self._show_config_form_2(user_input) - -# async def async_step_user_3(self, user_input=None): -# """Handle a flow initialized by the user.""" -# _LOGGER.debug("data: %s", self._data) -# if user_input is not None: -# if user_input[CONF_MANUAL_PLAN] == '""': -# user_input[CONF_MANUAL_PLAN] = "" -# self._data.update(user_input) -# return self.async_create_entry(title="", data=self._data) - -# return await self._show_config_form_3(user_input) - -# async def _show_config_form(self, user_input: Optional[Dict[str, Any]]): -# """Show the configuration form to edit location data.""" -# return self.async_show_form( -# step_id="user", -# data_schema=_get_schema_step_1(user_input, self._data), -# errors=self._errors, -# ) - -# async def _show_config_form_2(self, user_input: Optional[Dict[str, Any]]): -# """Show the configuration form to edit location data.""" -# utility_list = await _get_utility_list(self.hass, self._data) -# return self.async_show_form( -# step_id="user_2", -# data_schema=_get_schema_step_2(user_input, self._data, utility_list), -# errors=self._errors, -# ) - -# async def _show_config_form_3(self, user_input: Optional[Dict[str, Any]]): -# """Show the configuration form to edit location data.""" -# plan_list = await _get_plan_list(self.hass, self._data) -# return self.async_show_form( -# step_id="user_3", -# data_schema=_get_schema_step_3( -# self.hass, user_input, self._data, plan_list -# ), -# errors=self._errors, -# ) + if user_input is not None: + if user_input[CONF_SENSOR] == "(none)": + user_input.pop(CONF_SENSOR, None) + self._data.update(user_input) + self.hass.config_entries.async_update_entry(self._entry, data=self._data) + await self.hass.config_entries.async_reload(self._entry.entry_id) + _LOGGER.debug("%s reconfigured.", DOMAIN) + return self.async_abort(reason="reconfigure_successful") + return await self._show_reconfig_3() + + async def _show_reconfig_3(self): + """Show the configuration form to edit configuration data.""" + defaults = {} + plan_list = await _get_plan_list(self.hass, self._data) + return self.async_show_form( + step_id="reconfig_3", + data_schema=_get_schema_step_3(self.hass, self._data, defaults, plan_list), + errors=self._errors, + ) def _get_schema_step_1( diff --git a/custom_components/openei/translations/en.json b/custom_components/openei/translations/en.json index 613217e..d2bc683 100644 --- a/custom_components/openei/translations/en.json +++ b/custom_components/openei/translations/en.json @@ -25,42 +25,38 @@ "sensor": "Energy sensor for Tier plans (optional)", "manual_plan": "Manually enter plan from OpenEI website (optional)" } - } - }, - "error": { - "auth": "Username/Password is wrong." - }, - "abort": { - "single_instance_allowed": "Only a single configuration of Blueprint is allowed." - } - }, - "options": { - "step": { - "user": { + }, + "reconfigure": { "title": "OpenEI (Step 1)", - "description": "If you do not have an API Key yet you can get one here: https://openei.org/services/api/signup/ \n\nEnter \"\" in address to clear it and use latitude and longitude from Home Assistant.", + "description": "If you do not have an API Key yet you can get one here: https://openei.org/services/api/signup/\n\nOmit address to use latitude and longitude from Home Assistant.", "data": { "api_key": "API Key", "radius": "Radius in miles (optional)", "location": "City,State or Zip Code (optional)" } }, - "user_2": { + "reconfig_2": { "title": "OpenEI (Step 2)", "description": "Select your utility company.\n\nIf you are entering a plan manually please select 'Not Listed'", "data": { "utility": "Utility Company" } }, - "user_3": { + "reconfig_3": { "title": "OpenEI (Step 3)", - "description": "Select your plan from the list. If you are unsure, check your utility bill.\n\nManual plan will override the selected rate plan.\n\nEnter \"\" to clear the manual plan.", + "description": "Select your plan from the list. If you are unsure, check your utility bill.\n\nManual plan will override the selected rate plan.", "data": { "rate_plan": "Rate Plan", "sensor": "Energy sensor for Tier plans (optional)", "manual_plan": "Manually enter plan from OpenEI website (optional)" } - } + } + }, + "error": { + "auth": "Username/Password is wrong." + }, + "abort": { + "single_instance_allowed": "Only a single configuration of Blueprint is allowed." } } } \ No newline at end of file diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 59cca8d..5e254cb 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -1,15 +1,20 @@ """Test OpenEI config flow.""" import logging +import re from unittest.mock import patch import pytest from homeassistant import config_entries, setup +from homeassistant.data_entry_flow import FlowResultType from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.openei.const import DOMAIN +from tests.common import load_fixture +from tests.const import CONFIG_DATA pytestmark = pytest.mark.asyncio +TEST_PATTERN = r"^https://api\.openei\.org/utility_rates\?.*$" @pytest.mark.parametrize( @@ -37,7 +42,6 @@ "radius": 0, "utility": "Fake Utility Co", "rate_plan": "randomstring", - "sensor": "(none)", "location": "", "manual_plan": "", }, @@ -104,432 +108,109 @@ async def test_form( assert len(mock_setup_entry.mock_calls) == 1 -# @pytest.mark.parametrize( -# "input_1,step_id_2,input_2,step_id_3,input_3,title,data", -# [ -# ( -# { -# "api_key": "fakeAPIKey_new", -# "radius": 20, -# "location": "", -# }, -# "user_2", -# { -# "utility": "Fake Utility Co", -# }, -# "user_3", -# { -# "rate_plan": "randomstring", -# "sensor": "(none)", -# "manual_plan": "", -# }, -# "Fake Utility Co", -# { -# "api_key": "fakeAPIKey_new", -# "radius": 20, -# "utility": "Fake Utility Co", -# "rate_plan": "randomstring", -# "sensor": "(none)", -# "location": "", -# "manual_plan": "", -# }, -# ), -# ], -# ) -# async def test_options_flow( -# input_1, -# step_id_2, -# input_2, -# step_id_3, -# input_3, -# title, -# data, -# hass, -# mock_api_config, -# caplog, -# ): -# """Test config flow options.""" -# entry = MockConfigEntry( -# domain=DOMAIN, -# title="Fake Utility Co", -# data={ -# "api_key": "fakeAPIKey", -# "radius": None, -# "location": "", -# "utility": "Fake Utility Co", -# "rate_plan": "randomstring", -# "sensor": "(none)", -# "manual_plan": None, -# }, -# ) -# entry.add_to_hass(hass) -# assert await hass.config_entries.async_setup(entry.entry_id) -# await hass.async_block_till_done() - -# await setup.async_setup_component(hass, "persistent_notification", {}) -# result = await hass.config_entries.options.async_init(entry.entry_id) - -# assert result["type"] == "form" -# assert result["errors"] == {} -# # assert result['title'] == title_1 - -# with patch("custom_components.openei.async_setup", return_value=True), patch( -# "custom_components.openei.async_setup_entry", -# return_value=True, -# ), patch( -# "custom_components.openei.config_flow._get_entities", -# return_value=["(none)"], -# ): - -# result2 = await hass.config_entries.options.async_configure( -# result["flow_id"], input_1 -# ) -# await hass.async_block_till_done() - -# assert result2["type"] == "form" -# assert result2["step_id"] == step_id_2 - -# result3 = await hass.config_entries.options.async_configure( -# result["flow_id"], input_2 -# ) -# await hass.async_block_till_done() - -# assert result3["type"] == "form" -# assert result3["step_id"] == step_id_3 -# result4 = await hass.config_entries.options.async_configure( -# result["flow_id"], input_3 -# ) -# await hass.async_block_till_done() -# assert result4["type"] == "create_entry" -# assert data == entry.data.copy() - -# await hass.async_block_till_done() - - -# @pytest.mark.parametrize( -# "input_1,step_id_2,input_2,step_id_3,input_3,title,data", -# [ -# ( -# { -# "api_key": "fakeAPIKey", -# "radius": 0, -# "location": "", -# }, -# "user_2", -# { -# "utility": "Fake Utility Co", -# }, -# "user_3", -# { -# "rate_plan": "randomstring", -# "sensor": "(none)", -# "manual_plan": "", -# }, -# "Fake Utility Co", -# { -# "api_key": "fakeAPIKey", -# "radius": 0, -# "utility": "Fake Utility Co", -# "rate_plan": "randomstring", -# "sensor": "(none)", -# "location": "", -# "manual_plan": "", -# }, -# ), -# ], -# ) -# async def test_options_flow_no_changes( -# input_1, -# step_id_2, -# input_2, -# step_id_3, -# input_3, -# title, -# data, -# hass, -# mock_api, -# caplog, -# ): -# """Test config flow options.""" -# entry = MockConfigEntry( -# domain=DOMAIN, -# title="Fake Utility Co", -# data={ -# "api_key": "fakeAPIKey", -# "radius": None, -# "location": "", -# "utility": "Fake Utility Co", -# "rate_plan": "randomstring", -# "sensor": "(none)", -# "manual_plan": "", -# }, -# ) - -# entry.add_to_hass(hass) - -# assert await hass.config_entries.async_setup(entry.entry_id) -# await hass.async_block_till_done() - -# await setup.async_setup_component(hass, "persistent_notification", {}) -# result = await hass.config_entries.options.async_init(entry.entry_id) - -# assert result["type"] == "form" -# assert result["errors"] == {} -# # assert result['title'] == title_1 - -# with patch("custom_components.openei.async_setup", return_value=True), patch( -# "custom_components.openei.async_setup_entry", -# return_value=True, -# ), patch( -# "custom_components.openei.config_flow._lookup_plans", -# return_value={ -# "Fake Utility Co": [{"name": "Fake Plan Name", "label": "randomstring"}] -# }, -# ): - -# result2 = await hass.config_entries.options.async_configure( -# result["flow_id"], input_1 -# ) -# await hass.async_block_till_done() - -# assert result2["type"] == "form" -# assert result2["step_id"] == step_id_2 - -# result3 = await hass.config_entries.options.async_configure( -# result["flow_id"], input_2 -# ) -# await hass.async_block_till_done() - -# assert result3["type"] == "form" -# assert result3["step_id"] == step_id_3 -# result4 = await hass.config_entries.options.async_configure( -# result["flow_id"], input_3 -# ) -# await hass.async_block_till_done() -# assert result4["type"] == "create_entry" -# assert data == entry.data.copy() - -# await hass.async_block_till_done() -# assert ( -# "Attempting to reload entities from the openei integration" in caplog.text -# ) - - -# @pytest.mark.parametrize( -# "input_1,step_id_2,input_2,step_id_3,input_3,title,data", -# [ -# ( -# { -# "api_key": "fakeAPIKey", -# "radius": 0, -# "location": '""', -# }, -# "user_2", -# { -# "utility": "Not Listed", -# }, -# "user_3", -# { -# "rate_plan": "Not Listed", -# "sensor": "(none)", -# "manual_plan": "randomstring", -# }, -# "Fake Utility Co", -# { -# "api_key": "fakeAPIKey", -# "radius": 0, -# "utility": "Not Listed", -# "rate_plan": "Not Listed", -# "sensor": "(none)", -# "location": "", -# "manual_plan": "randomstring", -# }, -# ), -# ], -# ) -# async def test_options_flow_some_changes( -# input_1, -# step_id_2, -# input_2, -# step_id_3, -# input_3, -# title, -# data, -# hass, -# mock_api, -# caplog, -# ): -# """Test config flow options.""" -# entry = MockConfigEntry( -# domain=DOMAIN, -# title="Fake Utility Co", -# data={ -# "api_key": "fakeAPIKey", -# "radius": None, -# "location": "12345", -# "utility": "Fake Utility Co", -# "rate_plan": "randomstring", -# "sensor": "(none)", -# "manual_plan": "", -# }, -# ) - -# entry.add_to_hass(hass) - -# assert await hass.config_entries.async_setup(entry.entry_id) -# await hass.async_block_till_done() - -# await setup.async_setup_component(hass, "persistent_notification", {}) -# result = await hass.config_entries.options.async_init(entry.entry_id) - -# assert result["type"] == "form" -# assert result["errors"] == {} -# # assert result['title'] == title_1 - -# with patch("custom_components.openei.async_setup", return_value=True), patch( -# "custom_components.openei.async_setup_entry", -# return_value=True, -# ), patch( -# "custom_components.openei.config_flow._lookup_plans", -# return_value={ -# "Fake Utility Co": [{"name": "Fake Plan Name", "label": "randomstring"}], -# "Not Listed": [{"name": "Not Listed", "label": "Not Listed"}], -# }, -# ): - -# result2 = await hass.config_entries.options.async_configure( -# result["flow_id"], input_1 -# ) -# await hass.async_block_till_done() - -# assert result2["type"] == "form" -# assert result2["step_id"] == step_id_2 - -# result3 = await hass.config_entries.options.async_configure( -# result["flow_id"], input_2 -# ) -# await hass.async_block_till_done() - -# assert result3["type"] == "form" -# assert result3["step_id"] == step_id_3 -# result4 = await hass.config_entries.options.async_configure( -# result["flow_id"], input_3 -# ) -# await hass.async_block_till_done() -# assert result4["type"] == "create_entry" -# assert data == entry.data.copy() - -# await hass.async_block_till_done() -# assert ( -# "Attempting to reload entities from the openei integration" in caplog.text -# ) - - -# @pytest.mark.parametrize( -# "input_1,step_id_2,input_2,step_id_3,input_3,title,data", -# [ -# ( -# { -# "api_key": "fakeAPIKey", -# "radius": 0, -# "location": '""', -# }, -# "user_2", -# { -# "utility": "Fake Utility Co", -# }, -# "user_3", -# { -# "rate_plan": "randomstring", -# "sensor": "(none)", -# "manual_plan": '""', -# }, -# "Fake Utility Co", -# { -# "api_key": "fakeAPIKey", -# "radius": 0, -# "utility": "Fake Utility Co", -# "rate_plan": "randomstring", -# "sensor": "(none)", -# "location": "", -# "manual_plan": "", -# }, -# ), -# ], -# ) -# async def test_options_flow_some_changes_2( -# input_1, -# step_id_2, -# input_2, -# step_id_3, -# input_3, -# title, -# data, -# hass, -# mock_api, -# caplog, -# ): -# """Test config flow options.""" -# entry = MockConfigEntry( -# domain=DOMAIN, -# title="Fake Utility Co", -# data={ -# "api_key": "fakeAPIKey", -# "radius": 0, -# "location": "12345", -# "utility": "Not Listed", -# "rate_plan": "Not Listed", -# "sensor": "(none)", -# "manual_plan": "somerandomstring", -# }, -# ) - -# entry.add_to_hass(hass) - -# assert await hass.config_entries.async_setup(entry.entry_id) -# await hass.async_block_till_done() - -# await setup.async_setup_component(hass, "persistent_notification", {}) -# result = await hass.config_entries.options.async_init(entry.entry_id) +@pytest.mark.parametrize( + "step_id,input_1,step_id_2,input_2,step_id_3,input_3,title,data", + [ + ( + "reconfigure", + { + "api_key": "new_fakeAPIKey", + "radius": 0, + "location": "", + }, + "reconfig_2", + { + "utility": "New Fake Utility Co", + }, + "reconfig_3", + { + "rate_plan": "new_randomstring", + "sensor": "(none)", + "manual_plan": "", + }, + "Fake Utility Co", + { + "api_key": "new_fakeAPIKey", + "radius": 0, + "utility": "New Fake Utility Co", + "rate_plan": "new_randomstring", + "location": "", + "manual_plan": "", + }, + ), + ], +) +async def test_reconfig_form( + step_id, + input_1, + step_id_2, + input_2, + step_id_3, + input_3, + title, + data, + hass, + mock_aioclient, +): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=DOMAIN, + title="Fake Utility Co", + data=CONFIG_DATA, + ) + mock_aioclient.get( + re.compile(TEST_PATTERN), + status=200, + body=load_fixture("plan_data.json"), + repeat=True, + ) -# assert result["type"] == "form" -# assert result["errors"] == {} -# # assert result['title'] == title_1 + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() -# with patch("custom_components.openei.async_setup", return_value=True), patch( -# "custom_components.openei.async_setup_entry", -# return_value=True, -# ), patch( -# "custom_components.openei.config_flow._lookup_plans", -# return_value={ -# "Fake Utility Co": [{"name": "Fake Plan Name", "label": "randomstring"}], -# "Not Listed": [{"name": "Not Listed", "label": "Not Listed"}], -# }, -# ): + reconfigure_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_RECONFIGURE, + "entry_id": entry.entry_id, + }, + ) + assert reconfigure_result["type"] is FlowResultType.FORM + assert reconfigure_result["step_id"] == step_id -# result2 = await hass.config_entries.options.async_configure( -# result["flow_id"], input_1 -# ) -# await hass.async_block_till_done() + with patch( + "custom_components.openei.config_flow._lookup_plans", + return_value={ + "Fake Utility Co": [{"name": "Fake Plan Name", "label": "randomstring"}], + "New Fake Utility Co": [ + {"name": "Fake Plan Name", "label": "new_randomstring"} + ], + }, + ), patch( + "custom_components.openei.config_flow._get_entities", + return_value=["(none)"], + ): + result = await hass.config_entries.flow.async_configure( + reconfigure_result["flow_id"], input_1 + ) + assert result["type"] == "form" + assert result["step_id"] == step_id_2 -# assert result2["type"] == "form" -# assert result2["step_id"] == step_id_2 + result = await hass.config_entries.flow.async_configure( + result["flow_id"], input_2 + ) -# result3 = await hass.config_entries.options.async_configure( -# result["flow_id"], input_2 -# ) -# await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == step_id_3 + result = await hass.config_entries.flow.async_configure( + result["flow_id"], input_3 + ) -# assert result3["type"] == "form" -# assert result3["step_id"] == step_id_3 -# result4 = await hass.config_entries.options.async_configure( -# result["flow_id"], input_3 -# ) -# await hass.async_block_till_done() -# assert result4["type"] == "create_entry" -# assert data == entry.data.copy() + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + await hass.async_block_till_done() -# await hass.async_block_till_done() -# assert ( -# "Attempting to reload entities from the openei integration" in caplog.text -# ) + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.data.copy() == data