Add Fyta integration (#110816)

* Initial commit for fyta integration

* Update __init__.py

Delete BinarySensor for first PR

* Update __init__.py

Rewind wrongful deletion of comma

* Delete homeassistant/components/fyta/binary_sensor.py

Delete binary_sensor for first pr of integration

* Update manifest.json

Updated requirement to new version of fyta_cli 0.2.1, where bug in import of modules has been resolved.

* Update requirements_test_all.txt

adjust to updated manifest

* Update requirements_all.txt

adjust to updated manifest

* Update test_config_flow.py

* Update config_flow.py

update file to correct error with _entry attribute

* Fyta integration - update initial PR based on review in initial PR #110816 (#2)

* adjustments to pass test for config_flow

* backport of changes in intitial PR to dev

* update text_config_flow

* changes based on review in initial PR #110816

* Update homeassistant/components/fyta/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/config_flow.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/config_flow.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/coordinator.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/config_flow.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/strings.json

Co-authored-by: Sid <27780930+autinerd@users.noreply.github.com>

* Update homeassistant/components/fyta/strings.json

Co-authored-by: Sid <27780930+autinerd@users.noreply.github.com>

* Update homeassistant/components/fyta/manifest.json

Co-authored-by: Sid <27780930+autinerd@users.noreply.github.com>

* Adjustments based on PR-commet of Feb 19 (#3)

* add test for config_flow.validate_input

* update based on pr review

* update based on pr review

* further refinings based on PR review

* Update tests/components/fyta/test_config_flow.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update test_config_flow.py

Update tests based on PR comment

* Update homeassistant/components/fyta/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* add handling and test for duplicate entry

* Update homeassistant/components/fyta/coordinator.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/entity.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update test_config_flow.py

parametrize test for exceptions

* Update config_flow.py

Move _async_abort_entries_match, add arguments

* Update coordinator.py

* Update typing in coordinator.py

* Update coordinator.py

update typing

* Update coordinator.py

corrected typo

* Update coordinator.py

* Update entity.py

* Update sensor.py

* Update icons.json

* Update homeassistant/components/fyta/entity.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/entity.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update entity.py

* Update test_config_flow.py

* Update config_flow.py (change FlowResult to ConfigFlowResult)

* Update config_flow.py

* Update homeassistant/components/fyta/config_flow.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update homeassistant/components/fyta/config_flow.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update homeassistant/components/fyta/coordinator.py

Co-authored-by: Robert Resch <robert@resch.dev>

* Update coordinator.py

* Update config_flow.py (typing FlowResult -> ConfigFlowResult)

* Update config_flow.py

* Aktualisieren von config_flow.py

* remove coordinator entities

* Update strings.json

remove plant_number

* Update icons.json

remove plant_number

* Update manifest.json

Update requirement to latest fyta_cli version

* Update requirements_all.txt

* Update requirements_test_all.txt

* Update homeassistant/components/fyta/sensor.py

* Update homeassistant/components/fyta/sensor.py

* Update homeassistant/components/fyta/coordinator.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/coordinator.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/coordinator.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/entity.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/strings.json

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/fyta/strings.json

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update tests/components/fyta/test_config_flow.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update tests/components/fyta/test_config_flow.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* move test-helpers into conftest.py, adjust import of coordinator.py

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Sid <27780930+autinerd@users.noreply.github.com>
Co-authored-by: Robert Resch <robert@resch.dev>
This commit is contained in:
dontinelli
2024-03-15 18:13:35 +01:00
committed by GitHub
parent cfc2f17f35
commit 98132d1cd3
18 changed files with 652 additions and 0 deletions

View File

@ -461,6 +461,10 @@ omit =
homeassistant/components/frontier_silicon/browse_media.py
homeassistant/components/frontier_silicon/media_player.py
homeassistant/components/futurenow/light.py
homeassistant/components/fyta/__init__.py
homeassistant/components/fyta/coordinator.py
homeassistant/components/fyta/entity.py
homeassistant/components/fyta/sensor.py
homeassistant/components/garadget/cover.py
homeassistant/components/garages_amsterdam/__init__.py
homeassistant/components/garages_amsterdam/binary_sensor.py

View File

@ -453,6 +453,8 @@ build.json @home-assistant/supervisor
/tests/components/frontier_silicon/ @wlcrs
/homeassistant/components/fully_kiosk/ @cgarwood
/tests/components/fully_kiosk/ @cgarwood
/homeassistant/components/fyta/ @dontinelli
/tests/components/fyta/ @dontinelli
/homeassistant/components/garages_amsterdam/ @klaasnicolaas
/tests/components/garages_amsterdam/ @klaasnicolaas
/homeassistant/components/gardena_bluetooth/ @elupus

View File

@ -0,0 +1,48 @@
"""Initialization of FYTA integration."""
from __future__ import annotations
import logging
from fyta_cli.fyta_connector import FytaConnector
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import FytaCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.SENSOR,
]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the Fyta integration."""
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
fyta = FytaConnector(username, password)
coordinator = FytaCoordinator(hass, fyta)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Fyta entity."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -0,0 +1,64 @@
"""Config flow for FYTA integration."""
from __future__ import annotations
import logging
from typing import Any
from fyta_cli.fyta_connector import FytaConnector
from fyta_cli.fyta_exceptions import (
FytaAuthentificationError,
FytaConnectionError,
FytaPasswordError,
)
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
)
class FytaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Fyta."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Handle the initial step."""
errors = {}
if user_input:
self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]})
fyta = FytaConnector(user_input[CONF_USERNAME], user_input[CONF_PASSWORD])
try:
await fyta.login()
except FytaConnectionError:
errors["base"] = "cannot_connect"
except FytaAuthentificationError:
errors["base"] = "invalid_auth"
except FytaPasswordError:
errors["base"] = "invalid_auth"
errors[CONF_PASSWORD] = "password_error"
except Exception: # pylint: disable=broad-except
errors["base"] = "unknown"
else:
return self.async_create_entry(
title=user_input[CONF_USERNAME], data=user_input
)
finally:
await fyta.client.close()
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)

