Add new Aqvify integration (#172936)
This commit is contained in:
@@ -96,6 +96,7 @@ homeassistant.components.aprs.*
|
|||||||
homeassistant.components.apsystems.*
|
homeassistant.components.apsystems.*
|
||||||
homeassistant.components.aqualogic.*
|
homeassistant.components.aqualogic.*
|
||||||
homeassistant.components.aquostv.*
|
homeassistant.components.aquostv.*
|
||||||
|
homeassistant.components.aqvify.*
|
||||||
homeassistant.components.aranet.*
|
homeassistant.components.aranet.*
|
||||||
homeassistant.components.arcam_fmj.*
|
homeassistant.components.arcam_fmj.*
|
||||||
homeassistant.components.arris_tg2492lg.*
|
homeassistant.components.arris_tg2492lg.*
|
||||||
|
|||||||
Generated
+2
@@ -162,6 +162,8 @@ CLAUDE.md @home-assistant/core
|
|||||||
/tests/components/apsystems/ @mawoka-myblock @SonnenladenGmbH
|
/tests/components/apsystems/ @mawoka-myblock @SonnenladenGmbH
|
||||||
/homeassistant/components/aquacell/ @Jordi1990
|
/homeassistant/components/aquacell/ @Jordi1990
|
||||||
/tests/components/aquacell/ @Jordi1990
|
/tests/components/aquacell/ @Jordi1990
|
||||||
|
/homeassistant/components/aqvify/ @astrandb
|
||||||
|
/tests/components/aqvify/ @astrandb
|
||||||
/homeassistant/components/aranet/ @aschmitz @thecode @anrijs
|
/homeassistant/components/aranet/ @aschmitz @thecode @anrijs
|
||||||
/tests/components/aranet/ @aschmitz @thecode @anrijs
|
/tests/components/aranet/ @aschmitz @thecode @anrijs
|
||||||
/homeassistant/components/arcam_fmj/ @elupus
|
/homeassistant/components/arcam_fmj/ @elupus
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""The Aqvify integration."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .coordinator import AqvifyConfigEntry, AqvifyCoordinator
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: AqvifyConfigEntry) -> bool:
|
||||||
|
"""Set up Aqvify from a config entry."""
|
||||||
|
|
||||||
|
coordinator = AqvifyCoordinator(hass, entry)
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: AqvifyConfigEntry) -> bool:
|
||||||
|
"""Unload Aqvify config entry."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""Config flow for the Aqvify integration."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from aiohttp import ClientResponseError
|
||||||
|
from pyaqvify import AqvifyAPI, AqvifyAuthException
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import CONF_API_KEY
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_API_KEY): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AqvifyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Aqvify."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
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:
|
||||||
|
hub = AqvifyAPI(
|
||||||
|
user_input[CONF_API_KEY],
|
||||||
|
websession=async_get_clientsession(self.hass),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
account_data = await hub.async_get_account_id()
|
||||||
|
except AqvifyAuthException:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except ClientResponseError:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(account_data.account_id)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(title="Aqvify", data=user_input)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=STEP_USER_DATA_SCHEMA,
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"aqvify_url": "https://app.aqvify.com/User",
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""Constants for the Aqvify integration."""
|
||||||
|
|
||||||
|
DOMAIN = "aqvify"
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
"""Coordinator for Aqvify integration."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aiohttp import ClientResponseError
|
||||||
|
from pyaqvify import AqvifyAPI, AqvifyAuthException, AqvifyDeviceData, AqvifyDevices
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_API_KEY
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
UPDATE_INTERVAL = timedelta(seconds=60)
|
||||||
|
|
||||||
|
type AqvifyConfigEntry = ConfigEntry[AqvifyCoordinator]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AqvifyCoordinatorData:
|
||||||
|
"""Data class for storing coordinator data."""
|
||||||
|
|
||||||
|
devices: AqvifyDevices
|
||||||
|
device_data: dict[str, AqvifyDeviceData]
|
||||||
|
|
||||||
|
|
||||||
|
class AqvifyCoordinator(DataUpdateCoordinator[AqvifyCoordinatorData]):
|
||||||
|
"""Data update coordinator for Aqvify devices."""
|
||||||
|
|
||||||
|
config_entry: AqvifyConfigEntry
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant, entry: AqvifyConfigEntry) -> None:
|
||||||
|
"""Initialize the Aqvify data update coordinator."""
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
logger=_LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=UPDATE_INTERVAL,
|
||||||
|
config_entry=entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.api_client = AqvifyAPI(
|
||||||
|
entry.data[CONF_API_KEY], websession=async_get_clientsession(hass)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_setup(self) -> None:
|
||||||
|
"""Set up the coordinator."""
|
||||||
|
try:
|
||||||
|
await self.api_client.async_get_account_id()
|
||||||
|
except AqvifyAuthException as err:
|
||||||
|
raise ConfigEntryAuthFailed(f"Invalid Aqvify API key: {err}") from err
|
||||||
|
except (ClientResponseError, TimeoutError) as err:
|
||||||
|
raise ConfigEntryNotReady(
|
||||||
|
f"Failed to connect to Aqvify API: {err}"
|
||||||
|
) from err
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> AqvifyCoordinatorData:
|
||||||
|
"""Fetch device state."""
|
||||||
|
try:
|
||||||
|
devices = await self.api_client.async_get_devices()
|
||||||
|
except ClientResponseError as err:
|
||||||
|
raise UpdateFailed(f"Error communicating with Aqvify API: {err}") from err
|
||||||
|
except TimeoutError as err:
|
||||||
|
raise UpdateFailed(f"Timeout communicating with Aqvify API: {err}") from err
|
||||||
|
|
||||||
|
device_data = {}
|
||||||
|
for device in devices.devices.values():
|
||||||
|
try:
|
||||||
|
device_key = str(device.device_key)
|
||||||
|
device_data[
|
||||||
|
device_key
|
||||||
|
] = await self.api_client.async_get_device_latest_data(device_key)
|
||||||
|
except ClientResponseError as err:
|
||||||
|
raise UpdateFailed(
|
||||||
|
f"Error communicating with Aqvify API: {err}"
|
||||||
|
) from err
|
||||||
|
except TimeoutError as err:
|
||||||
|
raise UpdateFailed(
|
||||||
|
f"Timeout communicating with Aqvify API: {err}"
|
||||||
|
) from err
|
||||||
|
|
||||||
|
return AqvifyCoordinatorData(
|
||||||
|
devices=devices,
|
||||||
|
device_data=device_data,
|
||||||
|
)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"""Defines a base Aqvify entity."""
|
||||||
|
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AqvifyCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
class AqvifyBaseEntity(CoordinatorEntity[AqvifyCoordinator]):
|
||||||
|
"""Defines a base Aqvify entity."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AqvifyCoordinator,
|
||||||
|
description: EntityDescription,
|
||||||
|
device_key: str,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Aqvify entity."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
|
||||||
|
account_id = self.coordinator.config_entry.unique_id
|
||||||
|
self.device_key = device_key
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, f"{account_id}_{device_key}")},
|
||||||
|
name=coordinator.data.devices.devices[device_key].name,
|
||||||
|
manufacturer="Aqvify",
|
||||||
|
configuration_url="https://app.aqvify.com",
|
||||||
|
serial_number=device_key,
|
||||||
|
)
|
||||||
|
self._attr_unique_id = f"{account_id}_{device_key}_{description.key}"
|
||||||
|
self.entity_description = description
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"meter_value": {
|
||||||
|
"default": "mdi:waves-arrow-up"
|
||||||
|
},
|
||||||
|
"water_level": {
|
||||||
|
"default": "mdi:waves"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"domain": "aqvify",
|
||||||
|
"name": "Aqvify",
|
||||||
|
"codeowners": ["@astrandb"],
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/aqvify",
|
||||||
|
"integration_type": "hub",
|
||||||
|
"iot_class": "cloud_polling",
|
||||||
|
"loggers": ["pyaqvify"],
|
||||||
|
"quality_scale": "bronze",
|
||||||
|
"requirements": ["pyaqvify==0.0.8"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
No actions in this integration.
|
||||||
|
appropriate-polling: done
|
||||||
|
brands: done
|
||||||
|
common-modules: done
|
||||||
|
config-flow-test-coverage: done
|
||||||
|
config-flow: done
|
||||||
|
dependency-transparency: done
|
||||||
|
docs-actions:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
The integration does not provide any actions.
|
||||||
|
docs-high-level-description: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: done
|
||||||
|
entity-event-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Entities of this integration do 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: todo
|
||||||
|
docs-installation-parameters: todo
|
||||||
|
entity-unavailable: todo
|
||||||
|
integration-owner: todo
|
||||||
|
log-when-unavailable: todo
|
||||||
|
parallel-updates: done
|
||||||
|
reauthentication-flow: todo
|
||||||
|
test-coverage: todo
|
||||||
|
|
||||||
|
# Gold
|
||||||
|
devices: todo
|
||||||
|
diagnostics: todo
|
||||||
|
discovery-update-info: todo
|
||||||
|
discovery: todo
|
||||||
|
docs-data-update: todo
|
||||||
|
docs-examples: todo
|
||||||
|
docs-known-limitations: todo
|
||||||
|
docs-supported-devices: todo
|
||||||
|
docs-supported-functions: todo
|
||||||
|
docs-troubleshooting: todo
|
||||||
|
docs-use-cases: todo
|
||||||
|
dynamic-devices: todo
|
||||||
|
entity-category: todo
|
||||||
|
entity-device-class: todo
|
||||||
|
entity-disabled-by-default: todo
|
||||||
|
entity-translations: todo
|
||||||
|
exception-translations: todo
|
||||||
|
icon-translations: done
|
||||||
|
reconfiguration-flow: todo
|
||||||
|
repair-issues: todo
|
||||||
|
stale-devices: todo
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: todo
|
||||||
|
inject-websession: todo
|
||||||
|
strict-typing: todo
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"""Sensor platform for Aqvify integration."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pyaqvify import AqvifyDeviceData
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
StateType,
|
||||||
|
)
|
||||||
|
from homeassistant.const import UnitOfLength
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import AqvifyConfigEntry
|
||||||
|
from .entity import AqvifyBaseEntity
|
||||||
|
|
||||||
|
# Coordinator is used to centralize the data updates.
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class AqvifySensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Description of an Aqvify sensor entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[AqvifyDeviceData], float | int | None]
|
||||||
|
|
||||||
|
|
||||||
|
ENTITIES: tuple[AqvifySensorEntityDescription, ...] = (
|
||||||
|
AqvifySensorEntityDescription(
|
||||||
|
key="meter_value",
|
||||||
|
translation_key="meter_value",
|
||||||
|
native_unit_of_measurement=UnitOfLength.METERS,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
value_fn=lambda value: value.meter_value,
|
||||||
|
),
|
||||||
|
AqvifySensorEntityDescription(
|
||||||
|
key="water_level",
|
||||||
|
translation_key="water_level",
|
||||||
|
native_unit_of_measurement=UnitOfLength.METERS,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
value_fn=lambda value: value.water_level,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: AqvifyConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Aqvify sensor entities from a config entry."""
|
||||||
|
async_add_entities(
|
||||||
|
AqvifySensor(entry.runtime_data, description, device_key)
|
||||||
|
for description in ENTITIES
|
||||||
|
for device_key in entry.runtime_data.data.devices.devices
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AqvifySensor(AqvifyBaseEntity, SensorEntity):
|
||||||
|
"""Representation of an Aqvify sensor entity."""
|
||||||
|
|
||||||
|
entity_description: AqvifySensorEntityDescription
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> StateType | datetime | None:
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self.entity_description.value_fn(
|
||||||
|
self.coordinator.data.device_data[self.device_key]
|
||||||
|
)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"api_key": "API key"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"api_key": "Your Aqvify API key"
|
||||||
|
},
|
||||||
|
"description": "Navigate to your [Aqvify account]({aqvify_url}), copy your API key, and paste it below."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"meter_value": {
|
||||||
|
"name": "Meter value"
|
||||||
|
},
|
||||||
|
"water_level": {
|
||||||
|
"name": "Water level"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+1
@@ -74,6 +74,7 @@ FLOWS = {
|
|||||||
"aprilaire",
|
"aprilaire",
|
||||||
"apsystems",
|
"apsystems",
|
||||||
"aquacell",
|
"aquacell",
|
||||||
|
"aqvify",
|
||||||
"aranet",
|
"aranet",
|
||||||
"arcam_fmj",
|
"arcam_fmj",
|
||||||
"arve",
|
"arve",
|
||||||
|
|||||||
@@ -520,6 +520,12 @@
|
|||||||
"config_flow": false,
|
"config_flow": false,
|
||||||
"iot_class": "local_polling"
|
"iot_class": "local_polling"
|
||||||
},
|
},
|
||||||
|
"aqvify": {
|
||||||
|
"name": "Aqvify",
|
||||||
|
"integration_type": "hub",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "cloud_polling"
|
||||||
|
},
|
||||||
"aranet": {
|
"aranet": {
|
||||||
"name": "Aranet",
|
"name": "Aranet",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
|
|||||||
@@ -716,6 +716,16 @@ disallow_untyped_defs = true
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
|
|
||||||
|
[mypy-homeassistant.components.aqvify.*]
|
||||||
|
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.aranet.*]
|
[mypy-homeassistant.components.aranet.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
|||||||
Generated
+3
@@ -2017,6 +2017,9 @@ pyanglianwater==3.1.2
|
|||||||
# homeassistant.components.aprilaire
|
# homeassistant.components.aprilaire
|
||||||
pyaprilaire==0.9.1
|
pyaprilaire==0.9.1
|
||||||
|
|
||||||
|
# homeassistant.components.aqvify
|
||||||
|
pyaqvify==0.0.8
|
||||||
|
|
||||||
# homeassistant.components.atag
|
# homeassistant.components.atag
|
||||||
pyatag==0.3.5.3
|
pyatag==0.3.5.3
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
"""Tests for Aqvify integration."""
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||||
|
"""Helper for setting up the component."""
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
"""Common fixtures for the Aqvify tests."""
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
from pyaqvify import AqvifyAccount, AqvifyDeviceData, AqvifyDevices
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.aqvify.const import DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import (
|
||||||
|
MockConfigEntry,
|
||||||
|
async_load_json_array_fixture,
|
||||||
|
async_load_json_object_fixture,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||||
|
"""Override async_setup_entry."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.aqvify.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup_entry:
|
||||||
|
yield mock_setup_entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||||
|
"""Return the default mocked config entry."""
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
minor_version=1,
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="Aqvify test",
|
||||||
|
data={"api_key": "fake_api_key"},
|
||||||
|
entry_id="aqvify_test",
|
||||||
|
unique_id="test_account_id",
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
return config_entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_aqvify_client(
|
||||||
|
device_fixture: list[dict[str, Any]],
|
||||||
|
device_data_fixture: dict[str, Any],
|
||||||
|
account_fixture: dict[str, Any],
|
||||||
|
) -> Generator[MagicMock]:
|
||||||
|
"""Mock an Aqvify client."""
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.aqvify.coordinator.AqvifyAPI",
|
||||||
|
autospec=True,
|
||||||
|
) as mock_client,
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.aqvify.config_flow.AqvifyAPI",
|
||||||
|
new=mock_client,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
client = mock_client.return_value
|
||||||
|
|
||||||
|
client.async_get_account_id.return_value = AqvifyAccount(account_fixture)
|
||||||
|
client.async_get_devices.return_value = AqvifyDevices(device_fixture)
|
||||||
|
client.async_get_device_latest_data.return_value = AqvifyDeviceData(
|
||||||
|
device_data_fixture
|
||||||
|
)
|
||||||
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="package")
|
||||||
|
def load_device_file() -> str:
|
||||||
|
"""Fixture for loading device file."""
|
||||||
|
return "default_devices.json"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="package")
|
||||||
|
def load_device_data_file() -> str:
|
||||||
|
"""Fixture for loading device data file."""
|
||||||
|
return "default_device_data.json"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="package")
|
||||||
|
def load_account_file() -> str:
|
||||||
|
"""Fixture for loading account file."""
|
||||||
|
return "default_account.json"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def device_fixture(
|
||||||
|
hass: HomeAssistant, load_device_file: str
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Fixture for device."""
|
||||||
|
return await async_load_json_array_fixture(hass, load_device_file, DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def device_data_fixture(
|
||||||
|
hass: HomeAssistant, load_device_data_file: str
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Fixture for device data."""
|
||||||
|
return await async_load_json_object_fixture(hass, load_device_data_file, DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def account_fixture(
|
||||||
|
hass: HomeAssistant, load_account_file: str
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Fixture for account data."""
|
||||||
|
return await async_load_json_object_fixture(hass, load_account_file, DOMAIN)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"accountId": "test_account_id"
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"dateTime": "2026-06-04T09:36:06+00:00",
|
||||||
|
"waterLevel": -0.136786005,
|
||||||
|
"meterValue": 0.823213995,
|
||||||
|
"status": null
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"deviceKey": "DeviceKey_1",
|
||||||
|
"name": "Device 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"deviceKey": "DeviceKey_2",
|
||||||
|
"name": "Device 2"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_device_registry_integration
|
||||||
|
list([
|
||||||
|
DeviceRegistryEntrySnapshot({
|
||||||
|
'area_id': None,
|
||||||
|
'config_entries': <ANY>,
|
||||||
|
'config_entries_subentries': <ANY>,
|
||||||
|
'configuration_url': 'https://app.aqvify.com',
|
||||||
|
'connections': set({
|
||||||
|
}),
|
||||||
|
'disabled_by': None,
|
||||||
|
'entry_type': None,
|
||||||
|
'hw_version': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'identifiers': set({
|
||||||
|
tuple(
|
||||||
|
'aqvify',
|
||||||
|
'test_account_id_DeviceKey_1',
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'manufacturer': 'Aqvify',
|
||||||
|
'model': None,
|
||||||
|
'model_id': None,
|
||||||
|
'name': 'Device 1',
|
||||||
|
'name_by_user': None,
|
||||||
|
'primary_config_entry': <ANY>,
|
||||||
|
'serial_number': 'DeviceKey_1',
|
||||||
|
'sw_version': None,
|
||||||
|
'via_device_id': None,
|
||||||
|
}),
|
||||||
|
DeviceRegistryEntrySnapshot({
|
||||||
|
'area_id': None,
|
||||||
|
'config_entries': <ANY>,
|
||||||
|
'config_entries_subentries': <ANY>,
|
||||||
|
'configuration_url': 'https://app.aqvify.com',
|
||||||
|
'connections': set({
|
||||||
|
}),
|
||||||
|
'disabled_by': None,
|
||||||
|
'entry_type': None,
|
||||||
|
'hw_version': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'identifiers': set({
|
||||||
|
tuple(
|
||||||
|
'aqvify',
|
||||||
|
'test_account_id_DeviceKey_2',
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'manufacturer': 'Aqvify',
|
||||||
|
'model': None,
|
||||||
|
'model_id': None,
|
||||||
|
'name': 'Device 2',
|
||||||
|
'name_by_user': None,
|
||||||
|
'primary_config_entry': <ANY>,
|
||||||
|
'serial_number': 'DeviceKey_2',
|
||||||
|
'sw_version': None,
|
||||||
|
'via_device_id': None,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
# ---
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_sensor_snapshot[sensor.device_1_meter_value-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': list([
|
||||||
|
None,
|
||||||
|
]),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.device_1_meter_value',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'object_id_base': 'Meter value',
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Meter value',
|
||||||
|
'platform': 'aqvify',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'meter_value',
|
||||||
|
'unique_id': 'test_account_id_DeviceKey_1_meter_value',
|
||||||
|
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_snapshot[sensor.device_1_meter_value-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'distance',
|
||||||
|
'friendly_name': 'Device 1 Meter value',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.device_1_meter_value',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '0.823213995',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_snapshot[sensor.device_1_water_level-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': list([
|
||||||
|
None,
|
||||||
|
]),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.device_1_water_level',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'object_id_base': 'Water level',
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Water level',
|
||||||
|
'platform': 'aqvify',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'water_level',
|
||||||
|
'unique_id': 'test_account_id_DeviceKey_1_water_level',
|
||||||
|
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_snapshot[sensor.device_1_water_level-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'distance',
|
||||||
|
'friendly_name': 'Device 1 Water level',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.device_1_water_level',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '-0.136786005',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_snapshot[sensor.device_2_meter_value-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': list([
|
||||||
|
None,
|
||||||
|
]),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.device_2_meter_value',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'object_id_base': 'Meter value',
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Meter value',
|
||||||
|
'platform': 'aqvify',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'meter_value',
|
||||||
|
'unique_id': 'test_account_id_DeviceKey_2_meter_value',
|
||||||
|
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_snapshot[sensor.device_2_meter_value-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'distance',
|
||||||
|
'friendly_name': 'Device 2 Meter value',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.device_2_meter_value',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '0.823213995',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_snapshot[sensor.device_2_water_level-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': list([
|
||||||
|
None,
|
||||||
|
]),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.device_2_water_level',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'object_id_base': 'Water level',
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.DISTANCE: 'distance'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Water level',
|
||||||
|
'platform': 'aqvify',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'water_level',
|
||||||
|
'unique_id': 'test_account_id_DeviceKey_2_water_level',
|
||||||
|
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor_snapshot[sensor.device_2_water_level-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'distance',
|
||||||
|
'friendly_name': 'Device 2 Water level',
|
||||||
|
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||||
|
'unit_of_measurement': <UnitOfLength.METERS: 'm'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.device_2_water_level',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '-0.136786005',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
"""Test the Aqvify config flow."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
|
from aiohttp import ClientResponseError
|
||||||
|
from pyaqvify import AqvifyAuthException
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.aqvify.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
|
from homeassistant.const import CONF_API_KEY
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
|
|
||||||
|
async def test_full_flow(
|
||||||
|
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_aqvify_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["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_API_KEY: "test-api-key",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "Aqvify"
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_API_KEY: "test-api-key",
|
||||||
|
}
|
||||||
|
assert result["result"].unique_id == "test_account_id"
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("side_effect", "error_base"),
|
||||||
|
[
|
||||||
|
(AqvifyAuthException, "invalid_auth"),
|
||||||
|
(
|
||||||
|
ClientResponseError(request_info=None, history=None, status=500),
|
||||||
|
"cannot_connect",
|
||||||
|
),
|
||||||
|
(TypeError, "unknown"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_form_invalid(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
mock_aqvify_client: MagicMock,
|
||||||
|
side_effect: Exception,
|
||||||
|
error_base: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test we handle errors during form submission."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_aqvify_client.async_get_account_id.side_effect = side_effect
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_API_KEY: "test-api-key",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["errors"] == {"base": error_base}
|
||||||
|
|
||||||
|
# Make sure the config flow tests finish with either an
|
||||||
|
# FlowResultType.CREATE_ENTRY or FlowResultType.ABORT so
|
||||||
|
# we can show the config flow is able to recover from an error.
|
||||||
|
mock_aqvify_client.async_get_account_id.side_effect = None
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_API_KEY: "test-api-key",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "Aqvify"
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_API_KEY: "test-api-key",
|
||||||
|
}
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_same_account_setup(
|
||||||
|
hass: HomeAssistant, mock_config_entry: AsyncMock, mock_aqvify_client: MagicMock
|
||||||
|
) -> None:
|
||||||
|
"""Test setup same account twice."""
|
||||||
|
|
||||||
|
# Create an existing config entry for the same user account
|
||||||
|
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["errors"] == {}
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_API_KEY: "test-api-key2",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""Test the Aqvify init."""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from pyaqvify import AqvifyAuthException
|
||||||
|
import pytest
|
||||||
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
import homeassistant.helpers.device_registry as dr
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_load_unload_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_aqvify_client: MagicMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test load and unload entry."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
entry = mock_config_entry
|
||||||
|
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("error", "expected_state"),
|
||||||
|
[
|
||||||
|
(None, ConfigEntryState.LOADED),
|
||||||
|
(AqvifyAuthException, ConfigEntryState.SETUP_ERROR),
|
||||||
|
(TimeoutError, ConfigEntryState.SETUP_RETRY),
|
||||||
|
],
|
||||||
|
ids=["no_error", "auth_error", "timeout_error"],
|
||||||
|
)
|
||||||
|
async def test_setup_entry_with_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_aqvify_client: MagicMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
error: Exception | None,
|
||||||
|
expected_state: ConfigEntryState,
|
||||||
|
) -> None:
|
||||||
|
"""Test setup entry with error."""
|
||||||
|
mock_aqvify_client.async_get_account_id.side_effect = error
|
||||||
|
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
assert mock_config_entry.state is expected_state
|
||||||
|
|
||||||
|
|
||||||
|
async def test_device_registry_integration(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_aqvify_client: MagicMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
device_registry: dr.DeviceRegistry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test device registry integration creates correct devices."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
# Get all devices created for this config entry
|
||||||
|
device_entries = dr.async_entries_for_config_entry(
|
||||||
|
device_registry, mock_config_entry.entry_id
|
||||||
|
)
|
||||||
|
|
||||||
|
# Snapshot the devices to ensure they have the correct structure
|
||||||
|
assert device_entries == snapshot
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""Test Aqvify sensor platform."""
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from syrupy.assertion 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
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_sensor_snapshot(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_aqvify_client: MagicMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
) -> None:
|
||||||
|
"""Test sensor setup for cloud connection."""
|
||||||
|
with patch("homeassistant.components.aqvify.PLATFORMS", [Platform.SENSOR]):
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
await snapshot_platform(
|
||||||
|
hass, entity_registry, snapshot, mock_config_entry.entry_id
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user