|
|
@@ -8,6 +8,7 @@ import voluptuous as vol
|
|
|
|
|
|
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow
|
|
|
from homeassistant.core import callback
|
|
|
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
|
from homeassistant.helpers.selector import (
|
|
|
NumberSelector,
|
|
|
NumberSelectorConfig,
|
|
|
@@ -21,6 +22,8 @@ from homeassistant.helpers.selector import (
|
|
|
)
|
|
|
|
|
|
from .const import (
|
|
|
+ CONF_API_BASE_URL,
|
|
|
+ CONF_API_TOKEN,
|
|
|
CONF_CALDAV_URL,
|
|
|
CONF_CALENDAR_NAME,
|
|
|
CONF_PASSWORD,
|
|
|
@@ -30,6 +33,7 @@ from .const import (
|
|
|
DEFAULT_REFRESH_INTERVAL,
|
|
|
DEFAULT_ROOM_STATE_NAME,
|
|
|
DOMAIN,
|
|
|
+ SYNC_MODE_API,
|
|
|
SYNC_MODE_CALDAV,
|
|
|
SYNC_MODE_MANUAL,
|
|
|
)
|
|
|
@@ -46,6 +50,9 @@ class RoomCalConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
|
async def async_step_reauth(
|
|
|
self, entry_data: dict[str, Any]
|
|
|
) -> ConfigFlowResult:
|
|
|
+ mode = self._get_reauth_entry().options.get(CONF_SYNC_MODE, SYNC_MODE_MANUAL)
|
|
|
+ if mode == SYNC_MODE_API:
|
|
|
+ return await self.async_step_reauth_api()
|
|
|
return await self.async_step_reauth_confirm()
|
|
|
|
|
|
async def async_step_reauth_confirm(
|
|
|
@@ -85,6 +92,38 @@ class RoomCalConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
|
errors=errors,
|
|
|
)
|
|
|
|
|
|
+ async def async_step_reauth_api(
|
|
|
+ self, user_input: dict[str, Any] | None = None
|
|
|
+ ) -> ConfigFlowResult:
|
|
|
+ errors: dict[str, str] = {}
|
|
|
+
|
|
|
+ if user_input is not None:
|
|
|
+ try:
|
|
|
+ await _test_api_connection(
|
|
|
+ self.hass,
|
|
|
+ self._get_reauth_entry().options.get(CONF_API_BASE_URL, ""),
|
|
|
+ user_input[CONF_API_TOKEN],
|
|
|
+ )
|
|
|
+ except _ApiAuthError:
|
|
|
+ errors["base"] = "invalid_auth"
|
|
|
+ except Exception:
|
|
|
+ errors["base"] = "cannot_connect"
|
|
|
+ else:
|
|
|
+ return self.async_update_reload_and_abort(
|
|
|
+ self._get_reauth_entry(),
|
|
|
+ data_updates={CONF_API_TOKEN: user_input[CONF_API_TOKEN]},
|
|
|
+ )
|
|
|
+
|
|
|
+ return self.async_show_form(
|
|
|
+ step_id="reauth_api",
|
|
|
+ data_schema=vol.Schema({
|
|
|
+ vol.Required(CONF_API_TOKEN): TextSelector(
|
|
|
+ TextSelectorConfig(type=TextSelectorType.PASSWORD)
|
|
|
+ ),
|
|
|
+ }),
|
|
|
+ errors=errors,
|
|
|
+ )
|
|
|
+
|
|
|
@staticmethod
|
|
|
@callback
|
|
|
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
|
|
@@ -108,6 +147,8 @@ class RoomCalOptionsFlow(OptionsFlow):
|
|
|
self._selected_mode = user_input[CONF_SYNC_MODE]
|
|
|
if self._selected_mode == SYNC_MODE_CALDAV:
|
|
|
return await self.async_step_caldav()
|
|
|
+ if self._selected_mode == SYNC_MODE_API:
|
|
|
+ return await self.async_step_api()
|
|
|
merged = {**self.config_entry.options, CONF_SYNC_MODE: self._selected_mode}
|
|
|
return self.async_create_entry(data=merged)
|
|
|
|
|
|
@@ -120,6 +161,7 @@ class RoomCalOptionsFlow(OptionsFlow):
|
|
|
options=[
|
|
|
{"value": SYNC_MODE_MANUAL, "label": "Manual"},
|
|
|
{"value": SYNC_MODE_CALDAV, "label": "CalDAV"},
|
|
|
+ {"value": SYNC_MODE_API, "label": "Room Cal API"},
|
|
|
],
|
|
|
mode=SelectSelectorMode.LIST,
|
|
|
)
|
|
|
@@ -175,8 +217,75 @@ class RoomCalOptionsFlow(OptionsFlow):
|
|
|
errors=errors,
|
|
|
)
|
|
|
|
|
|
+ async def async_step_api(
|
|
|
+ self, user_input: dict[str, Any] | None = None
|
|
|
+ ) -> ConfigFlowResult:
|
|
|
+ errors: dict[str, str] = {}
|
|
|
+
|
|
|
+ if user_input is not None:
|
|
|
+ api_token = user_input.pop(CONF_API_TOKEN, "") or self.config_entry.data.get(CONF_API_TOKEN, "")
|
|
|
+
|
|
|
+ try:
|
|
|
+ await _test_api_connection(
|
|
|
+ self.hass,
|
|
|
+ user_input.get(CONF_API_BASE_URL, ""),
|
|
|
+ api_token,
|
|
|
+ )
|
|
|
+ except _ApiAuthError:
|
|
|
+ errors["base"] = "invalid_auth"
|
|
|
+ except Exception:
|
|
|
+ errors["base"] = "cannot_connect"
|
|
|
+
|
|
|
+ if not errors:
|
|
|
+ new_data = {**self.config_entry.data, CONF_API_TOKEN: api_token}
|
|
|
+ self.hass.config_entries.async_update_entry(self.config_entry, data=new_data)
|
|
|
+ merged = {
|
|
|
+ **self.config_entry.options,
|
|
|
+ CONF_SYNC_MODE: self._selected_mode,
|
|
|
+ **user_input,
|
|
|
+ }
|
|
|
+ return self.async_create_entry(data=merged)
|
|
|
+
|
|
|
+ opts = self.config_entry.options
|
|
|
+ return self.async_show_form(
|
|
|
+ step_id="api",
|
|
|
+ data_schema=vol.Schema({
|
|
|
+ vol.Required(CONF_API_BASE_URL, default=opts.get(CONF_API_BASE_URL, "")): str,
|
|
|
+ vol.Optional(CONF_API_TOKEN, default=""): TextSelector(
|
|
|
+ TextSelectorConfig(type=TextSelectorType.PASSWORD)
|
|
|
+ ),
|
|
|
+ vol.Optional(
|
|
|
+ CONF_REFRESH_INTERVAL,
|
|
|
+ default=opts.get(CONF_REFRESH_INTERVAL, DEFAULT_REFRESH_INTERVAL),
|
|
|
+ ): NumberSelector(
|
|
|
+ NumberSelectorConfig(min=1, max=1440, unit_of_measurement="min", mode=NumberSelectorMode.BOX)
|
|
|
+ ),
|
|
|
+ }),
|
|
|
+ errors=errors,
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+class _ApiAuthError(Exception):
|
|
|
+ pass
|
|
|
+
|
|
|
|
|
|
def _test_caldav_auth(url: str, username: str, password: str) -> None:
|
|
|
"""Blocking: verify credentials by fetching the principal."""
|
|
|
client = caldav.DAVClient(url=url, username=username, password=password)
|
|
|
client.principal()
|
|
|
+
|
|
|
+
|
|
|
+async def _test_api_connection(hass: Any, base_url: str, token: str) -> None:
|
|
|
+ """Async: verify API connectivity by hitting /status."""
|
|
|
+ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
|
+ session = async_get_clientsession(hass)
|
|
|
+ url = base_url.rstrip("/") + "/status"
|
|
|
+ try:
|
|
|
+ async with session.get(url, headers={"Authorization": f"Bearer {token}"}) as resp:
|
|
|
+ if resp.status in (401, 403):
|
|
|
+ raise _ApiAuthError(f"HTTP {resp.status}")
|
|
|
+ resp.raise_for_status()
|
|
|
+ except _ApiAuthError:
|
|
|
+ raise
|
|
|
+ except Exception as exc:
|
|
|
+ raise Exception(f"Cannot connect: {exc}") from exc
|