View File

@ -0,0 +1,2 @@
"""Const for fyta integration."""
DOMAIN = "fyta"

View File

@ -0,0 +1,55 @@
"""Coordinator for FYTA integration."""
from datetime import datetime, timedelta
import logging
from typing import Any
from fyta_cli.fyta_connector import FytaConnector
from fyta_cli.fyta_exceptions import (
FytaAuthentificationError,
FytaConnectionError,
FytaPasswordError,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
class FytaCoordinator(DataUpdateCoordinator[dict[int, dict[str, Any]]]):
"""Fyta custom coordinator."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, fyta: FytaConnector) -> None:
"""Initialize my coordinator."""
super().__init__(
hass,
_LOGGER,
name="FYTA Coordinator",
update_interval=timedelta(seconds=60),
)
self.fyta = fyta
async def _async_update_data(
self,
) -> dict[int, dict[str, Any]]:
"""Fetch data from API endpoint."""
if self.fyta.expiration is None or self.fyta.expiration < datetime.now():
await self.renew_authentication()
return await self.fyta.update_all_plants()
async def renew_authentication(self) -> None:
"""Renew access token for FYTA API."""
try:
await self.fyta.login()
except FytaConnectionError as ex:
raise ConfigEntryNotReady from ex
except (FytaAuthentificationError, FytaPasswordError) as ex:
raise ConfigEntryError from ex

View File

@ -0,0 +1,47 @@
"""Entities for FYTA integration."""
from typing import Any
from homeassistant.components.sensor import SensorEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import FytaCoordinator
class FytaPlantEntity(CoordinatorEntity[FytaCoordinator]):
"""Base Fyta Plant entity."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: FytaCoordinator,
entry: ConfigEntry,
description: SensorEntityDescription,
plant_id: int,
) -> None:
"""Initialize the Fyta sensor."""
super().__init__(coordinator)
self.plant_id = plant_id
self._attr_unique_id = f"{entry.entry_id}-{plant_id}-{description.key}"
self._attr_device_info = DeviceInfo(
manufacturer="Fyta",
model="Plant",
identifiers={(DOMAIN, f"{entry.entry_id}-{plant_id}")},
name=self.plant.get("name"),
sw_version=self.plant.get("sw_version"),
)
self.entity_description = description
@property
def plant(self) -> dict[str, Any]:
"""Get plant data."""
return self.coordinator.data[self.plant_id]
@property
def available(self) -> bool:
"""Test if entity is available."""
return super().available and self.plant_id in self.coordinator.data

View File

@ -0,0 +1,27 @@
{
"entity": {
"sensor": {
"status": {
"default": "mdi:flower"
},
"temperature_status": {
"default": "mdi:thermometer-lines"
},
"light_status": {
"default": "mdi:sun-clock-outline"
},
"moisture_status": {
"default": "mdi:water-percent-alert"
},
"salinity_status": {
"default": "mdi:sprout-outline"
},
"light": {
"default": "mdi:weather-sunny"
},
"salinity": {
"default": "mdi:sprout-outline"
}
}
}
}

