diff --git a/README.md b/README.md index 66c32d4..c157ecb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Clockify for Home Assistant -[![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg)](https://github.com/hacs/integration) +[![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg)](https://github.com/hacs/integration) A Home Assistant custom integration for [Clockify](https://clockify.me/) time tracking. Track your current timer status, start/stop time entries, and see your daily/weekly/monthly hours directly in Home Assistant. @@ -15,9 +15,8 @@ A Home Assistant custom integration for [Clockify](https://clockify.me/) time tr ### HACS (recommended) 1. Make sure [HACS](https://hacs.xyz/) is installed. -2. In HACS, go to **Integrations**, click the three-dot menu, choose **Custom repositories**, and add this repository URL with category **Integration** (only needed until this integration is available in the default HACS list). -3. Search for "Clockify" in HACS and install it. -4. Restart Home Assistant. +2. Search for "Clockify" in HACS and install it. +3. Restart Home Assistant. ### Manual @@ -35,6 +34,8 @@ Configuration is done via the UI: - **Timer started (anyone)** → the "started" URL - **Timer stopped (anyone)** → the "stopped" URL + The URLs are built from the external URL set in Home Assistant under **Settings** → **System** → **Network**, since Clockify has to reach Home Assistant from the internet. If no external URL is configured, the URLs start with `https://INSERT_URL/`; replace that part with your Home Assistant's public address before pasting them into Clockify. + Webhooks let Clockify push timer changes to Home Assistant instantly instead of the integration having to poll the Clockify API on a schedule, which keeps API usage low and avoids hitting Clockify's rate limits, even on a free-tier. ## License diff --git a/custom_components/clockify/config_flow.py b/custom_components/clockify/config_flow.py index bf3e9e1..275f770 100644 --- a/custom_components/clockify/config_flow.py +++ b/custom_components/clockify/config_flow.py @@ -26,6 +26,7 @@ CONF_WORKSPACE_ID, DOMAIN, ) +from .webhook import build_webhook_url _LOGGER = logging.getLogger(__name__) @@ -129,7 +130,6 @@ async def async_step_webhook_urls(self, user_input: dict[str, Any] | None = None if not self._config_data: return self.async_abort(reason="unknown") - # FIX: Correct persistent notification call persistent_notification.async_create( self.hass, title="Clockify Webhook Setup", @@ -175,9 +175,8 @@ def _build_webhook_ids(self, config_data: dict[str, Any]) -> tuple[str, str]: return f"{digest}_timer_started", f"{digest}_timer_stopped" def _build_webhook_urls(self, started_webhook_id: str, stopped_webhook_id: str) -> tuple[str, str]: - """Build display-only webhook URLs; the host is a placeholder for the user to fill in.""" - base_url = "https://INSERT_URL/" - return f"{base_url}api/webhook/{started_webhook_id}", f"{base_url}api/webhook/{stopped_webhook_id}" + """Build the webhook URLs from Home Assistant's configured external URL.""" + return build_webhook_url(self.hass, started_webhook_id), build_webhook_url(self.hass, stopped_webhook_id) async def _async_validate_credentials(self, api_key: str) -> tuple[dict[str, Any], list[dict[str, Any]]]: """Validate provided API key and return user data + workspaces.""" diff --git a/custom_components/clockify/manifest.json b/custom_components/clockify/manifest.json index e8a9f4b..fb37aa9 100644 --- a/custom_components/clockify/manifest.json +++ b/custom_components/clockify/manifest.json @@ -5,11 +5,13 @@ "@yoali11" ], "config_flow": true, - "dependencies": [], + "dependencies": [ + "webhook" + ], "documentation": "https://github.com/yoali11/clockify", "integration_type": "service", "iot_class": "cloud_push", "issue_tracker": "https://github.com/yoali11/clockify/issues", "requirements": [], - "version": "1.0.1" + "version": "1.1.4" } \ No newline at end of file diff --git a/custom_components/clockify/strings.json b/custom_components/clockify/strings.json index f720eb0..36c556e 100644 --- a/custom_components/clockify/strings.json +++ b/custom_components/clockify/strings.json @@ -24,7 +24,7 @@ }, "webhook_urls": { "title": "Clockify Webhook URLs", - "description": "Copy these URLs into Clockify under Preferences -> Advanced -> Webhooks.\n\nTimer Started: {started_url}\nTimer Stopped: {stopped_url}" + "description": "Copy these URLs into Clockify under Preferences -> Advanced -> Webhooks.\n\nTimer Started: {started_url}\nTimer Stopped: {stopped_url}\n\nThe URLs use the external URL configured in Home Assistant (Settings -> System -> Network). If they start with https://INSERT_URL, no external URL is configured: replace that part with the address Clockify can reach your Home Assistant on." } }, "error": { diff --git a/custom_components/clockify/translations/en.json b/custom_components/clockify/translations/en.json index f720eb0..36c556e 100644 --- a/custom_components/clockify/translations/en.json +++ b/custom_components/clockify/translations/en.json @@ -24,7 +24,7 @@ }, "webhook_urls": { "title": "Clockify Webhook URLs", - "description": "Copy these URLs into Clockify under Preferences -> Advanced -> Webhooks.\n\nTimer Started: {started_url}\nTimer Stopped: {stopped_url}" + "description": "Copy these URLs into Clockify under Preferences -> Advanced -> Webhooks.\n\nTimer Started: {started_url}\nTimer Stopped: {stopped_url}\n\nThe URLs use the external URL configured in Home Assistant (Settings -> System -> Network). If they start with https://INSERT_URL, no external URL is configured: replace that part with the address Clockify can reach your Home Assistant on." } }, "error": { diff --git a/custom_components/clockify/webhook.py b/custom_components/clockify/webhook.py index a743597..787b232 100644 --- a/custom_components/clockify/webhook.py +++ b/custom_components/clockify/webhook.py @@ -9,7 +9,7 @@ from homeassistant.components import webhook from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.network import get_url +from homeassistant.helpers.network import NoURLAvailableError, get_url from .const import ( CONF_STARTED_WEBHOOK_ID, @@ -22,6 +22,8 @@ _LOGGER = logging.getLogger(__name__) +WEBHOOK_URL_PLACEHOLDER = "https://INSERT_URL" + async def async_setup_webhooks( hass: HomeAssistant, entry: Any, entry_data: dict[str, Any] @@ -35,8 +37,8 @@ async def async_setup_webhooks( entry.data.get(CONF_STOPPED_WEBHOOK_ID) or f"{entry_id}_timer_stopped" ) - started_url = _build_webhook_url(hass, started_webhook_id) - stopped_url = _build_webhook_url(hass, stopped_webhook_id) + started_url = build_webhook_url(hass, started_webhook_id) + stopped_url = build_webhook_url(hass, stopped_webhook_id) entry_data[CONF_STARTED_WEBHOOK_ID] = started_webhook_id entry_data[CONF_STOPPED_WEBHOOK_ID] = stopped_webhook_id @@ -77,12 +79,16 @@ def async_unregister_webhooks(hass: HomeAssistant, entry_data: dict[str, Any]) - webhook.async_unregister(hass, webhook_id) -def _build_webhook_url(hass: HomeAssistant, webhook_id: str) -> str: - """Build the public webhook URL for a webhook ID.""" +def build_webhook_url(hass: HomeAssistant, webhook_id: str) -> str: + """Build the public webhook URL for a webhook ID. + + Clockify calls the webhook from the internet, so the external URL is preferred. + Falls back to a placeholder host when Home Assistant has no URL configured. + """ try: - base_url = get_url(hass, allow_external=True) - except Exception: # pragma: no cover - defensive - base_url = "https://your-home-assistant-instance" + base_url = get_url(hass, allow_external=True, prefer_external=True) + except NoURLAvailableError: + base_url = WEBHOOK_URL_PLACEHOLDER if not base_url.endswith("/"): base_url = f"{base_url}/" return f"{base_url}api/webhook/{webhook_id}"