From 7721840298a760020316997790133ec861025978 Mon Sep 17 00:00:00 2001 From: Brig Lamoreaux Date: Thu, 14 Dec 2023 03:07:13 -0700 Subject: [PATCH] Allow multiple configs for srp energy (#96573) * Allow multiple configs. * Rename test configs. * Remove unused property * Merge branch 'dev' into srp_energy_202307.coordinator * Use title in device name. --- .../components/srp_energy/config_flow.py | 69 ++++++++------- homeassistant/components/srp_energy/const.py | 4 + homeassistant/components/srp_energy/sensor.py | 14 +++- .../components/srp_energy/strings.json | 2 +- tests/components/srp_energy/__init__.py | 29 +++++-- tests/components/srp_energy/conftest.py | 7 +- .../components/srp_energy/test_config_flow.py | 84 ++++++++++++++----- tests/components/srp_energy/test_sensor.py | 6 +- 8 files changed, 145 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/srp_energy/config_flow.py b/homeassistant/components/srp_energy/config_flow.py index c52574ff312..ac32e005e06 100644 --- a/homeassistant/components/srp_energy/config_flow.py +++ b/homeassistant/components/srp_energy/config_flow.py @@ -7,12 +7,12 @@ from srpenergy.client import SrpEnergyClient import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_ID, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError -from .const import CONF_IS_TOU, DEFAULT_NAME, DOMAIN, LOGGER +from .const import CONF_IS_TOU, DOMAIN, LOGGER async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: @@ -40,46 +40,53 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 - async def async_step_user( - self, user_input: dict[str, Any] | None = None - ) -> FlowResult: - """Handle a flow initialized by the user.""" - errors = {} - default_title: str = DEFAULT_NAME - - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - if self.hass.config.location_name: - default_title = self.hass.config.location_name - - if user_input: - try: - await validate_input(self.hass, user_input) - except ValueError: - # Thrown when the account id is malformed - errors["base"] = "invalid_account" - except InvalidAuth: - errors["base"] = "invalid_auth" - except Exception: # pylint: disable=broad-except - LOGGER.exception("Unexpected exception") - return self.async_abort(reason="unknown") - else: - return self.async_create_entry(title=default_title, data=user_input) - + @callback + def _show_form(self, errors: dict[str, Any]) -> FlowResult: + """Show the form to the user.""" + LOGGER.debug("Show Form") return self.async_show_form( step_id="user", data_schema=vol.Schema( { + vol.Required( + CONF_NAME, default=self.hass.config.location_name + ): str, vol.Required(CONF_ID): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, vol.Optional(CONF_IS_TOU, default=False): bool, } ), - errors=errors or {}, + errors=errors, ) + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + LOGGER.debug("Config entry") + errors: dict[str, str] = {} + if not user_input: + return self._show_form(errors) + + try: + await validate_input(self.hass, user_input) + except ValueError: + # Thrown when the account id is malformed + errors["base"] = "invalid_account" + return self._show_form(errors) + except InvalidAuth: + errors["base"] = "invalid_auth" + return self._show_form(errors) + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected exception") + return self.async_abort(reason="unknown") + + await self.async_set_unique_id(user_input[CONF_ID]) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) + class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/srp_energy/const.py b/homeassistant/components/srp_energy/const.py index bace71aca55..b2ab05f43d5 100644 --- a/homeassistant/components/srp_energy/const.py +++ b/homeassistant/components/srp_energy/const.py @@ -11,3 +11,7 @@ CONF_IS_TOU = "is_tou" PHOENIX_TIME_ZONE = "America/Phoenix" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1440) + +DEVICE_CONFIG_URL = "https://www.srpnet.com/" +DEVICE_MANUFACTURER = "srpnet.com" +DEVICE_MODEL = "Service Api" diff --git a/homeassistant/components/srp_energy/sensor.py b/homeassistant/components/srp_energy/sensor.py index 37aacf4ff25..9e8b8d08de9 100644 --- a/homeassistant/components/srp_energy/sensor.py +++ b/homeassistant/components/srp_energy/sensor.py @@ -11,10 +11,11 @@ from homeassistant.const import UnitOfEnergy from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import SRPEnergyDataUpdateCoordinator -from .const import DOMAIN +from .const import DEVICE_CONFIG_URL, DEVICE_MANUFACTURER, DEVICE_MODEL, DOMAIN async def async_setup_entry( @@ -37,18 +38,23 @@ class SrpEntity(CoordinatorEntity[SRPEnergyDataUpdateCoordinator], SensorEntity) _attr_translation_key = "energy_usage" def __init__( - self, coordinator: SRPEnergyDataUpdateCoordinator, config_entry: ConfigEntry + self, + coordinator: SRPEnergyDataUpdateCoordinator, + config_entry: ConfigEntry, ) -> None: """Initialize the SrpEntity class.""" super().__init__(coordinator) self._attr_unique_id = f"{config_entry.entry_id}_total_usage" self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, config_entry.entry_id)}, - name="SRP Energy", + name=f"SRP Energy {config_entry.title}", entry_type=DeviceEntryType.SERVICE, + manufacturer=DEVICE_MANUFACTURER, + model=DEVICE_MODEL, + configuration_url=DEVICE_CONFIG_URL, ) @property - def native_value(self) -> float: + def native_value(self) -> StateType: """Return the state of the device.""" return self.coordinator.data diff --git a/homeassistant/components/srp_energy/strings.json b/homeassistant/components/srp_energy/strings.json index fd963411198..35195ddb4f2 100644 --- a/homeassistant/components/srp_energy/strings.json +++ b/homeassistant/components/srp_energy/strings.json @@ -17,7 +17,7 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } }, "entity": { diff --git a/tests/components/srp_energy/__init__.py b/tests/components/srp_energy/__init__.py index 99a5da84fe2..634d589195e 100644 --- a/tests/components/srp_energy/__init__.py +++ b/tests/components/srp_energy/__init__.py @@ -1,21 +1,36 @@ """Tests for the SRP Energy integration.""" +from typing import Final + from homeassistant.components.srp_energy.const import CONF_IS_TOU -from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_ID, CONF_NAME, CONF_PASSWORD, CONF_USERNAME -ACCNT_ID = "123456789" -ACCNT_IS_TOU = False -ACCNT_USERNAME = "abba" -ACCNT_PASSWORD = "ana" -ACCNT_NAME = "Home" +ACCNT_ID: Final = "123456789" +ACCNT_IS_TOU: Final = False +ACCNT_USERNAME: Final = "test_username" +ACCNT_PASSWORD: Final = "test_password" +ACCNT_NAME: Final = "Test Home" -TEST_USER_INPUT = { + +TEST_CONFIG_HOME: Final[dict[str, str]] = { + CONF_NAME: ACCNT_NAME, CONF_ID: ACCNT_ID, CONF_USERNAME: ACCNT_USERNAME, CONF_PASSWORD: ACCNT_PASSWORD, CONF_IS_TOU: ACCNT_IS_TOU, } +ACCNT_ID_2: Final = "987654321" +ACCNT_NAME_2: Final = "Test Cabin" + +TEST_CONFIG_CABIN: Final[dict[str, str]] = { + CONF_NAME: ACCNT_NAME_2, + CONF_ID: ACCNT_ID_2, + CONF_USERNAME: ACCNT_USERNAME, + CONF_PASSWORD: ACCNT_PASSWORD, + CONF_IS_TOU: ACCNT_IS_TOU, +} + MOCK_USAGE = [ ("7/31/2022", "00:00 AM", "2022-07-31T00:00:00", "1.2", "0.19"), ("7/31/2022", "01:00 AM", "2022-07-31T01:00:00", "1.3", "0.20"), diff --git a/tests/components/srp_energy/conftest.py b/tests/components/srp_energy/conftest.py index 3ffebe167c2..e3597081d77 100644 --- a/tests/components/srp_energy/conftest.py +++ b/tests/components/srp_energy/conftest.py @@ -9,10 +9,11 @@ from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.srp_energy.const import DOMAIN, PHOENIX_TIME_ZONE +from homeassistant.const import CONF_ID from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from . import MOCK_USAGE, TEST_USER_INPUT +from . import MOCK_USAGE, TEST_CONFIG_HOME from tests.common import MockConfigEntry @@ -42,8 +43,7 @@ def fixture_test_date(hass: HomeAssistant, hass_tz_info) -> dt.datetime | None: def fixture_mock_config_entry() -> MockConfigEntry: """Return the default mocked config entry.""" return MockConfigEntry( - domain=DOMAIN, - data=TEST_USER_INPUT, + domain=DOMAIN, data=TEST_CONFIG_HOME, unique_id=TEST_CONFIG_HOME[CONF_ID] ) @@ -81,7 +81,6 @@ async def init_integration( mock_srp_energy_config_flow, ) -> MockConfigEntry: """Set up the Srp Energy integration for testing.""" - freezer.move_to(test_date) mock_config_entry.add_to_hass(hass) diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py index dfd1d41e820..572b67259f1 100644 --- a/tests/components/srp_energy/test_config_flow.py +++ b/tests/components/srp_energy/test_config_flow.py @@ -1,13 +1,23 @@ """Test the SRP Energy config flow.""" from unittest.mock import MagicMock, patch -from homeassistant import config_entries from homeassistant.components.srp_energy.const import CONF_IS_TOU, DOMAIN +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_SOURCE, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from . import ACCNT_ID, ACCNT_IS_TOU, ACCNT_PASSWORD, ACCNT_USERNAME, TEST_USER_INPUT +from . import ( + ACCNT_ID, + ACCNT_ID_2, + ACCNT_IS_TOU, + ACCNT_NAME, + ACCNT_NAME_2, + ACCNT_PASSWORD, + ACCNT_USERNAME, + TEST_CONFIG_CABIN, + TEST_CONFIG_HOME, +) from tests.common import MockConfigEntry @@ -17,7 +27,7 @@ async def test_show_form( ) -> None: """Test show configuration form.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER} + DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) assert result["type"] == FlowResultType.FORM @@ -29,12 +39,12 @@ async def test_show_form( return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure( - flow_id=result["flow_id"], user_input=TEST_USER_INPUT + flow_id=result["flow_id"], user_input=TEST_CONFIG_HOME ) await hass.async_block_till_done() assert result["type"] == FlowResultType.CREATE_ENTRY - assert result["title"] == "test home" + assert result["title"] == ACCNT_NAME assert "data" in result assert result["data"][CONF_ID] == ACCNT_ID @@ -56,11 +66,11 @@ async def test_form_invalid_account( mock_srp_energy_config_flow.validate.side_effect = ValueError result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER} + DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( - flow_id=result["flow_id"], user_input=TEST_USER_INPUT + flow_id=result["flow_id"], user_input=TEST_CONFIG_HOME ) assert result["type"] == FlowResultType.FORM @@ -75,11 +85,11 @@ async def test_form_invalid_auth( mock_srp_energy_config_flow.validate.return_value = False result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER} + DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( - flow_id=result["flow_id"], user_input=TEST_USER_INPUT + flow_id=result["flow_id"], user_input=TEST_CONFIG_HOME ) assert result["type"] == FlowResultType.FORM @@ -94,11 +104,11 @@ async def test_form_unknown_error( mock_srp_energy_config_flow.validate.side_effect = Exception result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER} + DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( - flow_id=result["flow_id"], user_input=TEST_USER_INPUT + flow_id=result["flow_id"], user_input=TEST_CONFIG_HOME ) assert result["type"] == FlowResultType.ABORT @@ -109,18 +119,52 @@ async def test_flow_entry_already_configured( hass: HomeAssistant, init_integration: MockConfigEntry ) -> None: """Test user input for config_entry that already exists.""" - user_input = { - CONF_ID: init_integration.data[CONF_ID], - CONF_USERNAME: "abba2", - CONF_PASSWORD: "ana2", - CONF_IS_TOU: False, - } + # Verify mock config setup from fixture + assert init_integration.state == ConfigEntryState.LOADED + assert init_integration.data[CONF_ID] == ACCNT_ID + assert init_integration.unique_id == ACCNT_ID - assert user_input[CONF_ID] == ACCNT_ID + # Attempt a second config using same account id. This is the unique id between configs. + user_input_second = TEST_CONFIG_HOME + user_input_second[CONF_ID] = init_integration.data[CONF_ID] + + assert user_input_second[CONF_ID] == ACCNT_ID result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: config_entries.SOURCE_USER}, data=user_input + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input_second ) assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "single_instance_allowed" + assert result["reason"] == "already_configured" + + +async def test_flow_multiple_configs( + hass: HomeAssistant, init_integration: MockConfigEntry, capsys +) -> None: + """Test multiple config entries.""" + # Verify mock config setup from fixture + assert init_integration.state == ConfigEntryState.LOADED + assert init_integration.data[CONF_ID] == ACCNT_ID + assert init_integration.unique_id == ACCNT_ID + + # Attempt a second config using different account id. This is the unique id between configs. + assert TEST_CONFIG_CABIN[CONF_ID] != ACCNT_ID + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=TEST_CONFIG_CABIN + ) + + # Verify created + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == ACCNT_NAME_2 + + assert "data" in result + assert result["data"][CONF_ID] == ACCNT_ID_2 + assert result["data"][CONF_USERNAME] == ACCNT_USERNAME + assert result["data"][CONF_PASSWORD] == ACCNT_PASSWORD + assert result["data"][CONF_IS_TOU] == ACCNT_IS_TOU + + # Verify multiple configs + entries = hass.config_entries.async_entries() + domain_entries = [entry for entry in entries if entry.domain == DOMAIN] + assert len(domain_entries) == 2 diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py index 32d2d971d2c..2d49fd13bf1 100644 --- a/tests/components/srp_energy/test_sensor.py +++ b/tests/components/srp_energy/test_sensor.py @@ -28,7 +28,7 @@ async def test_loading_sensors(hass: HomeAssistant, init_integration) -> None: async def test_srp_entity(hass: HomeAssistant, init_integration) -> None: """Test the SrpEntity.""" - usage_state = hass.states.get("sensor.srp_energy_energy_usage") + usage_state = hass.states.get("sensor.srp_energy_mock_title_energy_usage") assert usage_state.state == "150.8" # Validate attributions @@ -61,7 +61,7 @@ async def test_srp_entity_update_failed( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - usage_state = hass.states.get("sensor.home_energy_usage") + usage_state = hass.states.get("sensor.srp_energy_mock_title_energy_usage") assert usage_state is None @@ -84,5 +84,5 @@ async def test_srp_entity_timeout( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - usage_state = hass.states.get("sensor.home_energy_usage") + usage_state = hass.states.get("sensor.srp_energy_mock_title_energy_usage") assert usage_state is None