Просмотр исходного кода

Add Room Cal API backend with status sensors

Implements a new sync mode (api) that fetches reservations from the
room_cal HTTP API and exposes its /status health data as HA sensors.

- backends/api.py: RoomCalApiBackend — fetches /reservation/{today}/{+90d}
  for current/next slots; async_fetch_status() hits /status; Bearer token auth
- models.py: ApiStatusData dataclass (status, checks, checked_at, stale)
- coordinator.py: uses RoomCalApiBackend when sync_mode=api; passes
  api_status through CoordinatorData
- sensor.py: ApiStatusSensor and ApiCheckedAtSensor created when mode=api
- config_flow.py: async_step_api options step (api_base_url + api_token);
  reauth dispatches to reauth_api step for API mode
- strings/translations: api options step and reauth_api step strings
ZiB 4 месяцев назад
Родитель
Сommit
51e01312ff

+ 2 - 0
custom_components/room_cal_ha/backends/__init__.py

@@ -1,3 +1,4 @@
+from .api import RoomCalApiBackend
 from .base import BackendAuthError, BackendConnectError, ReservationBackend
 from .base import BackendAuthError, BackendConnectError, ReservationBackend
 from .caldav import CalDavBackend
 from .caldav import CalDavBackend
 
 
@@ -6,4 +7,5 @@ __all__ = [
   "BackendConnectError",
   "BackendConnectError",
   "ReservationBackend",
   "ReservationBackend",
   "CalDavBackend",
   "CalDavBackend",
+  "RoomCalApiBackend",
 ]
 ]

+ 108 - 0
custom_components/room_cal_ha/backends/api.py

@@ -0,0 +1,108 @@
+from __future__ import annotations
+
+import logging
+from datetime import date, datetime, timedelta
+from typing import TYPE_CHECKING
+
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+
+from ..const import CONF_API_BASE_URL, CONF_API_TOKEN
+from ..models import ApiStatusData, ReservationData
+from .base import BackendAuthError, BackendConnectError, ReservationBackend
+
+if TYPE_CHECKING:
+  from homeassistant.config_entries import ConfigEntry
+  from homeassistant.core import HomeAssistant
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class RoomCalApiBackend(ReservationBackend):
+
+  def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
+    self._hass = hass
+    self._entry = entry
+
+  def _base_url(self) -> str:
+    return self._entry.options.get(CONF_API_BASE_URL, "").rstrip("/")
+
+  def _headers(self) -> dict[str, str]:
+    # TODO: Get token — currently uses the stored api_token from entry.data
+    token = self._entry.data.get(CONF_API_TOKEN, "")
+    return {"Authorization": f"Bearer {token}"}
+
+  async def async_fetch(self) -> tuple[ReservationData | None, ReservationData | None]:
+    session = async_get_clientsession(self._hass)
+    today = date.today()
+    end = today + timedelta(days=90)
+    url = f"{self._base_url()}/reservation/{today}/{end}"
+    try:
+      async with session.get(url, headers=self._headers()) as resp:
+        if resp.status in (401, 403):
+          raise BackendAuthError(f"API returned HTTP {resp.status}")
+        resp.raise_for_status()
+        data: dict[str, dict | None] = await resp.json()
+    except BackendAuthError:
+      raise
+    except Exception as exc:
+      raise BackendConnectError(str(exc)) from exc
+
+    current_raw = data.get(str(today))
+
+    seen_checkins: set[str] = set()
+    if current_raw:
+      seen_checkins.add(current_raw.get("checkin_date", ""))
+
+    next_raw: dict | None = None
+    for d_str in sorted(data.keys()):
+      slot = data[d_str]
+      if slot is None:
+        continue
+      checkin = slot.get("checkin_date", "")
+      if checkin > str(today) and checkin not in seen_checkins:
+        seen_checkins.add(checkin)
+        next_raw = slot
+        break
+
+    return _parse_reservation(current_raw), _parse_reservation(next_raw)
+
+  async def async_fetch_status(self) -> ApiStatusData:
+    session = async_get_clientsession(self._hass)
+    url = f"{self._base_url()}/status"
+    try:
+      async with session.get(url, headers=self._headers()) as resp:
+        if resp.status in (401, 403):
+          return ApiStatusData(status="auth_error")
+        body = await resp.json()
+        stale = resp.status != 200
+    except Exception as exc:
+      _LOGGER.debug("Failed to fetch API status: %s", exc)
+      return ApiStatusData(status="unknown")
+
+    checked_at: datetime | None = None
+    if raw := body.get("checked_at"):
+      try:
+        checked_at = datetime.fromisoformat(raw)
+      except ValueError:
+        pass
+
+    return ApiStatusData(
+      status=body.get("status", "unknown"),
+      checks=body.get("checks", {}),
+      checked_at=checked_at,
+      stale=stale,
+    )
+
+
+def _parse_reservation(data: dict | None) -> ReservationData | None:
+  if data is None:
+    return None
+  checkin_str = data.get("checkin_date")
+  checkout_str = data.get("checkout_date")
+  return ReservationData(
+    guest_name=data.get("guest_name"),
+    check_in_date=date.fromisoformat(checkin_str) if checkin_str else None,
+    check_out_date=date.fromisoformat(checkout_str) if checkout_str else None,
+    wifi_key=data.get("wifi_key"),
+    door_code=data.get("door_code"),
+  )

