Add Autarco integration (#121600)
* Init Autarco integration * Add integration code with tests * Update every 5 minutes * Process all feedback from Joost * Bump lib to v2.0.0 * Add more then one site if present * Fix issue with entity translation * Update the test for sensor entities * Fix round two based on feedback from Joost * Add autarco to strict typing * Update tests/components/autarco/test_config_flow.py * Update tests/components/autarco/test_config_flow.py --------- Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
abeac3f3aa
commit
fce68018b7
21 changed files with 790 additions and 0 deletions
|
@ -97,6 +97,7 @@ homeassistant.components.assist_pipeline.*
|
||||||
homeassistant.components.asterisk_cdr.*
|
homeassistant.components.asterisk_cdr.*
|
||||||
homeassistant.components.asterisk_mbox.*
|
homeassistant.components.asterisk_mbox.*
|
||||||
homeassistant.components.asuswrt.*
|
homeassistant.components.asuswrt.*
|
||||||
|
homeassistant.components.autarco.*
|
||||||
homeassistant.components.auth.*
|
homeassistant.components.auth.*
|
||||||
homeassistant.components.automation.*
|
homeassistant.components.automation.*
|
||||||
homeassistant.components.awair.*
|
homeassistant.components.awair.*
|
||||||
|
|
|
@ -155,6 +155,8 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/aurora_abb_powerone/ @davet2001
|
/tests/components/aurora_abb_powerone/ @davet2001
|
||||||
/homeassistant/components/aussie_broadband/ @nickw444 @Bre77
|
/homeassistant/components/aussie_broadband/ @nickw444 @Bre77
|
||||||
/tests/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
|
/homeassistant/components/auth/ @home-assistant/core
|
||||||
/tests/components/auth/ @home-assistant/core
|
/tests/components/auth/ @home-assistant/core
|
||||||
/homeassistant/components/automation/ @home-assistant/core
|
/homeassistant/components/automation/ @home-assistant/core
|
||||||
|
|
49
homeassistant/components/autarco/__init__.py
Normal file
49
homeassistant/components/autarco/__init__.py
Normal file
|
@ -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)
|
57
homeassistant/components/autarco/config_flow.py
Normal file
57
homeassistant/components/autarco/config_flow.py
Normal file
|
@ -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,
|
||||||
|
)
|
13
homeassistant/components/autarco/const.py
Normal file
13
homeassistant/components/autarco/const.py
Normal file
|
@ -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"
|
47
homeassistant/components/autarco/coordinator.py
Normal file
47
homeassistant/components/autarco/coordinator.py
Normal file
|
@ -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),
|
||||||
|
)
|
9
homeassistant/components/autarco/manifest.json
Normal file
9
homeassistant/components/autarco/manifest.json
Normal file
|
@ -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"]
|
||||||
|
}
|
113
homeassistant/components/autarco/sensor.py
Normal file
113
homeassistant/components/autarco/sensor.py
Normal file
|
@ -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)
|
40
homeassistant/components/autarco/strings.json
Normal file
40
homeassistant/components/autarco/strings.json
Normal file
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -69,6 +69,7 @@ FLOWS = {
|
||||||
"aurora",
|
"aurora",
|
||||||
"aurora_abb_powerone",
|
"aurora_abb_powerone",
|
||||||
"aussie_broadband",
|
"aussie_broadband",
|
||||||
|
"autarco",
|
||||||
"awair",
|
"awair",
|
||||||
"axis",
|
"axis",
|
||||||
"azure_data_explorer",
|
"azure_data_explorer",
|
||||||
|
|
|
@ -581,6 +581,12 @@
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
},
|
},
|
||||||
|
"autarco": {
|
||||||
|
"name": "Autarco",
|
||||||
|
"integration_type": "hub",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "cloud_polling"
|
||||||
|
},
|
||||||
"avion": {
|
"avion": {
|
||||||
"name": "Avi-on",
|
"name": "Avi-on",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
|
|
10
mypy.ini
10
mypy.ini
|
@ -732,6 +732,16 @@ disallow_untyped_defs = true
|
||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = 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.*]
|
[mypy-homeassistant.components.auth.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
|
|
@ -516,6 +516,9 @@ auroranoaa==0.0.3
|
||||||
# homeassistant.components.aurora_abb_powerone
|
# homeassistant.components.aurora_abb_powerone
|
||||||
aurorapy==0.2.7
|
aurorapy==0.2.7
|
||||||
|
|
||||||
|
# homeassistant.components.autarco
|
||||||
|
autarco==2.0.0
|
||||||
|
|
||||||
# homeassistant.components.avea
|
# homeassistant.components.avea
|
||||||
# avea==1.5.1
|
# avea==1.5.1
|
||||||
|
|
||||||
|
|
|
@ -462,6 +462,9 @@ auroranoaa==0.0.3
|
||||||
# homeassistant.components.aurora_abb_powerone
|
# homeassistant.components.aurora_abb_powerone
|
||||||
aurorapy==0.2.7
|
aurorapy==0.2.7
|
||||||
|
|
||||||
|
# homeassistant.components.autarco
|
||||||
|
autarco==2.0.0
|
||||||
|
|
||||||
# homeassistant.components.axis
|
# homeassistant.components.axis
|
||||||
axis==62
|
axis==62
|
||||||
|
|
||||||
|
|
12
tests/components/autarco/__init__.py
Normal file
12
tests/components/autarco/__init__.py
Normal file
|
@ -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)
|
66
tests/components/autarco/conftest.py
Normal file
66
tests/components/autarco/conftest.py
Normal file
|
@ -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",
|
||||||
|
},
|
||||||
|
)
|
6
tests/components/autarco/fixtures/solar.json
Normal file
6
tests/components/autarco/fixtures/solar.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"pv_now": 200,
|
||||||
|
"pv_today": 4,
|
||||||
|
"pv_month": 58,
|
||||||
|
"pv_to_date": 10379
|
||||||
|
}
|
196
tests/components/autarco/snapshots/test_sensor.ambr
Normal file
196
tests/components/autarco/snapshots/test_sensor.ambr
Normal file
|
@ -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': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'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': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||||
|
'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': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# 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': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.solar_energy_production_month',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '58',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_solar_sensors[sensor.solar_energy_production_today-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': None,
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'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': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||||
|
'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': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# 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': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.solar_energy_production_today',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '4',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_solar_sensors[sensor.solar_energy_production_total-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': None,
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'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': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
|
||||||
|
'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': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# 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': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.solar_energy_production_total',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '10379',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_solar_sensors[sensor.solar_power_production-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.solar_power_production',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
|
||||||
|
'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': <UnitOfPower.WATT: 'W'>,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_solar_sensors[sensor.solar_power_production-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'power',
|
||||||
|
'friendly_name': 'Solar Power production',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': <UnitOfPower.WATT: 'W'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.solar_power_production',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '200',
|
||||||
|
})
|
||||||
|
# ---
|
101
tests/components/autarco/test_config_flow.py
Normal file
101
tests/components/autarco/test_config_flow.py
Normal file
|
@ -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
|
28
tests/components/autarco/test_init.py
Normal file
28
tests/components/autarco/test_init.py
Normal file
|
@ -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
|
27
tests/components/autarco/test_sensor.py
Normal file
27
tests/components/autarco/test_sensor.py
Normal file
|
@ -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)
|
Loading…
Add table
Add a link
Reference in a new issue