Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add appwrite integration #134249

Draft
wants to merge 8 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 4 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
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ build.json @home-assistant/supervisor
/tests/components/application_credentials/ @home-assistant/core
/homeassistant/components/apprise/ @caronc
/tests/components/apprise/ @caronc
/homeassistant/components/appwrite/ @ParitoshBh
/tests/components/appwrite/ @ParitoshBh
/homeassistant/components/aprilaire/ @chamberlain2007
/tests/components/aprilaire/ @chamberlain2007
/homeassistant/components/aprs/ @PhilRW
Expand Down
46 changes: 46 additions & 0 deletions homeassistant/components/appwrite/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""The Appwrite integration."""

from __future__ import annotations

from appwrite.client import AppwriteException

from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed

from .appwrite import AppwriteClient, AppwriteConfigEntry
from .const import DOMAIN
from .services import AppwriteServices


async def async_setup_entry(
hass: HomeAssistant, config_entry: AppwriteConfigEntry
) -> bool:
"""Save user data in Appwrite config entry and init services."""
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = config_entry.data

# Set runtime data
appwrite_client = AppwriteClient(dict(config_entry.data))

try:
appwrite_client.async_validate_credentials()
except AppwriteException as ae:
raise ConfigEntryAuthFailed("Invalid credentials") from ae

config_entry.runtime_data = appwrite_client

# Setup services
services = AppwriteServices(hass, config_entry)
await services.setup()

return True


async def async_unload_entry(hass: HomeAssistant, entry: AppwriteConfigEntry) -> bool:
"""Unload services and config entry."""

for service in hass.services.async_services_for_domain(DOMAIN):
hass.services.async_remove(DOMAIN, service)

hass.data[DOMAIN].pop(entry.entry_id)
return True
74 changes: 74 additions & 0 deletions homeassistant/components/appwrite/appwrite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Class for interacting with Appwrite instance."""

import logging
from typing import Any

from appwrite.client import AppwriteException, Client
from appwrite.services.functions import Functions
from appwrite.services.health import Health

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_HOST
from homeassistant.exceptions import HomeAssistantError

from .const import CONF_PROJECT_ID

_LOGGER = logging.getLogger(__name__)


type AppwriteConfigEntry = ConfigEntry[AppwriteClient]


class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""


class InvalidUrl(HomeAssistantError):
"""Error to indicate there is invalid url."""


class AppwriteClient:
"""Appwrite client for credential validation and services."""

def __init__(
self,
data: dict[str, Any],
) -> None:
"""Initialize the API client."""
self.endpoint = f"{data[CONF_HOST]}/v1"
self.project_id = data[CONF_PROJECT_ID]
self.api_key = data[CONF_API_KEY]
self._appwrite_client = (
Client()
.set_endpoint(self.endpoint)
.set_project(self.project_id)
.set_key(self.api_key)
)

def async_validate_credentials(self) -> bool:
"""Check if we can authenticate with the host."""
try:
health_api = Health(self._appwrite_client)
result = health_api.get()
_LOGGER.debug("Health API response: %s", result)
except AppwriteException as ae:
_LOGGER.error(ae.message)
return False
return True

def async_execute_function(
self,
function_id: Any | None,
body: Any,
path: Any,
headers: Any,
scheduled_at: Any,
xasync: Any,
method: Any,
) -> None:
"""Execute function."""
functions = Functions(self._appwrite_client)
_LOGGER.debug("Executed function '%s'", function_id)
return functions.create_execution(
function_id, body, xasync, path, method, headers, scheduled_at
)
78 changes: 78 additions & 0 deletions homeassistant/components/appwrite/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Config flow for the Appwrite integration."""

from __future__ import annotations

from typing import Any

import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_HOST
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv

from .appwrite import AppwriteClient, InvalidAuth, InvalidUrl
from .const import CONF_ENDPOINT, CONF_PROJECT_ID, CONF_TITLE, DOMAIN

STEP_APPWRITE_AUTH_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PROJECT_ID): str,
vol.Required(CONF_API_KEY): str,
}
)


async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate if the user input allows us to connect to Appwrite instance."""
try:
# Cannot use cv.url validation in the schema itself so apply
# extra validation here
cv.url(data[CONF_HOST])
except vol.Invalid as vi:
raise InvalidUrl from vi

appwrite_client = AppwriteClient(data)
if not await hass.async_add_executor_job(
appwrite_client.async_validate_credentials
):
raise InvalidAuth

return {
CONF_TITLE: f"{data[CONF_HOST]} - {data[CONF_PROJECT_ID]}",
CONF_ENDPOINT: appwrite_client.endpoint,
CONF_PROJECT_ID: appwrite_client.project_id,
CONF_API_KEY: appwrite_client.api_key,
}


class AppwriteConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Appwrite."""

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:
user_input[CONF_HOST] = user_input[CONF_HOST].rstrip("/")
self._async_abort_entries_match(
{
CONF_HOST: user_input[CONF_HOST],
CONF_PROJECT_ID: user_input[CONF_PROJECT_ID],
}
)
try:
info = await validate_input(self.hass, user_input)
except InvalidAuth:
errors["base"] = "invalid_auth"
except InvalidUrl:
errors["base"] = "invalid_url"
else:
return self.async_create_entry(title=info[CONF_TITLE], data=user_input)

return self.async_show_form(
step_id="user", data_schema=STEP_APPWRITE_AUTH_SCHEMA, errors=errors
)
15 changes: 15 additions & 0 deletions homeassistant/components/appwrite/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Constants for the Appwrite integration."""

DOMAIN = "appwrite"
CONF_PROJECT_ID = "project_id"
CONF_ENDPOINT = "endpoint"
CONF_TITLE = "title"
CONF_FIELDS = "fields"
EXECUTE_FUNCTION = "execute_function"
FUNCTION_BODY = "function_body"
FUNCTION_ID = "function_id"
FUNCTION_PATH = "function_path"
FUNCTION_HEADERS = "function_headers"
FUNCTION_SCHEDULED_AT = "function_scheduled_at"
FUNCTION_ASYNC = "function_async"
FUNCTION_METHOD = "function_method"
7 changes: 7 additions & 0 deletions homeassistant/components/appwrite/icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"services": {
"execute_function": {
"service": "mdi:function"
}
}
}
13 changes: 13 additions & 0 deletions homeassistant/components/appwrite/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"domain": "appwrite",
"name": "Appwrite",
"codeowners": ["@ParitoshBh"],
"config_flow": true,
"dependencies": [],
"documentation": "https://www.home-assistant.io/integrations/appwrite",
"homekit": {},
"iot_class": "cloud_push",
"requirements": ["appwrite==6.1.0"],
"ssdp": [],
"zeroconf": []
}
72 changes: 72 additions & 0 deletions homeassistant/components/appwrite/quality_scale.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
rules:
# Bronze
action-setup: done
appropriate-polling:
status: exempt
comment: |
This integration uses a push API. No polling required.
brands: todo
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions: todo
docs-high-level-description: todo
docs-installation-instructions: todo
docs-removal-instructions: todo
entity-event-setup:
status: exempt
comment: |
No explicit event subscriptions.
entity-unique-id:
status: exempt
comment: >
No entities are registered.
has-entity-name:
status: exempt
comment: >
No entities are registered.
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done

# Silver
action-exceptions: todo
config-entry-unloading: todo
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: todo
integration-owner: todo
log-when-unavailable: todo
parallel-updates: todo
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: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo

# Platinum
async-dependency: todo
inject-websession: todo
strict-typing: todo
Loading