+ 109 - 0
custom_components/room_cal_ha/config_flow.py

@@ -8,6 +8,7 @@ import voluptuous as vol
 
 
 from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow
 from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult, OptionsFlow
 from homeassistant.core import callback
 from homeassistant.core import callback
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
 from homeassistant.helpers.selector import (
 from homeassistant.helpers.selector import (
   NumberSelector,
   NumberSelector,
   NumberSelectorConfig,
   NumberSelectorConfig,
@@ -21,6 +22,8 @@ from homeassistant.helpers.selector import (
 )
 )
 
 
 from .const import (
 from .const import (
+  CONF_API_BASE_URL,
+  CONF_API_TOKEN,
   CONF_CALDAV_URL,
   CONF_CALDAV_URL,
   CONF_CALENDAR_NAME,
   CONF_CALENDAR_NAME,
   CONF_PASSWORD,
   CONF_PASSWORD,
@@ -30,6 +33,7 @@ from .const import (
   DEFAULT_REFRESH_INTERVAL,
   DEFAULT_REFRESH_INTERVAL,
   DEFAULT_ROOM_STATE_NAME,
   DEFAULT_ROOM_STATE_NAME,
   DOMAIN,
   DOMAIN,
+  SYNC_MODE_API,
   SYNC_MODE_CALDAV,
   SYNC_MODE_CALDAV,
   SYNC_MODE_MANUAL,
   SYNC_MODE_MANUAL,
 )
 )
@@ -46,6 +50,9 @@ class RoomCalConfigFlow(ConfigFlow, domain=DOMAIN):
   async def async_step_reauth(
   async def async_step_reauth(
     self, entry_data: dict[str, Any]
     self, entry_data: dict[str, Any]
   ) -> ConfigFlowResult:
   ) -> 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()
     return await self.async_step_reauth_confirm()
 
 
   async def async_step_reauth_confirm(
   async def async_step_reauth_confirm(
@@ -85,6 +92,38 @@ class RoomCalConfigFlow(ConfigFlow, domain=DOMAIN):
       errors=errors,
       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
   @staticmethod
   @callback
   @callback
   def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
   def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
@@ -108,6 +147,8 @@ class RoomCalOptionsFlow(OptionsFlow):
       self._selected_mode = user_input[CONF_SYNC_MODE]
       self._selected_mode = user_input[CONF_SYNC_MODE]
       if self._selected_mode == SYNC_MODE_CALDAV:
       if self._selected_mode == SYNC_MODE_CALDAV:
         return await self.async_step_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}
       merged = {**self.config_entry.options, CONF_SYNC_MODE: self._selected_mode}
       return self.async_create_entry(data=merged)
       return self.async_create_entry(data=merged)
 
 
@@ -120,6 +161,7 @@ class RoomCalOptionsFlow(OptionsFlow):
             options=[
             options=[
               {"value": SYNC_MODE_MANUAL, "label": "Manual"},
               {"value": SYNC_MODE_MANUAL, "label": "Manual"},
               {"value": SYNC_MODE_CALDAV, "label": "CalDAV"},
               {"value": SYNC_MODE_CALDAV, "label": "CalDAV"},
+              {"value": SYNC_MODE_API, "label": "Room Cal API"},
             ],
             ],
             mode=SelectSelectorMode.LIST,
             mode=SelectSelectorMode.LIST,
           )
           )
@@ -175,8 +217,75 @@ class RoomCalOptionsFlow(OptionsFlow):
       errors=errors,
       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:
 def _test_caldav_auth(url: str, username: str, password: str) -> None:
   """Blocking: verify credentials by fetching the principal."""
   """Blocking: verify credentials by fetching the principal."""
   client = caldav.DAVClient(url=url, username=username, password=password)
   client = caldav.DAVClient(url=url, username=username, password=password)
   client.principal()
   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

+ 4 - 0
custom_components/room_cal_ha/const.py

@@ -14,6 +14,10 @@ CONF_SYNC_MODE = "sync_mode"
 CONF_CALDAV_URL = "caldav_url"
 CONF_CALDAV_URL = "caldav_url"
 CONF_CALENDAR_NAME = "calendar_name"
 CONF_CALENDAR_NAME = "calendar_name"
 CONF_REFRESH_INTERVAL = "refresh_interval"
 CONF_REFRESH_INTERVAL = "refresh_interval"
+CONF_API_BASE_URL = "api_base_url"
+
+# entry.data keys (sensitive)
+CONF_API_TOKEN = "api_token"
 
 
 # Sync mode values
 # Sync mode values
 SYNC_MODE_CALDAV = "caldav"
 SYNC_MODE_CALDAV = "caldav"

+ 18 - 2
custom_components/room_cal_ha/coordinator.py

@@ -11,7 +11,7 @@ from homeassistant.helpers.event import async_call_later
 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
 from homeassistant.util import dt as dt_util
 from homeassistant.util import dt as dt_util
 
 
-from .backends import BackendAuthError, BackendConnectError, CalDavBackend
+from .backends import BackendAuthError, BackendConnectError, CalDavBackend, RoomCalApiBackend
 from .const import (
 from .const import (
   CONF_REFRESH_INTERVAL,
   CONF_REFRESH_INTERVAL,
   CONF_SYNC_MODE,
   CONF_SYNC_MODE,
@@ -19,9 +19,10 @@ from .const import (
   DOMAIN,
   DOMAIN,
   STORE_CURRENT,
   STORE_CURRENT,
   STORE_NEXT,
   STORE_NEXT,
+  SYNC_MODE_API,
   SYNC_MODE_CALDAV,
   SYNC_MODE_CALDAV,
 )
 )
-from .models import ReservationData
+from .models import ApiStatusData, ReservationData
 from .store import ReservationStore
 from .store import ReservationStore
 
 
 _LOGGER = logging.getLogger(__name__)
 _LOGGER = logging.getLogger(__name__)
@@ -33,6 +34,7 @@ class CoordinatorData:
   next: ReservationData
   next: ReservationData
   current_last_refresh: datetime | None
   current_last_refresh: datetime | None
   next_last_refresh: datetime | None
   next_last_refresh: datetime | None
+  api_status: ApiStatusData | None = None
 
 
 
 
 class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
 class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
@@ -62,6 +64,7 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
     backend_current: ReservationData | None = None
     backend_current: ReservationData | None = None
     backend_next: ReservationData | None = None
     backend_next: ReservationData | None = None
     backend_fetched = False
     backend_fetched = False
+    api_status: ApiStatusData | None = None
 
 
     sync_mode = self.entry.options.get(CONF_SYNC_MODE, "")
     sync_mode = self.entry.options.get(CONF_SYNC_MODE, "")
 
 
@@ -76,6 +79,18 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
       except BackendConnectError as exc:
       except BackendConnectError as exc:
         _LOGGER.warning("CalDAV fetch failed, using cached data: %s", exc)
         _LOGGER.warning("CalDAV fetch failed, using cached data: %s", exc)
 
 
+    elif sync_mode == SYNC_MODE_API:
+      api_backend = RoomCalApiBackend(self.hass, self.entry)
+      try:
+        backend_current, backend_next = await api_backend.async_fetch()
+        backend_fetched = True
+      except BackendAuthError:
+        self.entry.async_start_reauth(self.hass)
+        raise UpdateFailed("API authorization failed; re-auth required")
+      except BackendConnectError as exc:
+        _LOGGER.warning("API fetch failed, using cached data: %s", exc)
+      api_status = await api_backend.async_fetch_status()
+
     now = dt_util.utcnow()
     now = dt_util.utcnow()
 
 
     if backend_fetched:
     if backend_fetched:
@@ -95,6 +110,7 @@ class RoomCalCoordinator(DataUpdateCoordinator[CoordinatorData]):
       next=new_next,
       next=new_next,
       current_last_refresh=self._current_last_refresh,
       current_last_refresh=self._current_last_refresh,
       next_last_refresh=self._next_last_refresh,
       next_last_refresh=self._next_last_refresh,
+      api_status=api_status,
     )
     )
 
 
   def _reschedule_mode_sync(self, current: ReservationData, nxt: ReservationData) -> None:
   def _reschedule_mode_sync(self, current: ReservationData, nxt: ReservationData) -> None:

