From 96100d85135ca80a40c73261eb07305c6ae27b0e Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 12 Sep 2021 18:46:05 -0700 Subject: [PATCH] fix: config flow errors related to the betas (#41) * fix: config flow errors related to the betas * Update en.json * clean up test code * last min tweaks --- custom_components/openei/__init__.py | 25 ++-- custom_components/openei/config_flow.py | 29 +++-- custom_components/openei/manifest.json | 2 +- custom_components/openei/translations/en.json | 7 +- tests/test_config_flow.py | 115 +++++++++++++++++- 5 files changed, 156 insertions(+), 22 deletions(-) diff --git a/custom_components/openei/__init__.py b/custom_components/openei/__init__.py index a0b65c4..e2644f9 100644 --- a/custom_components/openei/__init__.py +++ b/custom_components/openei/__init__.py @@ -16,7 +16,6 @@ CONF_LOCATION, CONF_MANUAL_PLAN, CONF_PLAN, - CONF_RADIUS, CONF_SENSOR, DOMAIN, PLATFORMS, @@ -42,13 +41,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): 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[CONF_SENSOR] = None + updated_config.pop(CONF_SENSOR, None) - if CONF_MANUAL_PLAN in updated_config.keys() and entry.data.get(CONF_MANUAL_PLAN): + if CONF_MANUAL_PLAN in updated_config.keys() and updated_config[CONF_MANUAL_PLAN]: updated_config[CONF_PLAN] = updated_config[CONF_MANUAL_PLAN] updated_config.pop(CONF_MANUAL_PLAN, None) + if CONF_LOCATION in updated_config.keys() and updated_config[CONF_LOCATION] == "": + updated_config.pop(CONF_LOCATION, None) + + _LOGGER.debug("updated_config: %s", updated_config) if updated_config != entry.data: hass.config_entries.async_update_entry(entry, data=updated_config) @@ -115,19 +120,25 @@ async def _async_refresh_data(self, data=None) -> None: raise UpdateFailed() from exception -def get_sensors(hass, config): +def get_sensors(hass, config) -> dict: api = config.data.get(CONF_API_KEY) plan = config.data.get(CONF_PLAN) meter = config.data.get(CONF_SENSOR) - readings = None + reading = None if meter: - readings = hass.states.get(meter).state + _LOGGER.debug("Using meter data from sensor: %s", meter) + reading = hass.states.get(meter) + if not reading: + reading = None + _LOGGER.warning("Sensor: %s is not valid.", meter) + else: + reading = reading.state rate = openeihttp.Rates( api=api, plan=plan, - readings=readings, + reading=reading, ) rate.update() data = {} diff --git a/custom_components/openei/config_flow.py b/custom_components/openei/config_flow.py index 5bcf9de..cd619cc 100644 --- a/custom_components/openei/config_flow.py +++ b/custom_components/openei/config_flow.py @@ -121,25 +121,34 @@ async def async_step_init(self, user_input=None): # pylint: disable=unused-argu 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] = None + if user_input[CONF_RADIUS] == '""': + user_input[CONF_RADIUS] = None self._data.update(user_input) + _LOGGER.debug("Step 1: %s", 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) + _LOGGER.debug("Step 2: %s", 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_SENSOR] == "(none)": + user_input[CONF_SENSOR] = None self._data.update(user_input) + _LOGGER.debug("Step 3: %s", user_input) return self.async_create_entry(title="", data=self._data) return await self._show_config_form_3(user_input) @@ -148,7 +157,7 @@ async def _show_config_form(self, user_input): # pylint: disable=unused-argumen """Show the configuration form to edit location data.""" return self.async_show_form( step_id="user", - data_schema=_get_schema_step_1(self.hass, self._data, self._data), + data_schema=_get_schema_step_1(self.hass, user_input, self._data), errors=self._errors, ) @@ -158,7 +167,7 @@ async def _show_config_form_2(self, user_input): # pylint: disable=unused-argum return self.async_show_form( step_id="user_2", data_schema=_get_schema_step_2( - self.hass, self._data, self._data, utility_list + self.hass, user_input, self._data, utility_list ), errors=self._errors, ) @@ -169,7 +178,7 @@ async def _show_config_form_3(self, user_input): # pylint: disable=unused-argum return self.async_show_form( step_id="user_3", data_schema=_get_schema_step_3( - self.hass, self._data, self._data, plan_list + self.hass, user_input, self._data, plan_list ), errors=self._errors, ) @@ -210,6 +219,8 @@ def _get_schema_step_2( entry_id: str = None, ) -> vol.Schema: """Gets a schema using the default_dict as a backup.""" + if user_input is None: + user_input = {} def _get_default(key: str, fallback_default: Any = None) -> None: """Gets default value for key.""" @@ -232,8 +243,10 @@ def _get_schema_step_3( entry_id: str = None, ) -> vol.Schema: """Gets a schema using the default_dict as a backup.""" + if user_input is None: + user_input = {} - if CONF_SENSOR in default_dict.keys() and default_dict[CONF_SENSOR] is None: + if CONF_SENSOR in default_dict.keys() and default_dict[CONF_SENSOR] == "(none)": default_dict.pop(CONF_SENSOR, None) def _get_default(key: str, fallback_default: Any = None) -> Any | None: @@ -250,7 +263,7 @@ def _get_default(key: str, fallback_default: Any = None) -> Any | None: ): cv.string, vol.Required( CONF_SENSOR, default=_get_default(CONF_SENSOR, "(none)") - ): vol.In(_get_entities(hass, SENSORS_DOMAIN, "energy", ["(none)"])), + ): vol.In(_get_entities(hass, SENSORS_DOMAIN, "energy", "(none)")), }, ) @@ -309,7 +322,7 @@ async def _get_plan_list(hass, user_input) -> list | None: def _lookup_plans(handler) -> list: """Return list of utilities and plans.""" response = handler.lookup_plans() - response.insert(0, "Not Listed") + response["Not Listed"] = [{"name": "Not Listed", "label": "Not Listed"}] _LOGGER.debug("lookup_plans: %s", response) return response diff --git a/custom_components/openei/manifest.json b/custom_components/openei/manifest.json index 726de82..3611216 100644 --- a/custom_components/openei/manifest.json +++ b/custom_components/openei/manifest.json @@ -7,6 +7,6 @@ "iot_class": "cloud_polling", "config_flow": true, "codeowners": ["@firstof9"], - "requirements": ["python-openei==0.1.12"], + "requirements": ["python-openei==0.1.14"], "version": "0.1.5" } diff --git a/custom_components/openei/translations/en.json b/custom_components/openei/translations/en.json index 826c59d..29aaed0 100644 --- a/custom_components/openei/translations/en.json +++ b/custom_components/openei/translations/en.json @@ -3,7 +3,7 @@ "step": { "user": { "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\nIf location information is omitted your latitude and longitude will be used.", + "description": "If you do not have an API Key yet you can get one here: https://openei.org/services/api/signup/\n\nIf location information is omitted your latitude and longitude will be used.\n\nEnter \"\" to clear optional fields.", "data": { "api_key": "API Key", "radius": "Radius in miles (optional)", @@ -38,10 +38,11 @@ "step": { "user": { "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\nIf location information is omitted your latitude and longitude will be used.", + "description": "If you do not have an API Key yet you can get one here: https://openei.org/services/api/signup/\n\nIf location information is omitted your latitude and longitude will be used.\n\nEnter \"\" to clear optional fields.", "data": { "api_key": "API Key", - "radius": "Radius in miles (optional)" + "radius": "Radius in miles (optional)", + "location": "City,State or Zip Code (optional)" } }, "user_2": { diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 76769f0..f939b26 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -125,7 +125,7 @@ async def test_form( "radius": "20", "utility": "Fake Utility Co", "rate_plan": "randomstring", - "sensor": "(none)", + "sensor": None, "location": "", "manual_plan": "", }, @@ -224,7 +224,7 @@ async def test_options_flow( "user_3", { "rate_plan": "randomstring", - "sensor": ["(none)"], + "sensor": "(none)", "manual_plan": "", }, "Fake Utility Co", @@ -233,7 +233,7 @@ async def test_options_flow( "radius": "", "utility": "Fake Utility Co", "rate_plan": "randomstring", - "sensor": ["(none)"], + "sensor": None, "location": "", "manual_plan": "", }, @@ -315,3 +315,112 @@ async def test_options_flow_no_changes( 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": "", + "location": "", + }, + "user_2", + { + "utility": "Fake Utility Co", + }, + "user_3", + { + "rate_plan": "randomstring", + "sensor": "(none)", + "manual_plan": "", + }, + "Fake Utility Co", + { + "api_key": "fakeAPIKey", + "radius": "", + "utility": "Fake Utility Co", + "rate_plan": "randomstring", + "sensor": None, + "location": "", + "manual_plan": "", + }, + ), + ], +) +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": "", + "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"}] + }, + ): + + 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 + ) \ No newline at end of file