Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CODEOWNERS

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions homeassistant/components/aqvify/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""The Aqvify integration."""

import logging

from pyaqvify import AqvifyAPI, AqvifyAuthException

from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession

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."""

_api = AqvifyAPI(entry.data[CONF_API_KEY], websession=async_get_clientsession(hass))
try:
await _api.async_get_account_id()
except AqvifyAuthException as err:
_LOGGER.error("Invalid API key for Aqvify API: %s", err)
raise ConfigEntryAuthFailed from err
except Exception as err:
_LOGGER.error("Failed to connect to Aqvify API: %s", err)
raise ConfigEntryNotReady from err
Comment thread
astrandb marked this conversation as resolved.
Outdated

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)
89 changes: 89 additions & 0 deletions homeassistant/components/aqvify/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Config flow for the Aqvify integration."""

import logging
from typing import Any

from aiohttp import ClientResponseError
from pyaqvify import AqvifyAccount, AqvifyAPI, AqvifyAuthException
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
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,
}
)


async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect.

Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""

hub = AqvifyAPI(data[CONF_API_KEY], websession=async_get_clientsession(hass))

try:
data = await hub.async_get_account_id()
Comment thread
astrandb marked this conversation as resolved.
Outdated
except AqvifyAuthException as err:
raise InvalidAuth from err
except ClientResponseError as err:
raise CannotConnect from err
account_id = AqvifyAccount(data).account_id
return {"title": "Aqvify", "account_id": account_id}


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:
try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(
unique_id=info["account_id"], raise_on_progress=True
Comment thread
astrandb marked this conversation as resolved.
Outdated
)
self._abort_if_unique_id_configured(
updates=user_input, reload_on_update=True
Comment thread
astrandb marked this conversation as resolved.
Outdated
)
return self.async_create_entry(title=info["title"], 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",
},
)


class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""


class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
Comment thread
astrandb marked this conversation as resolved.
Outdated
3 changes: 3 additions & 0 deletions homeassistant/components/aqvify/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Constants for the Aqvify integration."""

DOMAIN = "aqvify"
87 changes: 87 additions & 0 deletions homeassistant/components/aqvify/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Coordinator for Aqvify integration."""

from dataclasses import dataclass
from datetime import timedelta
import logging

from aiohttp import ClientResponseError
from pyaqvify import AqvifyAPI, AqvifyDeviceData, AqvifyDevices

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
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)
Comment thread
astrandb marked this conversation as resolved.

type AqvifyConfigEntry = ConfigEntry[AqvifyCoordinator]


@dataclass
class AqvifyDeviceInfo:
"""Data about the Aqvify device."""

device_key: str
device_name: str
Comment thread
astrandb marked this conversation as resolved.
Outdated


@dataclass
class AqvifyCoordinatorData:
"""Data class for storing coordinator data."""

account_id: str
devices: AqvifyDevices
device_data: dict[str, AqvifyDeviceData]


class AqvifyCoordinator(DataUpdateCoordinator[AqvifyCoordinatorData]):
"""Data update coordinator for Aqvify devices."""

config_entry: AqvifyConfigEntry
device_info: AqvifyDeviceInfo

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_update_data(self) -> AqvifyCoordinatorData:
"""Fetch device state."""
try:
_data = await self.api_client.async_get_devices()
devices = AqvifyDevices(_data)
Comment thread
astrandb marked this conversation as resolved.
Outdated
Comment thread
astrandb marked this conversation as resolved.
Outdated
except ClientResponseError as err:
raise UpdateFailed(f"Error communicating with device: {err}") from err
Comment thread
astrandb marked this conversation as resolved.
Outdated
except TimeoutError as err:
raise UpdateFailed(f"Unexpected response: {err}") from err
Comment thread
astrandb marked this conversation as resolved.
Outdated

device_data = {}
for device in devices.devices.values():
try:
device_key = str(device.device_key)
_data = await self.api_client.async_get_device_latest_data(device_key)
device_data[device_key] = AqvifyDeviceData(_data)
except ClientResponseError as err:
raise UpdateFailed(f"Error communicating with device: {err}") from err
except TimeoutError as err:
raise UpdateFailed(f"Unexpected response: {err}") from err

return AqvifyCoordinatorData(
account_id=str(self.config_entry.unique_id),
Comment thread
astrandb marked this conversation as resolved.
Outdated
devices=devices,
device_data=device_data,
)
34 changes: 34 additions & 0 deletions homeassistant/components/aqvify/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""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)

self.device_key = device_key
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device_key)},
name=coordinator.data.devices.devices[device_key].name,
Comment thread
astrandb marked this conversation as resolved.
manufacturer="Aqvify",
configuration_url="https://app.aqvify.com",
serial_number=device_key,
)
self._attr_unique_id = f"{device_key}_{description.key}"
self.entity_description = description
12 changes: 12 additions & 0 deletions homeassistant/components/aqvify/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"entity": {
"sensor": {
"meter_value": {
"default": "mdi:water-well"
},
"water_level": {
"default": "mdi:water-well"
}
}
}
}
12 changes: 12 additions & 0 deletions homeassistant/components/aqvify/manifest.json
Original file line number Diff line number Diff line change
@@ -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.6"]
}
69 changes: 69 additions & 0 deletions homeassistant/components/aqvify/quality_scale.yaml
Original file line number Diff line number Diff line change
@@ -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 does not explicitly subscribe to events.
Comment thread
astrandb marked this conversation as resolved.
Outdated
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
Loading
Loading