+ 10 - 2
custom_components/room_cal_ha/models.py

@@ -1,10 +1,18 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
-from dataclasses import dataclass, fields
-from datetime import date, time
+from dataclasses import dataclass, field, fields
+from datetime import date, datetime, time
 from typing import Any
 from typing import Any
 
 
 
 
+@dataclass
+class ApiStatusData:
+  status: str = "unknown"
+  checks: dict = field(default_factory=dict)
+  checked_at: datetime | None = None
+  stale: bool = False
+
+
 @dataclass
 @dataclass
 class ReservationData:
 class ReservationData:
   guest_name: str | None = "<No Guest>"
   guest_name: str | None = "<No Guest>"

+ 72 - 1
custom_components/room_cal_ha/sensor.py

@@ -13,14 +13,18 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
 from .const import (
 from .const import (
   CONF_CURRENT_NAME,
   CONF_CURRENT_NAME,
   CONF_NEXT_NAME,
   CONF_NEXT_NAME,
+  CONF_ROOM_STATE_NAME,
+  CONF_SYNC_MODE,
   DEFAULT_CURRENT_NAME,
   DEFAULT_CURRENT_NAME,
   DEFAULT_NEXT_NAME,
   DEFAULT_NEXT_NAME,
+  DEFAULT_ROOM_STATE_NAME,
   DOMAIN,
   DOMAIN,
   STORE_CURRENT,
   STORE_CURRENT,
   STORE_NEXT,
   STORE_NEXT,
+  SYNC_MODE_API,
 )
 )
 from .coordinator import CoordinatorData, RoomCalCoordinator
 from .coordinator import CoordinatorData, RoomCalCoordinator
