From 0a034b99842731ed2a35d0bcd5ab2a8e93687312 Mon Sep 17 00:00:00 2001 From: bestycame Date: Thu, 20 Nov 2025 14:20:14 +0100 Subject: [PATCH] Add Hanna integration (#147085) Co-authored-by: Norbert Rittel Co-authored-by: Olivier d'Otreppe Co-authored-by: Joostlek --- CODEOWNERS | 2 + homeassistant/components/hanna/__init__.py | 54 ++++++++ homeassistant/components/hanna/config_flow.py | 62 +++++++++ homeassistant/components/hanna/const.py | 3 + homeassistant/components/hanna/coordinator.py | 72 ++++++++++ homeassistant/components/hanna/entity.py | 28 ++++ homeassistant/components/hanna/manifest.json | 10 ++ .../components/hanna/quality_scale.yaml | 70 ++++++++++ homeassistant/components/hanna/sensor.py | 106 +++++++++++++++ homeassistant/components/hanna/strings.json | 44 +++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/hanna/__init__.py | 1 + tests/components/hanna/conftest.py | 41 ++++++ tests/components/hanna/test_config_flow.py | 124 ++++++++++++++++++ 17 files changed, 630 insertions(+) create mode 100644 homeassistant/components/hanna/__init__.py create mode 100644 homeassistant/components/hanna/config_flow.py create mode 100644 homeassistant/components/hanna/const.py create mode 100644 homeassistant/components/hanna/coordinator.py create mode 100644 homeassistant/components/hanna/entity.py create mode 100644 homeassistant/components/hanna/manifest.json create mode 100644 homeassistant/components/hanna/quality_scale.yaml create mode 100644 homeassistant/components/hanna/sensor.py create mode 100644 homeassistant/components/hanna/strings.json create mode 100644 tests/components/hanna/__init__.py create mode 100644 tests/components/hanna/conftest.py create mode 100644 tests/components/hanna/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 8f541414a3c..fe7b6628223 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -627,6 +627,8 @@ build.json @home-assistant/supervisor /tests/components/guardian/ @bachya /homeassistant/components/habitica/ @tr4nt0r /tests/components/habitica/ @tr4nt0r +/homeassistant/components/hanna/ @bestycame +/tests/components/hanna/ @bestycame /homeassistant/components/hardkernel/ @home-assistant/core /tests/components/hardkernel/ @home-assistant/core /homeassistant/components/hardware/ @home-assistant/core diff --git a/homeassistant/components/hanna/__init__.py b/homeassistant/components/hanna/__init__.py new file mode 100644 index 00000000000..4d32cfb3942 --- /dev/null +++ b/homeassistant/components/hanna/__init__.py @@ -0,0 +1,54 @@ +"""The Hanna Instruments integration.""" + +from __future__ import annotations + +from typing import Any + +from hanna_cloud import HannaCloudClient + +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform +from homeassistant.core import HomeAssistant + +from .coordinator import HannaConfigEntry, HannaDataCoordinator + +PLATFORMS = [Platform.SENSOR] + + +def _authenticate_and_get_devices( + api_client: HannaCloudClient, + email: str, + password: str, +) -> list[dict[str, Any]]: + """Authenticate and get devices in a single executor job.""" + api_client.authenticate(email, password) + return api_client.get_devices() + + +async def async_setup_entry(hass: HomeAssistant, entry: HannaConfigEntry) -> bool: + """Set up Hanna Instruments from a config entry.""" + api_client = HannaCloudClient() + devices = await hass.async_add_executor_job( + _authenticate_and_get_devices, + api_client, + entry.data[CONF_EMAIL], + entry.data[CONF_PASSWORD], + ) + + # Create device coordinators + device_coordinators = {} + for device in devices: + coordinator = HannaDataCoordinator(hass, entry, device, api_client) + await coordinator.async_config_entry_first_refresh() + device_coordinators[coordinator.device_identifier] = coordinator + + # Set runtime data + entry.runtime_data = device_coordinators + + # Forward the setup to the platforms + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: HannaConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/hanna/config_flow.py b/homeassistant/components/hanna/config_flow.py new file mode 100644 index 00000000000..d1a54dc42cd --- /dev/null +++ b/homeassistant/components/hanna/config_flow.py @@ -0,0 +1,62 @@ +"""Config flow for Hanna Instruments integration.""" + +from __future__ import annotations + +import logging +from typing import Any + +from hanna_cloud import AuthenticationError, HannaCloudClient +from requests.exceptions import ConnectionError as RequestsConnectionError, Timeout +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class HannaConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Hanna Instruments.""" + + VERSION = 1 + data_schema = vol.Schema( + {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str} + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the setup flow.""" + + errors: dict[str, str] = {} + + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_EMAIL]) + self._abort_if_unique_id_configured() + client = HannaCloudClient() + try: + await self.hass.async_add_executor_job( + client.authenticate, + user_input[CONF_EMAIL], + user_input[CONF_PASSWORD], + ) + except (Timeout, RequestsConnectionError): + errors["base"] = "cannot_connect" + except AuthenticationError: + errors["base"] = "invalid_auth" + + if not errors: + return self.async_create_entry( + title=user_input[CONF_EMAIL], + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=self.add_suggested_values_to_schema( + self.data_schema, user_input + ), + errors=errors, + ) diff --git a/homeassistant/components/hanna/const.py b/homeassistant/components/hanna/const.py new file mode 100644 index 00000000000..ed9b5a84831 --- /dev/null +++ b/homeassistant/components/hanna/const.py @@ -0,0 +1,3 @@ +"""Constants for the Hanna integration.""" + +DOMAIN = "hanna" diff --git a/homeassistant/components/hanna/coordinator.py b/homeassistant/components/hanna/coordinator.py new file mode 100644 index 00000000000..c6915610d3f --- /dev/null +++ b/homeassistant/components/hanna/coordinator.py @@ -0,0 +1,72 @@ +"""Hanna Instruments data coordinator for Home Assistant. + +This module provides the data coordinator for fetching and managing Hanna Instruments +sensor data. +""" + +from datetime import timedelta +import logging +from typing import Any + +from hanna_cloud import HannaCloudClient +from requests.exceptions import RequestException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +type HannaConfigEntry = ConfigEntry[dict[str, HannaDataCoordinator]] + +_LOGGER = logging.getLogger(__name__) + + +class HannaDataCoordinator(DataUpdateCoordinator[dict[str, Any]]): + """Coordinator for fetching Hanna sensor data.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: HannaConfigEntry, + device: dict[str, Any], + api_client: HannaCloudClient, + ) -> None: + """Initialize the Hanna data coordinator.""" + self.api_client = api_client + self.device_data = device + super().__init__( + hass, + _LOGGER, + name=f"{DOMAIN}_{self.device_identifier}", + config_entry=config_entry, + update_interval=timedelta(seconds=30), + ) + + @property + def device_identifier(self) -> str: + """Return the device identifier.""" + return self.device_data["DID"] + + def get_parameters(self) -> list[dict[str, Any]]: + """Get all parameters from the sensor data.""" + return self.api_client.parameters + + def get_parameter_value(self, key: str) -> Any: + """Get the value for a specific parameter.""" + for parameter in self.get_parameters(): + if parameter["name"] == key: + return parameter["value"] + return None + + async def _async_update_data(self) -> dict[str, Any]: + """Fetch latest sensor data from the Hanna API.""" + try: + readings = await self.hass.async_add_executor_job( + self.api_client.get_last_device_reading, self.device_identifier + ) + except RequestException as e: + raise UpdateFailed(f"Error communicating with Hanna API: {e}") from e + except (KeyError, IndexError) as e: + raise UpdateFailed(f"Error parsing Hanna API response: {e}") from e + return readings diff --git a/homeassistant/components/hanna/entity.py b/homeassistant/components/hanna/entity.py new file mode 100644 index 00000000000..3de5723583a --- /dev/null +++ b/homeassistant/components/hanna/entity.py @@ -0,0 +1,28 @@ +"""Hanna Instruments entity base class for Home Assistant. + +This module provides the base entity class for Hanna Instruments entities. +""" + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import HannaDataCoordinator + + +class HannaEntity(CoordinatorEntity[HannaDataCoordinator]): + """Base class for Hanna entities.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: HannaDataCoordinator) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.device_identifier)}, + manufacturer=coordinator.device_data.get("manufacturer"), + model=coordinator.device_data.get("DM"), + name=coordinator.device_data.get("name"), + serial_number=coordinator.device_data.get("serial_number"), + sw_version=coordinator.device_data.get("sw_version"), + ) diff --git a/homeassistant/components/hanna/manifest.json b/homeassistant/components/hanna/manifest.json new file mode 100644 index 00000000000..b1e503e5e28 --- /dev/null +++ b/homeassistant/components/hanna/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "hanna", + "name": "Hanna", + "codeowners": ["@bestycame"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/hanna", + "iot_class": "cloud_polling", + "quality_scale": "bronze", + "requirements": ["hanna-cloud==0.0.6"] +} diff --git a/homeassistant/components/hanna/quality_scale.yaml b/homeassistant/components/hanna/quality_scale.yaml new file mode 100644 index 00000000000..f4eb96842e6 --- /dev/null +++ b/homeassistant/components/hanna/quality_scale.yaml @@ -0,0 +1,70 @@ +rules: + # Bronze + action-setup: + status: exempt + comment: | + This integration doesn't add actions. + appropriate-polling: + status: done + brands: done + common-modules: done + config-flow-test-coverage: done + config-flow: done + dependency-transparency: done + docs-actions: done + docs-high-level-description: done + docs-installation-instructions: done + docs-removal-instructions: done + entity-event-setup: + status: exempt + comment: | + Entities of this integration does not explicitly subscribe to events. + entity-unique-id: done + has-entity-name: done + runtime-data: done + test-before-configure: done + test-before-setup: done + unique-config-entry: done + + # Silver + action-exceptions: todo + config-entry-unloading: done + docs-configuration-parameters: + status: exempt + comment: | + This integration does not have any configuration parameters. + docs-installation-parameters: done + entity-unavailable: todo + integration-owner: done + log-when-unavailable: todo + parallel-updates: todo + reauthentication-flow: todo + test-coverage: todo + + # Gold + devices: done + diagnostics: todo + discovery-update-info: todo + discovery: todo + docs-data-update: done + docs-examples: todo + docs-known-limitations: todo + docs-supported-devices: done + docs-supported-functions: done + docs-troubleshooting: todo + docs-use-cases: todo + dynamic-devices: todo + entity-category: todo + entity-device-class: done + entity-disabled-by-default: todo + entity-translations: done + exception-translations: todo + icon-translations: todo + reconfiguration-flow: todo + repair-issues: todo + stale-devices: todo + + # Platinum + async-dependency: todo + inject-websession: todo + strict-typing: todo diff --git a/homeassistant/components/hanna/sensor.py b/homeassistant/components/hanna/sensor.py new file mode 100644 index 00000000000..6845f1a7c10 --- /dev/null +++ b/homeassistant/components/hanna/sensor.py @@ -0,0 +1,106 @@ +"""Hanna Instruments sensor integration for Home Assistant. + +This module provides sensor entities for various Hanna Instruments devices, +including pH, ORP, temperature, and chemical sensors. It uses the Hanna API +to fetch readings and updates them periodically. +""" + +from __future__ import annotations + +import logging + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import UnitOfElectricPotential, UnitOfTemperature, UnitOfVolume +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .coordinator import HannaConfigEntry, HannaDataCoordinator +from .entity import HannaEntity + +_LOGGER = logging.getLogger(__name__) + +SENSOR_DESCRIPTIONS = [ + SensorEntityDescription( + key="ph", + translation_key="ph_value", + device_class=SensorDeviceClass.PH, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="orp", + translation_key="chlorine_orp_value", + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="temp", + translation_key="water_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="airTemp", + translation_key="air_temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="acidBase", + translation_key="ph_acid_base_flow_rate", + icon="mdi:chemical-weapon", + device_class=SensorDeviceClass.VOLUME, + native_unit_of_measurement=UnitOfVolume.MILLILITERS, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="cl", + translation_key="chlorine_flow_rate", + icon="mdi:chemical-weapon", + device_class=SensorDeviceClass.VOLUME, + native_unit_of_measurement=UnitOfVolume.MILLILITERS, + state_class=SensorStateClass.MEASUREMENT, + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + entry: HannaConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Hanna sensors from a config entry.""" + device_coordinators = entry.runtime_data + + async_add_entities( + HannaSensor(coordinator, description) + for description in SENSOR_DESCRIPTIONS + for coordinator in device_coordinators.values() + ) + + +class HannaSensor(HannaEntity, SensorEntity): + """Representation of a Hanna sensor.""" + + def __init__( + self, + coordinator: HannaDataCoordinator, + description: SensorEntityDescription, + ) -> None: + """Initialize a Hanna sensor.""" + super().__init__(coordinator) + self._attr_unique_id = f"{coordinator.device_identifier}_{description.key}" + self.entity_description = description + + @property + def native_value(self) -> StateType: + """Return the value reported by the sensor.""" + return self.coordinator.get_parameter_value(self.entity_description.key) diff --git a/homeassistant/components/hanna/strings.json b/homeassistant/components/hanna/strings.json new file mode 100644 index 00000000000..c94a284bde9 --- /dev/null +++ b/homeassistant/components/hanna/strings.json @@ -0,0 +1,44 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "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%]" + }, + "step": { + "user": { + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "email": "Email address for your Hanna Cloud account", + "password": "Password for your Hanna Cloud account" + }, + "description": "Enter your Hanna Cloud credentials" + } + } + }, + "entity": { + "sensor": { + "air_temperature": { + "name": "Air temperature" + }, + "chlorine_flow_rate": { + "name": "Chlorine flow rate" + }, + "chlorine_orp_value": { + "name": "Chlorine ORP value" + }, + "ph_acid_base_flow_rate": { + "name": "pH Acid/Base flow rate" + }, + "water_temperature": { + "name": "Water temperature" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 0d697fd3763..e46f92a50a7 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -264,6 +264,7 @@ FLOWS = { "growatt_server", "guardian", "habitica", + "hanna", "harmony", "heos", "here_travel_time", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 3256a7019eb..0f5961e0f3d 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2576,6 +2576,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "hanna": { + "name": "Hanna", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "hardkernel": { "name": "Hardkernel", "integration_type": "hardware", diff --git a/requirements_all.txt b/requirements_all.txt index 39b54de9e1f..ec8d88827bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1143,6 +1143,9 @@ habiticalib==0.4.6 # homeassistant.components.bluetooth habluetooth==5.7.0 +# homeassistant.components.hanna +hanna-cloud==0.0.6 + # homeassistant.components.cloud hass-nabucasa==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6eedb41767..26f247f366f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1004,6 +1004,9 @@ habiticalib==0.4.6 # homeassistant.components.bluetooth habluetooth==5.7.0 +# homeassistant.components.hanna +hanna-cloud==0.0.6 + # homeassistant.components.cloud hass-nabucasa==1.5.1 diff --git a/tests/components/hanna/__init__.py b/tests/components/hanna/__init__.py new file mode 100644 index 00000000000..47a9b1d5aab --- /dev/null +++ b/tests/components/hanna/__init__.py @@ -0,0 +1 @@ +"""Tests for the Hanna integration.""" diff --git a/tests/components/hanna/conftest.py b/tests/components/hanna/conftest.py new file mode 100644 index 00000000000..4585325ba8b --- /dev/null +++ b/tests/components/hanna/conftest.py @@ -0,0 +1,41 @@ +"""Fixtures for Hanna Instruments integration tests.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.hanna.const import DOMAIN +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry(): + """Mock setting up a config entry.""" + with patch("homeassistant.components.hanna.async_setup_entry", return_value=True): + yield + + +@pytest.fixture +def mock_hanna_client(): + """Mock HannaCloudClient.""" + with ( + patch( + "homeassistant.components.hanna.config_flow.HannaCloudClient", autospec=True + ) as mock_client, + patch("homeassistant.components.hanna.HannaCloudClient", new=mock_client), + ): + client = mock_client.return_value + yield client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + data={CONF_EMAIL: "test@example.com", CONF_PASSWORD: "test-password"}, + title="test@example.com", + unique_id="test@example.com", + ) diff --git a/tests/components/hanna/test_config_flow.py b/tests/components/hanna/test_config_flow.py new file mode 100644 index 00000000000..ed3c682b49e --- /dev/null +++ b/tests/components/hanna/test_config_flow.py @@ -0,0 +1,124 @@ +"""Tests for the Hanna Instruments integration config flow.""" + +from unittest.mock import AsyncMock, MagicMock + +from hanna_cloud import AuthenticationError +import pytest +from requests.exceptions import ConnectionError as RequestsConnectionError, Timeout + +from homeassistant.components.hanna.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + + +async def test_full_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_hanna_client: MagicMock, +) -> None: + """Test full flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "test@example.com", + CONF_PASSWORD: "test-password", + }, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "test@example.com" + assert result["data"] == { + CONF_EMAIL: "test@example.com", + CONF_PASSWORD: "test-password", + } + assert result["result"].unique_id == "test@example.com" + + +@pytest.mark.parametrize( + ("exception", "expected_error"), + [ + ( + AuthenticationError("Authentication failed"), + "invalid_auth", + ), + ( + Timeout("Connection timeout"), + "cannot_connect", + ), + ( + RequestsConnectionError("Connection failed"), + "cannot_connect", + ), + ], +) +async def test_error_scenarios( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_hanna_client: MagicMock, + exception: Exception, + expected_error: str, +) -> None: + """Test various error scenarios in the config flow.""" + mock_hanna_client.authenticate.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_EMAIL: "test@example.com", CONF_PASSWORD: "test-password"}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": expected_error} + + # Repatch to succeed and complete the flow + mock_hanna_client.authenticate.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_EMAIL: "test@example.com", CONF_PASSWORD: "test-password"}, + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "test@example.com" + assert result["data"] == { + CONF_EMAIL: "test@example.com", + CONF_PASSWORD: "test-password", + } + + +async def test_duplicate_entry( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_hanna_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that duplicate entries are aborted.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_EMAIL: "test@example.com", CONF_PASSWORD: "test-password"}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured"