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.
This commit is contained in:
Brig Lamoreaux 2023-12-14 03:07:13 -07:00 committed by GitHub
parent 2e448d2d13
commit 7721840298
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 145 additions and 70 deletions

View file

@ -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."""

View file

@ -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"

View file

@ -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

View file

@ -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": {

View file

@ -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"),

View file

@ -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)

View file

@ -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

View file

@ -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