-from .models import ReservationData
+from .models import ApiStatusData, ReservationData
 
 
 
 
 async def async_setup_entry(
 async def async_setup_entry(
@@ -53,6 +57,17 @@ async def async_setup_entry(
       LastRefreshSensor(coordinator, entry, slot, device_info),
       LastRefreshSensor(coordinator, entry, slot, device_info),
     ]
     ]
 
 
+  if entry.options.get(CONF_SYNC_MODE) == SYNC_MODE_API:
+    room_state_name = entry.data.get(CONF_ROOM_STATE_NAME, DEFAULT_ROOM_STATE_NAME)
+    room_state_device = DeviceInfo(
+      identifiers={(DOMAIN, entry.entry_id)},
+      name=room_state_name,
+    )
+    entities += [
+      ApiStatusSensor(coordinator, entry, room_state_device),
+      ApiCheckedAtSensor(coordinator, entry, room_state_device),
+    ]
+
   async_add_entities(entities)
   async_add_entities(entities)
 
 
 
 
@@ -188,3 +203,59 @@ class LastRefreshSensor(CoordinatorEntity[RoomCalCoordinator], SensorEntity):
       if self._slot == STORE_CURRENT
       if self._slot == STORE_CURRENT
       else data.next_last_refresh
       else data.next_last_refresh
     )
     )
