diff --git a/.strict-typing b/.strict-typing index 7d6bd1286af..1707f0ca9c3 100644 --- a/.strict-typing +++ b/.strict-typing @@ -97,6 +97,7 @@ homeassistant.components.assist_pipeline.* homeassistant.components.asterisk_cdr.* homeassistant.components.asterisk_mbox.* homeassistant.components.asuswrt.* +homeassistant.components.autarco.* homeassistant.components.auth.* homeassistant.components.automation.* homeassistant.components.awair.* diff --git a/CODEOWNERS b/CODEOWNERS index 3eda36c247c..ac49e36d9ec 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -155,6 +155,8 @@ build.json @home-assistant/supervisor /tests/components/aurora_abb_powerone/ @davet2001 /homeassistant/components/aussie_broadband/ @nickw444 @Bre77 /tests/components/aussie_broadband/ @nickw444 @Bre77 +/homeassistant/components/autarco/ @klaasnicolaas +/tests/components/autarco/ @klaasnicolaas /homeassistant/components/auth/ @home-assistant/core /tests/components/auth/ @home-assistant/core /homeassistant/components/automation/ @home-assistant/core diff --git a/homeassistant/components/autarco/__init__.py b/homeassistant/components/autarco/__init__.py new file mode 100644 index 00000000000..0e29b25ad80 --- /dev/null +++ b/homeassistant/components/autarco/__init__.py @@ -0,0 +1,49 @@ +"""The Autarco integration.""" + +from __future__ import annotations + +import asyncio + +from autarco import Autarco + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .coordinator import AutarcoDataUpdateCoordinator + +PLATFORMS: list[Platform] = [Platform.SENSOR] + +type AutarcoConfigEntry = ConfigEntry[list[AutarcoDataUpdateCoordinator]] + + +async def async_setup_entry(hass: HomeAssistant, entry: AutarcoConfigEntry) -> bool: + """Set up Autarco from a config entry.""" + client = Autarco( + email=entry.data[CONF_EMAIL], + password=entry.data[CONF_PASSWORD], + session=async_get_clientsession(hass), + ) + account_sites = await client.get_account() + + coordinators: list[AutarcoDataUpdateCoordinator] = [ + AutarcoDataUpdateCoordinator(hass, client, site) for site in account_sites + ] + + await asyncio.gather( + *[ + coordinator.async_config_entry_first_refresh() + for coordinator in coordinators + ] + ) + + entry.runtime_data = coordinators + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: AutarcoConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/autarco/config_flow.py b/homeassistant/components/autarco/config_flow.py new file mode 100644 index 00000000000..a66f14047a7 --- /dev/null +++ b/homeassistant/components/autarco/config_flow.py @@ -0,0 +1,57 @@ +"""Config flow for Autarco integration.""" + +from __future__ import annotations + +from typing import Any + +from autarco import Autarco, AutarcoAuthenticationError, AutarcoConnectionError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_EMAIL): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +class AutarcoConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Autarco.""" + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]}) + client = Autarco( + email=user_input[CONF_EMAIL], + password=user_input[CONF_PASSWORD], + session=async_get_clientsession(self.hass), + ) + try: + await client.get_account() + except AutarcoAuthenticationError: + errors["base"] = "invalid_auth" + except AutarcoConnectionError: + errors["base"] = "cannot_connect" + else: + return self.async_create_entry( + title=user_input[CONF_EMAIL], + data={ + CONF_EMAIL: user_input[CONF_EMAIL], + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + return self.async_show_form( + step_id="user", + errors=errors, + data_schema=DATA_SCHEMA, + ) diff --git a/homeassistant/components/autarco/const.py b/homeassistant/components/autarco/const.py new file mode 100644 index 00000000000..4f7f98533de --- /dev/null +++ b/homeassistant/components/autarco/const.py @@ -0,0 +1,13 @@ +"""Constants for the Autarco integration.""" + +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Final + +DOMAIN: Final = "autarco" +LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = timedelta(minutes=5) + +SENSORS_SOLAR: Final = "solar" diff --git a/homeassistant/components/autarco/coordinator.py b/homeassistant/components/autarco/coordinator.py new file mode 100644 index 00000000000..22055438f1d --- /dev/null +++ b/homeassistant/components/autarco/coordinator.py @@ -0,0 +1,47 @@ +"""Coordinator for Autarco integration.""" + +from __future__ import annotations + +from typing import NamedTuple + +from autarco import AccountSite, Autarco, Solar + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, LOGGER, SCAN_INTERVAL + + +class AutarcoData(NamedTuple): + """Class for defining data in dict.""" + + solar: Solar + + +class AutarcoDataUpdateCoordinator(DataUpdateCoordinator[AutarcoData]): + """Class to manage fetching Autarco data from the API.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + client: Autarco, + site: AccountSite, + ) -> None: + """Initialize global Autarco data updater.""" + super().__init__( + hass, + LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + self.client = client + self.site = site + + async def _async_update_data(self) -> AutarcoData: + """Fetch data from Autarco API.""" + return AutarcoData( + solar=await self.client.get_solar(self.site.public_key), + ) diff --git a/homeassistant/components/autarco/manifest.json b/homeassistant/components/autarco/manifest.json new file mode 100644 index 00000000000..f0900472b1e --- /dev/null +++ b/homeassistant/components/autarco/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "autarco", + "name": "Autarco", + "codeowners": ["@klaasnicolaas"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/autarco", + "iot_class": "cloud_polling", + "requirements": ["autarco==2.0.0"] +} diff --git a/homeassistant/components/autarco/sensor.py b/homeassistant/components/autarco/sensor.py new file mode 100644 index 00000000000..32d93c7226d --- /dev/null +++ b/homeassistant/components/autarco/sensor.py @@ -0,0 +1,113 @@ +"""Support for Autarco sensors.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from autarco import Solar + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import UnitOfEnergy, UnitOfPower +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 AutarcoConfigEntry +from .const import DOMAIN +from .coordinator import AutarcoDataUpdateCoordinator + + +@dataclass(frozen=True, kw_only=True) +class AutarcoSolarSensorEntityDescription(SensorEntityDescription): + """Describes an Autarco sensor entity.""" + + state: Callable[[Solar], StateType] + + +SENSORS_SOLAR: tuple[AutarcoSolarSensorEntityDescription, ...] = ( + AutarcoSolarSensorEntityDescription( + key="power_production", + translation_key="power_production", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + state=lambda solar: solar.power_production, + ), + AutarcoSolarSensorEntityDescription( + key="energy_production_today", + translation_key="energy_production_today", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state=lambda solar: solar.energy_production_today, + ), + AutarcoSolarSensorEntityDescription( + key="energy_production_month", + translation_key="energy_production_month", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state=lambda solar: solar.energy_production_month, + ), + AutarcoSolarSensorEntityDescription( + key="energy_production_total", + translation_key="energy_production_total", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state=lambda solar: solar.energy_production_total, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AutarcoConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Autarco sensors based on a config entry.""" + for coordinator in entry.runtime_data: + async_add_entities( + AutarcoSolarSensorEntity( + coordinator=coordinator, + description=description, + ) + for description in SENSORS_SOLAR + ) + + +class AutarcoSolarSensorEntity( + CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity +): + """Defines an Autarco solar sensor.""" + + entity_description: AutarcoSolarSensorEntityDescription + _attr_has_entity_name = True + + def __init__( + self, + *, + coordinator: AutarcoDataUpdateCoordinator, + description: AutarcoSolarSensorEntityDescription, + ) -> None: + """Initialize Autarco sensor.""" + super().__init__(coordinator) + + self.entity_description = description + self._attr_unique_id = f"{coordinator.site.site_id}_solar_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{coordinator.site.site_id}_solar")}, + entry_type=DeviceEntryType.SERVICE, + manufacturer="Autarco", + name="Solar", + ) + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.state(self.coordinator.data.solar) diff --git a/homeassistant/components/autarco/strings.json b/homeassistant/components/autarco/strings.json new file mode 100644 index 00000000000..7ab81655764 --- /dev/null +++ b/homeassistant/components/autarco/strings.json @@ -0,0 +1,40 @@ +{ + "config": { + "step": { + "user": { + "description": "Connect to your Autarco account to get information about your solar panels.", + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "email": "The email address of your Autarco account.", + "password": "The password of your Autarco account." + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "entity": { + "sensor": { + "power_production": { + "name": "Power production" + }, + "energy_production_today": { + "name": "Energy production today" + }, + "energy_production_month": { + "name": "Energy production month" + }, + "energy_production_total": { + "name": "ENergy production total" + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c1c8474fa31..faf84d9fd38 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -69,6 +69,7 @@ FLOWS = { "aurora", "aurora_abb_powerone", "aussie_broadband", + "autarco", "awair", "axis", "azure_data_explorer", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 7df69226e4c..b798a02f7a8 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -581,6 +581,12 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "autarco": { + "name": "Autarco", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "avion": { "name": "Avi-on", "integration_type": "hub", diff --git a/mypy.ini b/mypy.ini index b24898b3287..cf16c4f5f63 100644 --- a/mypy.ini +++ b/mypy.ini @@ -732,6 +732,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.autarco.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.auth.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index 70114e57a18..4bcc12415f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -516,6 +516,9 @@ auroranoaa==0.0.3 # homeassistant.components.aurora_abb_powerone aurorapy==0.2.7 +# homeassistant.components.autarco +autarco==2.0.0 + # homeassistant.components.avea # avea==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d17e0e2e501..e03288234ce 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -462,6 +462,9 @@ auroranoaa==0.0.3 # homeassistant.components.aurora_abb_powerone aurorapy==0.2.7 +# homeassistant.components.autarco +autarco==2.0.0 + # homeassistant.components.axis axis==62 diff --git a/tests/components/autarco/__init__.py b/tests/components/autarco/__init__.py new file mode 100644 index 00000000000..208e5999fc7 --- /dev/null +++ b/tests/components/autarco/__init__.py @@ -0,0 +1,12 @@ +"""Tests for the Autarco integration.""" + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None: + """Fixture for setting up the integration.""" + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/autarco/conftest.py b/tests/components/autarco/conftest.py new file mode 100644 index 00000000000..b31d7b4cc95 --- /dev/null +++ b/tests/components/autarco/conftest.py @@ -0,0 +1,66 @@ +"""Common fixtures for the Autarco tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +from autarco import AccountSite, Solar +import pytest + +from homeassistant.components.autarco.const import DOMAIN +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.autarco.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry + + +@pytest.fixture +def mock_autarco_client() -> Generator[AsyncMock]: + """Mock a Autarco client.""" + with ( + patch( + "homeassistant.components.autarco.Autarco", + autospec=True, + ) as mock_client, + patch( + "homeassistant.components.autarco.config_flow.Autarco", + new=mock_client, + ), + ): + client = mock_client.return_value + client.get_account.return_value = [ + AccountSite( + site_id=1, + public_key="key-public", + system_name="test-system", + retailer="test-retailer", + health="OK", + ) + ] + client.get_solar.return_value = Solar( + power_production=200, + energy_production_today=4, + energy_production_month=58, + energy_production_total=10379, + ) + yield client + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Mock a config entry.""" + return MockConfigEntry( + domain=DOMAIN, + title="Autarco", + data={ + CONF_EMAIL: "test@autarco.com", + CONF_PASSWORD: "test-password", + }, + ) diff --git a/tests/components/autarco/fixtures/solar.json b/tests/components/autarco/fixtures/solar.json new file mode 100644 index 00000000000..0cc023a5c2d --- /dev/null +++ b/tests/components/autarco/fixtures/solar.json @@ -0,0 +1,6 @@ +{ + "pv_now": 200, + "pv_today": 4, + "pv_month": 58, + "pv_to_date": 10379 +} diff --git a/tests/components/autarco/snapshots/test_sensor.ambr b/tests/components/autarco/snapshots/test_sensor.ambr new file mode 100644 index 00000000000..77a400182b6 --- /dev/null +++ b/tests/components/autarco/snapshots/test_sensor.ambr @@ -0,0 +1,196 @@ +# serializer version: 1 +# name: test_solar_sensors[sensor.solar_energy_production_month-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solar_energy_production_month', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy production month', + 'platform': 'autarco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_production_month', + 'unique_id': '1_solar_energy_production_month', + 'unit_of_measurement': , + }) +# --- +# name: test_solar_sensors[sensor.solar_energy_production_month-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Solar Energy production month', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solar_energy_production_month', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '58', + }) +# --- +# name: test_solar_sensors[sensor.solar_energy_production_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solar_energy_production_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Energy production today', + 'platform': 'autarco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_production_today', + 'unique_id': '1_solar_energy_production_today', + 'unit_of_measurement': , + }) +# --- +# name: test_solar_sensors[sensor.solar_energy_production_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Solar Energy production today', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solar_energy_production_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '4', + }) +# --- +# name: test_solar_sensors[sensor.solar_energy_production_total-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solar_energy_production_total', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'ENergy production total', + 'platform': 'autarco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'energy_production_total', + 'unique_id': '1_solar_energy_production_total', + 'unit_of_measurement': , + }) +# --- +# name: test_solar_sensors[sensor.solar_energy_production_total-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'energy', + 'friendly_name': 'Solar ENergy production total', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solar_energy_production_total', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '10379', + }) +# --- +# name: test_solar_sensors[sensor.solar_power_production-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.solar_power_production', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power production', + 'platform': 'autarco', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'power_production', + 'unique_id': '1_solar_power_production', + 'unit_of_measurement': , + }) +# --- +# name: test_solar_sensors[sensor.solar_power_production-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'power', + 'friendly_name': 'Solar Power production', + 'state_class': , + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.solar_power_production', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '200', + }) +# --- diff --git a/tests/components/autarco/test_config_flow.py b/tests/components/autarco/test_config_flow.py new file mode 100644 index 00000000000..621ad7f55c8 --- /dev/null +++ b/tests/components/autarco/test_config_flow.py @@ -0,0 +1,101 @@ +"""Test the Autarco config flow.""" + +from unittest.mock import AsyncMock + +from autarco import AutarcoAuthenticationError, AutarcoConnectionError +import pytest + +from homeassistant.components.autarco.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_user_flow( + hass: HomeAssistant, + mock_autarco_client: AsyncMock, + mock_setup_entry: AsyncMock, +) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") is FlowResultType.FORM + assert result.get("step_id") == "user" + assert not result.get("errors") + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_EMAIL: "test@autarco.com", CONF_PASSWORD: "test-password"}, + ) + + assert result.get("type") is FlowResultType.CREATE_ENTRY + assert result.get("title") == "test@autarco.com" + assert result.get("data") == { + CONF_EMAIL: "test@autarco.com", + CONF_PASSWORD: "test-password", + } + assert len(mock_autarco_client.get_account.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_duplicate_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_autarco_client: AsyncMock, +) -> None: + """Test abort when setting up duplicate entry.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") is FlowResultType.FORM + assert not result.get("errors") + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_EMAIL: "test@autarco.com", CONF_PASSWORD: "test-password"}, + ) + + assert result.get("type") is FlowResultType.ABORT + assert result.get("reason") == "already_configured" + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (AutarcoConnectionError, "cannot_connect"), + (AutarcoAuthenticationError, "invalid_auth"), + ], +) +async def test_exceptions( + hass: HomeAssistant, + mock_autarco_client: AsyncMock, + mock_setup_entry: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test exceptions.""" + mock_autarco_client.get_account.side_effect = exception + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_EMAIL: "test@autarco.com", CONF_PASSWORD: "test-password"}, + ) + assert result.get("type") is FlowResultType.FORM + assert result.get("errors") == {"base": error} + + mock_autarco_client.get_account.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_EMAIL: "test@autarco.com", CONF_PASSWORD: "test-password"}, + ) + assert result.get("type") is FlowResultType.CREATE_ENTRY diff --git a/tests/components/autarco/test_init.py b/tests/components/autarco/test_init.py new file mode 100644 index 00000000000..81c5f947251 --- /dev/null +++ b/tests/components/autarco/test_init.py @@ -0,0 +1,28 @@ +"""Test the Autarco init module.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock + +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +async def test_load_unload_entry( + hass: HomeAssistant, + mock_autarco_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test load and unload entry.""" + await setup_integration(hass, mock_config_entry) + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_remove(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/autarco/test_sensor.py b/tests/components/autarco/test_sensor.py new file mode 100644 index 00000000000..e01de1fb0d3 --- /dev/null +++ b/tests/components/autarco/test_sensor.py @@ -0,0 +1,27 @@ +"""Test the sensor provided by the Autarco integration.""" + +from unittest.mock import MagicMock, patch + +from syrupy import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_solar_sensors( + hass: HomeAssistant, + mock_autarco_client: MagicMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test the Autarco - Solar sensor.""" + with patch("homeassistant.components.autarco.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)