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:
parent
2e448d2d13
commit
7721840298
8 changed files with 145 additions and 70 deletions
|
@ -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."""
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue