mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 07:25:52 -05:00
Improve handling of disconnected meters with Rainforest Automation Eagle-200 (#161185)
Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
@@ -10,7 +10,14 @@ import voluptuous as vol
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_TYPE
|
||||
|
||||
from .const import CONF_CLOUD_ID, CONF_HARDWARE_ADDRESS, CONF_INSTALL_CODE, DOMAIN
|
||||
from .const import (
|
||||
CONF_CLOUD_ID,
|
||||
CONF_HARDWARE_ADDRESS,
|
||||
CONF_INSTALL_CODE,
|
||||
DOMAIN,
|
||||
TYPE_EAGLE_100,
|
||||
TYPE_EAGLE_200,
|
||||
)
|
||||
from .data import CannotConnect, InvalidAuth, async_get_type
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -63,11 +70,32 @@ class RainforestEagleConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
user_input[CONF_TYPE] = eagle_type
|
||||
user_input[CONF_HARDWARE_ADDRESS] = hardware_address
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_CLOUD_ID], data=user_input
|
||||
)
|
||||
# Verify it is a known device, first
|
||||
if not eagle_type:
|
||||
errors["base"] = "unknown_device_type"
|
||||
elif eagle_type == TYPE_EAGLE_100:
|
||||
user_input[CONF_TYPE] = eagle_type
|
||||
|
||||
# For EAGLE-100, there is no hardware address to select, so set it to None and move on
|
||||
user_input[CONF_HARDWARE_ADDRESS] = None
|
||||
elif eagle_type == TYPE_EAGLE_200:
|
||||
user_input[CONF_TYPE] = eagle_type
|
||||
|
||||
# For EAGLE-200, a connected meter's hardware address is required to create the entry
|
||||
if not hardware_address:
|
||||
# hardware_address will be None if there are no meters at all or if none are currently Connected
|
||||
errors["base"] = "no_meters_connected"
|
||||
else:
|
||||
user_input[CONF_HARDWARE_ADDRESS] = hardware_address
|
||||
else:
|
||||
# This is a device that isn't supported, yet, but was detected by async_get_type
|
||||
errors["base"] = "unsupported_device_type"
|
||||
|
||||
# All information gathering is done, so if there are no errors at this point, create the entry
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_CLOUD_ID], data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=create_schema(user_input), errors=errors
|
||||
|
||||
@@ -34,7 +34,7 @@ class InvalidAuth(RainforestError):
|
||||
|
||||
async def async_get_type(hass, cloud_id, install_code, host):
|
||||
"""Try API call 'get_network_info' to see if target device is Eagle-100 or Eagle-200."""
|
||||
# For EAGLE-200, fetch the hardware address of the meter too.
|
||||
# For EAGLE-200, fetch the hardware address of the first connected meter, too.
|
||||
hub = aioeagle.EagleHub(
|
||||
aiohttp_client.async_get_clientsession(hass), cloud_id, install_code, host=host
|
||||
)
|
||||
@@ -50,8 +50,17 @@ async def async_get_type(hass, cloud_id, install_code, host):
|
||||
|
||||
if meters is not None:
|
||||
if meters:
|
||||
hardware_address = meters[0].hardware_address
|
||||
# If there is at least one meter, use the first one with a connection status of "Connected"
|
||||
hardware_address = next(
|
||||
(
|
||||
m.hardware_address
|
||||
for m in meters
|
||||
if getattr(m, "connection_status", None) == "Connected"
|
||||
),
|
||||
None,
|
||||
)
|
||||
else:
|
||||
# If there are no meters (empty list, since None was already checked for), set the hardware address to None
|
||||
hardware_address = None
|
||||
|
||||
return TYPE_EAGLE_200, hardware_address
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
"no_meters_connected": "No meters are currently connected. Ensure your meter is connected and try again.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"unknown_device_type": "Unable to determine the type of Rainforest Eagle device. Please ensure your device is supported.",
|
||||
"unsupported_device_type": "This type of Rainforest Eagle device is not supported."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
|
||||
@@ -8,6 +8,7 @@ from homeassistant.components.rainforest_eagle.const import (
|
||||
CONF_HARDWARE_ADDRESS,
|
||||
CONF_INSTALL_CODE,
|
||||
DOMAIN,
|
||||
TYPE_EAGLE_100,
|
||||
TYPE_EAGLE_200,
|
||||
)
|
||||
from homeassistant.components.rainforest_eagle.data import CannotConnect, InvalidAuth
|
||||
@@ -16,8 +17,8 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant) -> None:
|
||||
"""Test we get the form."""
|
||||
async def test_form_multiple_meters_first_connected(hass: HomeAssistant) -> None:
|
||||
"""Test proper flow with an EAGLE-200 with a list of meters, one of which is connected (should auto-select it)."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
@@ -25,17 +26,29 @@ async def test_form(hass: HomeAssistant) -> None:
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
# Simulate multiple meters with one connected
|
||||
class MockElectricMeter:
|
||||
def __init__(self, hardware_address, connection_status) -> None:
|
||||
self.hardware_address = hardware_address
|
||||
self.connection_status = connection_status
|
||||
|
||||
meters = [
|
||||
MockElectricMeter("meter-1", "Not Joined"),
|
||||
MockElectricMeter("meter-2", "Connected"),
|
||||
MockElectricMeter("meter-3", "Not Joined"),
|
||||
]
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.rainforest_eagle.config_flow.async_get_type",
|
||||
return_value=(TYPE_EAGLE_200, "mock-hw"),
|
||||
"aioeagle.EagleHub.get_device_list",
|
||||
return_value=meters,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.rainforest_eagle.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_CLOUD_ID: "abcdef",
|
||||
@@ -45,18 +58,232 @@ async def test_form(hass: HomeAssistant) -> None:
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "abcdef"
|
||||
assert result2["data"] == {
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "abcdef"
|
||||
assert result["data"] == {
|
||||
CONF_TYPE: TYPE_EAGLE_200,
|
||||
CONF_HOST: "192.168.1.55",
|
||||
CONF_CLOUD_ID: "abcdef",
|
||||
CONF_INSTALL_CODE: "123456",
|
||||
CONF_HARDWARE_ADDRESS: "mock-hw",
|
||||
CONF_HARDWARE_ADDRESS: "meter-2",
|
||||
}
|
||||
assert result["result"].unique_id == "abcdef"
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_eagle_200_meters_none_connected(hass: HomeAssistant) -> None:
|
||||
"""Test proper flow with an EAGLE-200 with a list of meters, but all are disconnected (Error should be shown)."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
# Simulate all meters being disconnected
|
||||
class MockElectricMeter:
|
||||
def __init__(self, hardware_address, connection_status) -> None:
|
||||
self.hardware_address = hardware_address
|
||||
self.connection_status = connection_status
|
||||
|
||||
meters = [
|
||||
MockElectricMeter("meter-1", "Not Joined"),
|
||||
MockElectricMeter("meter-2", "Not Joined"),
|
||||
MockElectricMeter("meter-3", "Not Joined"),
|
||||
]
|
||||
|
||||
with patch(
|
||||
"aioeagle.EagleHub.get_device_list",
|
||||
return_value=meters,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_CLOUD_ID: "abcdef",
|
||||
CONF_INSTALL_CODE: "123456",
|
||||
CONF_HOST: "192.168.1.55",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "no_meters_connected"}
|
||||
|
||||
|
||||
async def test_form_eagle_200_no_meters(hass: HomeAssistant) -> None:
|
||||
"""Test proper flow with an EAGLE-200 with an empty list of meters (Error should be shown)."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
# Simulate no meters (empty list)
|
||||
with (
|
||||
patch(
|
||||
"aioeagle.EagleHub.get_device_list",
|
||||
return_value=[],
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_CLOUD_ID: "abcdef",
|
||||
CONF_INSTALL_CODE: "123456",
|
||||
CONF_HOST: "192.168.1.55",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "no_meters_connected"}
|
||||
|
||||
|
||||
async def test_form_eagle_100(hass: HomeAssistant) -> None:
|
||||
"""Test proper flow for EAGLE-100 (KeyError from get_device_list, then legacy response)."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
# Patch get_device_list to raise KeyError (expected from EAGLE-100), and async_add_executor_job to return proper EAGLE-100 response
|
||||
eagle_100_response = {"NetworkInfo": {"ModelId": "Z109-EAGLE"}}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"aioeagle.EagleHub.get_device_list",
|
||||
side_effect=KeyError,
|
||||
),
|
||||
patch(
|
||||
"eagle100.Eagle.get_network_info",
|
||||
return_value=eagle_100_response,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.core.HomeAssistant.async_add_executor_job",
|
||||
return_value=eagle_100_response,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.rainforest_eagle.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_CLOUD_ID: "abcdef",
|
||||
CONF_INSTALL_CODE: "123456",
|
||||
CONF_HOST: "192.168.1.55",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "abcdef"
|
||||
assert result["data"] == {
|
||||
CONF_TYPE: TYPE_EAGLE_100,
|
||||
CONF_HOST: "192.168.1.55",
|
||||
CONF_CLOUD_ID: "abcdef",
|
||||
CONF_INSTALL_CODE: "123456",
|
||||
CONF_HARDWARE_ADDRESS: None,
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_unknown_device_type(hass: HomeAssistant) -> None:
|
||||
"""Test flow when device type cannot be determined (get_device_list raises an error but other responses aren't the expected values)."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
# Patch get_device_list to raise KeyError (expected from EAGLE-100), and async_add_executor_job to return an unknown device response
|
||||
unknown_device_response = {"NetworkInfo": {"ModelId": "UNKNOWN-DEVICE"}}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"aioeagle.EagleHub.get_device_list",
|
||||
side_effect=KeyError,
|
||||
),
|
||||
patch(
|
||||
"eagle100.Eagle.get_network_info",
|
||||
return_value=unknown_device_response,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.core.HomeAssistant.async_add_executor_job",
|
||||
return_value=unknown_device_response,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_CLOUD_ID: "abcdef",
|
||||
CONF_INSTALL_CODE: "123456",
|
||||
CONF_HOST: "192.168.1.55",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "unknown_device_type"}
|
||||
|
||||
|
||||
async def test_form_unsupported_device_type(hass: HomeAssistant) -> None:
|
||||
"""Test flow when device type is unsupported (async_get_type returns an unexpected device type)."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.rainforest_eagle.config_flow.async_get_type",
|
||||
return_value=("UNSUPPORTED_DEVICE_TYPE", None),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_CLOUD_ID: "abcdef",
|
||||
CONF_INSTALL_CODE: "123456",
|
||||
CONF_HOST: "192.168.1.55",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "unsupported_device_type"}
|
||||
|
||||
|
||||
async def test_form_unexpected_exception(hass: HomeAssistant) -> None:
|
||||
"""Test flow when an unexpected exception occurs."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.rainforest_eagle.config_flow.async_get_type",
|
||||
side_effect=Exception,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_CLOUD_ID: "abcdef",
|
||||
CONF_INSTALL_CODE: "123456",
|
||||
CONF_HOST: "192.168.1.55",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_form_invalid_auth(hass: HomeAssistant) -> None:
|
||||
"""Test we handle invalid auth."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -67,7 +294,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None:
|
||||
"aioeagle.EagleHub.get_device_list",
|
||||
side_effect=InvalidAuth,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_CLOUD_ID: "abcdef",
|
||||
@@ -76,8 +303,8 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None:
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "invalid_auth"}
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
@@ -90,7 +317,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"aioeagle.EagleHub.get_device_list",
|
||||
side_effect=CannotConnect,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_CLOUD_ID: "abcdef",
|
||||
@@ -99,5 +326,5 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
Reference in New Issue
Block a user