Skip to content

Commit 836b59a

Browse files
committed
Add appwrite integration
1 parent 760cbcc commit 836b59a

20 files changed

+1059
-0
lines changed

CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ build.json @home-assistant/supervisor
127127
/tests/components/application_credentials/ @home-assistant/core
128128
/homeassistant/components/apprise/ @caronc
129129
/tests/components/apprise/ @caronc
130+
/homeassistant/components/appwrite/ @ParitoshBh
131+
/tests/components/appwrite/ @ParitoshBh
130132
/homeassistant/components/aprilaire/ @chamberlain2007
131133
/tests/components/aprilaire/ @chamberlain2007
132134
/homeassistant/components/aprs/ @PhilRW
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""The Appwrite integration."""
2+
3+
from __future__ import annotations
4+
5+
from appwrite.client import AppwriteException
6+
7+
from homeassistant.core import HomeAssistant
8+
from homeassistant.exceptions import ConfigEntryAuthFailed
9+
10+
from .appwrite import AppwriteClient, AppwriteConfigEntry
11+
from .const import DOMAIN
12+
from .services import AppwriteServices
13+
14+
15+
async def async_setup_entry(
16+
hass: HomeAssistant, config_entry: AppwriteConfigEntry
17+
) -> bool:
18+
"""Save user data in Appwrite config entry and init services."""
19+
hass.data.setdefault(DOMAIN, {})
20+
hass.data[DOMAIN][config_entry.entry_id] = config_entry.data
21+
22+
# Set runtime data
23+
appwrite_client = AppwriteClient(dict(config_entry.data))
24+
25+
try:
26+
appwrite_client.async_validate_credentials()
27+
except AppwriteException as ae:
28+
raise ConfigEntryAuthFailed("Invalid credentials") from ae
29+
30+
config_entry.runtime_data = appwrite_client
31+
32+
# Setup services
33+
services = AppwriteServices(hass, config_entry)
34+
await services.setup()
35+
36+
return True
37+
38+
39+
async def async_unload_entry(hass: HomeAssistant, entry: AppwriteConfigEntry) -> bool:
40+
"""Unload services and config entry."""
41+
42+
for service in hass.services.async_services_for_domain(DOMAIN):
43+
hass.services.async_remove(DOMAIN, service)
44+
45+
hass.data[DOMAIN].pop(entry.entry_id)
46+
return True
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""Class for interacting with Appwrite instance."""
2+
3+
import logging
4+
from typing import Any
5+
6+
from appwrite.client import AppwriteException, Client
7+
from appwrite.services.functions import Functions
8+
from appwrite.services.health import Health
9+
10+
from homeassistant.config_entries import ConfigEntry
11+
from homeassistant.const import CONF_API_KEY, CONF_HOST
12+
from homeassistant.exceptions import HomeAssistantError
13+
14+
from .const import CONF_PROJECT_ID
15+
16+
_LOGGER = logging.getLogger(__name__)
17+
18+
19+
type AppwriteConfigEntry = ConfigEntry[AppwriteClient]
20+
21+
22+
class InvalidAuth(HomeAssistantError):
23+
"""Error to indicate there is invalid auth."""
24+
25+
26+
class InvalidUrl(HomeAssistantError):
27+
"""Error to indicate there is invalid url."""
28+
29+
30+
class AppwriteClient:
31+
"""Appwrite client for credential validation and services."""
32+
33+
def __init__(
34+
self,
35+
data: dict[str, Any],
36+
) -> None:
37+
"""Initialize the API client."""
38+
self.endpoint = f"{data[CONF_HOST]}/v1"
39+
self.project_id = data[CONF_PROJECT_ID]
40+
self.api_key = data[CONF_API_KEY]
41+
self._appwrite_client = (
42+
Client()
43+
.set_endpoint(self.endpoint)
44+
.set_project(self.project_id)
45+
.set_key(self.api_key)
46+
)
47+
48+
def async_validate_credentials(self) -> bool:
49+
"""Check if we can authenticate with the host."""
50+
try:
51+
health_api = Health(self._appwrite_client)
52+
result = health_api.get()
53+
_LOGGER.debug("Health API response: %s", result)
54+
except AppwriteException as ae:
55+
_LOGGER.error(ae.message)
56+
return False
57+
return True
58+
59+
def async_execute_function(
60+
self,
61+
function_id: Any | None,
62+
body: Any,
63+
path: Any,
64+
headers: Any,
65+
scheduled_at: Any,
66+
xasync: Any,
67+
method: Any,
68+
) -> None:
69+
"""Execute function."""
70+
functions = Functions(self._appwrite_client)
71+
_LOGGER.debug("Executed function '%s'", function_id)
72+
return functions.create_execution(
73+
function_id, body, xasync, path, method, headers, scheduled_at
74+
)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
"""Config flow for the Appwrite integration."""
2+
3+
from __future__ import annotations
4+
5+
from typing import Any
6+
7+
import voluptuous as vol
8+
9+
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
10+
from homeassistant.const import CONF_API_KEY, CONF_HOST
11+
from homeassistant.core import HomeAssistant
12+
import homeassistant.helpers.config_validation as cv
13+
14+
from .appwrite import AppwriteClient, InvalidAuth, InvalidUrl
15+
from .const import CONF_ENDPOINT, CONF_PROJECT_ID, CONF_TITLE, DOMAIN
16+
17+
STEP_APPWRITE_AUTH_SCHEMA = vol.Schema(
18+
{
19+
vol.Required(CONF_HOST): str,
20+
vol.Required(CONF_PROJECT_ID): str,
21+
vol.Required(CONF_API_KEY): str,
22+
}
23+
)
24+
25+
26+
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
27+
"""Validate if the user input allows us to connect to Appwrite instance."""
28+
try:
29+
# Cannot use cv.url validation in the schema itself so apply
30+
# extra validation here
31+
cv.url(data[CONF_HOST])
32+
except vol.Invalid as vi:
33+
raise InvalidUrl from vi
34+
35+
appwrite_client = AppwriteClient(data)
36+
if not await hass.async_add_executor_job(
37+
appwrite_client.async_validate_credentials
38+
):
39+
raise InvalidAuth
40+
41+
return {
42+
CONF_TITLE: f"{data[CONF_HOST]} - {data[CONF_PROJECT_ID]}",
43+
CONF_ENDPOINT: appwrite_client.endpoint,
44+
CONF_PROJECT_ID: appwrite_client.project_id,
45+
CONF_API_KEY: appwrite_client.api_key,
46+
}
47+
48+
49+
class AppwriteConfigFlow(ConfigFlow, domain=DOMAIN):
50+
"""Handle a config flow for Appwrite."""
51+
52+
VERSION = 1
53+
54+
async def async_step_user(
55+
self, user_input: dict[str, Any] | None = None
56+
) -> ConfigFlowResult:
57+
"""Handle the initial step."""
58+
errors: dict[str, str] = {}
59+
if user_input is not None:
60+
user_input[CONF_HOST] = user_input[CONF_HOST].rstrip("/")
61+
self._async_abort_entries_match(
62+
{
63+
CONF_HOST: user_input[CONF_HOST],
64+
CONF_PROJECT_ID: user_input[CONF_PROJECT_ID],
65+
}
66+
)
67+
try:
68+
info = await validate_input(self.hass, user_input)
69+
except InvalidAuth:
70+
errors["base"] = "invalid_auth"
71+
except InvalidUrl:
72+
errors["base"] = "invalid_url"
73+
else:
74+
return self.async_create_entry(title=info[CONF_TITLE], data=user_input)
75+
76+
return self.async_show_form(
77+
step_id="user", data_schema=STEP_APPWRITE_AUTH_SCHEMA, errors=errors
78+
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Constants for the Appwrite integration."""
2+
3+
DOMAIN = "appwrite"
4+
CONF_PROJECT_ID = "project_id"
5+
CONF_ENDPOINT = "endpoint"
6+
CONF_TITLE = "title"
7+
CONF_FIELDS = "fields"
8+
EXECUTE_FUNCTION = "execute_function"
9+
FUNCTION_BODY = "function_body"
10+
FUNCTION_ID = "function_id"
11+
FUNCTION_PATH = "function_path"
12+
FUNCTION_HEADERS = "function_headers"
13+
FUNCTION_SCHEDULED_AT = "function_scheduled_at"
14+
FUNCTION_ASYNC = "function_async"
15+
FUNCTION_METHOD = "function_method"
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"services": {
3+
"execute_function": {
4+
"service": "mdi:function"
5+
}
6+
}
7+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"domain": "appwrite",
3+
"name": "Appwrite",
4+
"codeowners": ["@ParitoshBh"],
5+
"config_flow": true,
6+
"dependencies": [],
7+
"documentation": "https://www.home-assistant.io/integrations/appwrite",
8+
"homekit": {},
9+
"iot_class": "cloud_polling",
10+
"requirements": ["appwrite==6.1.0"],
11+
"ssdp": [],
12+
"zeroconf": []
13+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
rules:
2+
# Bronze
3+
action-setup: done
4+
appropriate-polling:
5+
status: exempt
6+
comment: |
7+
This integration uses a push API. No polling required.
8+
brands: todo
9+
common-modules: done
10+
config-flow-test-coverage: done
11+
config-flow: done
12+
dependency-transparency: done
13+
docs-actions: todo
14+
docs-high-level-description: todo
15+
docs-installation-instructions: todo
16+
docs-removal-instructions: todo
17+
entity-event-setup:
18+
status: exempt
19+
comment: |
20+
No explicit event subscriptions.
21+
entity-unique-id:
22+
status: exempt
23+
comment: >
24+
No entities are registered.
25+
has-entity-name:
26+
status: exempt
27+
comment: >
28+
No entities are registered.
29+
runtime-data: done
30+
test-before-configure: done
31+
test-before-setup: done
32+
unique-config-entry: done
33+
34+
# Silver
35+
action-exceptions: todo
36+
config-entry-unloading: todo
37+
docs-configuration-parameters: todo
38+
docs-installation-parameters: todo
39+
entity-unavailable: todo
40+
integration-owner: todo
41+
log-when-unavailable: todo
42+
parallel-updates: todo
43+
reauthentication-flow: todo
44+
test-coverage: todo
45+
46+
# Gold
47+
devices: todo
48+
diagnostics: todo
49+
discovery-update-info: todo
50+
discovery: todo
51+
docs-data-update: todo
52+
docs-examples: todo
53+
docs-known-limitations: todo
54+
docs-supported-devices: todo
55+
docs-supported-functions: todo
56+
docs-troubleshooting: todo
57+
docs-use-cases: todo
58+
dynamic-devices: todo
59+
entity-category: todo
60+
entity-device-class: todo
61+
entity-disabled-by-default: todo
62+
entity-translations: todo
63+
exception-translations: todo
64+
icon-translations: todo
65+
reconfiguration-flow: todo
66+
repair-issues: todo
67+
stale-devices: todo
68+
69+
# Platinum
70+
async-dependency: todo
71+
inject-websession: todo
72+
strict-typing: todo

0 commit comments

Comments
 (0)