+
+
+class ApiStatusSensor(CoordinatorEntity[RoomCalCoordinator], SensorEntity):
+  _attr_has_entity_name = True
+  _attr_name = "API Status"
+  _attr_icon = "mdi:api"
+
+  def __init__(
+    self,
+    coordinator: RoomCalCoordinator,
+    entry: ConfigEntry,
+    device_info: DeviceInfo,
+  ) -> None:
+    super().__init__(coordinator)
+    self._attr_unique_id = f"{entry.entry_id}_api_status"
+    self._attr_device_info = device_info
+
+  def _status(self) -> ApiStatusData | None:
+    return self.coordinator.data.api_status
+
+  @property
+  def native_value(self) -> str | None:
+    s = self._status()
+    return s.status if s is not None else None
+
+  @property
+  def extra_state_attributes(self) -> dict | None:
+    s = self._status()
+    if s is None:
+      return None
+    attrs: dict = {"checks": s.checks, "stale": s.stale}
+    if s.checked_at is not None:
+      attrs["checked_at"] = s.checked_at.isoformat()
+    return attrs
+
+
+class ApiCheckedAtSensor(CoordinatorEntity[RoomCalCoordinator], SensorEntity):
+  _attr_has_entity_name = True
+  _attr_name = "API Last Checked"
+  _attr_device_class = SensorDeviceClass.TIMESTAMP
+  _attr_icon = "mdi:clock-check-outline"
+
+  def __init__(
+    self,
+    coordinator: RoomCalCoordinator,
+    entry: ConfigEntry,
+    device_info: DeviceInfo,
+  ) -> None:
+    super().__init__(coordinator)
+    self._attr_unique_id = f"{entry.entry_id}_api_checked_at"
+    self._attr_device_info = device_info
+
+  @property
+  def native_value(self) -> datetime | None:
+    s = self.coordinator.data.api_status
+    return s.checked_at if s is not None else None

+ 17 - 2
custom_components/room_cal_ha/strings.json

@@ -8,11 +8,18 @@
           "username": "Username",
           "username": "Username",
           "password": "Password"
           "password": "Password"
         }
         }
+      },
+      "reauth_api": {
+        "title": "Re-authenticate Room Cal API",
+        "description": "Enter your API token to restore the connection.",
+        "data": {
+          "api_token": "API Token"
+        }
       }
       }
     },
     },
     "error": {
     "error": {
       "invalid_auth": "Invalid credentials.",
       "invalid_auth": "Invalid credentials.",
-      "cannot_connect": "Cannot connect to the CalDAV server."
+      "cannot_connect": "Cannot connect to the server."
     },
     },
     "abort": {
     "abort": {
       "already_configured": "This integration is already configured."
       "already_configured": "This integration is already configured."
@@ -35,11 +42,19 @@
           "password": "Password",
           "password": "Password",
           "refresh_interval": "Refresh Interval"
           "refresh_interval": "Refresh Interval"
         }
         }
+      },
+      "api": {
+        "title": "Room Cal API Settings",
+        "data": {
+          "api_base_url": "API Base URL",
+          "api_token": "API Token",
+          "refresh_interval": "Refresh Interval"
+        }
       }
       }
     },
     },
     "error": {
     "error": {
       "invalid_auth": "Invalid credentials.",
       "invalid_auth": "Invalid credentials.",
-      "cannot_connect": "Cannot connect to the CalDAV server."
+      "cannot_connect": "Cannot connect to the server."
     }
     }
   },
   },
   "services": {
   "services": {

+ 17 - 2
custom_components/room_cal_ha/translations/en.json

@@ -8,11 +8,18 @@
           "username": "Username",
           "username": "Username",
           "password": "Password"
           "password": "Password"
         }
         }
+      },
+      "reauth_api": {
+        "title": "Re-authenticate Room Cal API",
+        "description": "Enter your API token to restore the connection.",
+        "data": {
+          "api_token": "API Token"
+        }
       }
       }
     },
     },
     "error": {
     "error": {
       "invalid_auth": "Invalid credentials.",
       "invalid_auth": "Invalid credentials.",
-      "cannot_connect": "Cannot connect to the CalDAV server."
+      "cannot_connect": "Cannot connect to the server."
     },
     },
     "abort": {
     "abort": {
       "already_configured": "This integration is already configured."
       "already_configured": "This integration is already configured."
@@ -35,11 +42,19 @@
           "password": "Password",
           "password": "Password",
           "refresh_interval": "Refresh Interval"
           "refresh_interval": "Refresh Interval"
         }
         }
+      },
+      "api": {
+        "title": "Room Cal API Settings",
+        "data": {
+          "api_base_url": "API Base URL",
+          "api_token": "API Token",
+          "refresh_interval": "Refresh Interval"
+        }
       }
       }
     },
     },
     "error": {
     "error": {
       "invalid_auth": "Invalid credentials.",
       "invalid_auth": "Invalid credentials.",
-      "cannot_connect": "Cannot connect to the CalDAV server."
+      "cannot_connect": "Cannot connect to the server."
     }
     }
   },
   },
   "services": {
   "services": {