Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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

Expand All @@ -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
Expand Down
7 changes: 3 additions & 4 deletions custom_components/clockify/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
CONF_WORKSPACE_ID,
DOMAIN,
)
from .webhook import build_webhook_url

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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."""
Expand Down
6 changes: 4 additions & 2 deletions custom_components/clockify/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
2 changes: 1 addition & 1 deletion custom_components/clockify/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion custom_components/clockify/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
22 changes: 14 additions & 8 deletions custom_components/clockify/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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}"
Expand Down
Loading