View File

@ -0,0 +1,10 @@
{
"domain": "fyta",
"name": "FYTA",
"codeowners": ["@dontinelli"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/fyta",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": ["fyta_cli==0.3.3"]
}

View File

@ -0,0 +1,143 @@
"""Summary data from Fyta."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from typing import Final
from fyta_cli.fyta_connector import PLANT_STATUS
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import FytaCoordinator
from .entity import FytaPlantEntity
@dataclass(frozen=True)
class FytaSensorEntityDescription(SensorEntityDescription):
"""Describes Fyta sensor entity."""
value_fn: Callable[[str | int | float | datetime], str | int | float | datetime] = (
lambda value: value
)
PLANT_STATUS_LIST: list[str] = ["too_low", "low", "perfect", "high", "too_high"]
SENSORS: Final[list[FytaSensorEntityDescription]] = [
FytaSensorEntityDescription(
key="scientific_name",
translation_key="scientific_name",
),
FytaSensorEntityDescription(
key="status",
translation_key="plant_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_STATUS_LIST,
value_fn=lambda value: PLANT_STATUS[value],
),
FytaSensorEntityDescription(
key="temperature_status",
translation_key="temperature_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_STATUS_LIST,
value_fn=lambda value: PLANT_STATUS[value],
),
FytaSensorEntityDescription(
key="light_status",
translation_key="light_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_STATUS_LIST,
value_fn=lambda value: PLANT_STATUS[value],
),
FytaSensorEntityDescription(
key="moisture_status",
translation_key="moisture_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_STATUS_LIST,
value_fn=lambda value: PLANT_STATUS[value],
),
FytaSensorEntityDescription(
key="salinity_status",
translation_key="salinity_status",
device_class=SensorDeviceClass.ENUM,
options=PLANT_STATUS_LIST,
value_fn=lambda value: PLANT_STATUS[value],
),
FytaSensorEntityDescription(
key="temperature",
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
),
FytaSensorEntityDescription(
key="light",
translation_key="light",
native_unit_of_measurement="mol/d",
state_class=SensorStateClass.MEASUREMENT,
),
FytaSensorEntityDescription(
key="moisture",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.MOISTURE,
state_class=SensorStateClass.MEASUREMENT,
),
FytaSensorEntityDescription(
key="salinity",
translation_key="salinity",
native_unit_of_measurement="mS/cm",
state_class=SensorStateClass.MEASUREMENT,
),
FytaSensorEntityDescription(
key="ph",
device_class=SensorDeviceClass.PH,
state_class=SensorStateClass.MEASUREMENT,
),
FytaSensorEntityDescription(
key="battery_level",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
]
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the FYTA sensors."""
coordinator: FytaCoordinator = hass.data[DOMAIN][entry.entry_id]
plant_entities = [
FytaPlantSensor(coordinator, entry, sensor, plant_id)
for plant_id in coordinator.fyta.plant_list
for sensor in SENSORS
if sensor.key in coordinator.data[plant_id]
]
async_add_entities(plant_entities)
class FytaPlantSensor(FytaPlantEntity, SensorEntity):
"""Represents a Fyta sensor."""
entity_description: FytaSensorEntityDescription
@property
def native_value(self) -> str | int | float | datetime:
"""Return the state for this sensor."""
val = self.plant[self.entity_description.key]
return self.entity_description.value_fn(val)

View File

@ -0,0 +1,83 @@
{
"config": {
"step": {
"user": {
"title": "Credentials for FYTA API",
"description": "Provide username and password to connect to the FYTA server",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"password_error": "Invalid password",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"entity": {
"sensor": {
"scientific_name": {
"name": "Scientific name"
},
"plant_status": {
"name": "Plant state",
"state": {
"too_low": "Too low",
"low": "Low",
"perfect": "Perfect",
"high": "High",
"too_high": "Too high"
}
},
"temperature_status": {
"name": "Temperature state",
"state": {
"too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]",
"low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]",
"perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]",
"high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]",
"too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]"
}
},
"light_status": {
"name": "Light state",
"state": {
"too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]",
"low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]",
"perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]",
"high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]",
"too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]"
}
},
"moisture_status": {
"name": "Moisture state",
"state": {
"too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]",
"low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]",
"perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]",
"high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]",
"too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]"
}
},
"salinity_status": {
"name": "Salinity state",
"state": {
"too_low": "[%key:component::fyta::entity::sensor::plant_status::state::too_low%]",
"low": "[%key:component::fyta::entity::sensor::plant_status::state::low%]",
"perfect": "[%key:component::fyta::entity::sensor::plant_status::state::perfect%]",
"high": "[%key:component::fyta::entity::sensor::plant_status::state::high%]",
"too_high": "[%key:component::fyta::entity::sensor::plant_status::state::too_high%]"
}
},
"light": {
"name": "Light"
},
"salinity": {
"name": "Salinity"
}
}
}
}

View File

@ -179,6 +179,7 @@ FLOWS = {
"fronius",
"frontier_silicon",
"fully_kiosk",
"fyta",
"garages_amsterdam",
"gardena_bluetooth",
"gdacs",

View File

@ -2049,6 +2049,12 @@
"config_flow": false,
"iot_class": "local_polling"
},
"fyta": {
"name": "FYTA",
"integration_type": "hub",
"config_flow": true,
"iot_class": "cloud_polling"
},
"garadget": {
"name": "Garadget",
"integration_type": "hub",

View File

@ -895,6 +895,9 @@ freesms==0.2.0
# homeassistant.components.fritzbox_callmonitor
fritzconnection[qr]==1.13.2
# homeassistant.components.fyta
fyta_cli==0.3.3
# homeassistant.components.google_translate
gTTS==2.2.4

View File

@ -727,6 +727,9 @@ freebox-api==1.1.0
# homeassistant.components.fritzbox_callmonitor
fritzconnection[qr]==1.13.2
# homeassistant.components.fyta
fyta_cli==0.3.3
# homeassistant.components.google_translate
gTTS==2.2.4

View File

@ -0,0 +1 @@
"""Tests for the Fyta integration."""

View File

@ -0,0 +1,32 @@
"""Test helpers."""
from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
from .test_config_flow import ACCESS_TOKEN, EXPIRATION
@pytest.fixture
def mock_fyta():
"""Build a fixture for the Fyta API that connects successfully and returns one device."""
mock_fyta_api = AsyncMock()
with patch(
"homeassistant.components.fyta.config_flow.FytaConnector",
return_value=mock_fyta_api,
) as mock_fyta_api:
mock_fyta_api.return_value.login.return_value = {
"access_token": ACCESS_TOKEN,
"expiration": EXPIRATION,
}
yield mock_fyta_api
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
"""Override async_setup_entry."""
with patch(
"homeassistant.components.fyta.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry

View File

@ -0,0 +1,121 @@
"""Test the fyta config flow."""
from datetime import datetime
from unittest.mock import AsyncMock
from fyta_cli.fyta_exceptions import (
FytaAuthentificationError,
FytaConnectionError,
FytaPasswordError,
)
import pytest
from homeassistant import config_entries, data_entry_flow
from homeassistant.components.fyta.const import DOMAIN
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
USERNAME = "fyta_user"
PASSWORD = "fyta_pass"
ACCESS_TOKEN = "123xyz"
EXPIRATION = datetime.now()
async def test_user_flow(
hass: HomeAssistant, mock_fyta: AsyncMock, mock_setup_entry
) -> None:
"""Test we get the form."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["errors"] == {}
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == USERNAME
assert result2["data"] == {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.parametrize(
("exception", "error"),
[
(FytaConnectionError, {"base": "cannot_connect"}),
(FytaAuthentificationError, {"base": "invalid_auth"}),
(FytaPasswordError, {"base": "invalid_auth", CONF_PASSWORD: "password_error"}),
(Exception, {"base": "unknown"}),
],
)
async def test_form_exceptions(
hass: HomeAssistant,
exception: Exception,
error: dict[str, str],
mock_fyta: AsyncMock,
mock_setup_entry,
) -> None:
"""Test we can handle Form exceptions."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
mock_fyta.return_value.login.side_effect = exception
# tests with connection error
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == error
mock_fyta.return_value.login.side_effect = None
# tests with all information provided
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD}
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
assert result["title"] == USERNAME
assert result["data"][CONF_USERNAME] == USERNAME
assert result["data"][CONF_PASSWORD] == PASSWORD
assert len(mock_setup_entry.mock_calls) == 1
async def test_duplicate_entry(hass: HomeAssistant, mock_fyta: AsyncMock) -> None:
"""Test duplicate setup handling."""
entry = MockConfigEntry(
domain=DOMAIN,
title=USERNAME,
data={CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == data_entry_flow.FlowResultType.FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD},
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.FlowResultType.ABORT
assert result["reason"] == "already_configured"