jsm-ha-notifier
Docker service that bridges Jira Service Management (JSM/OpsGenie) alerts to Home Assistant — smar
1.2K
A lightweight Docker service that bridges Jira Service Management (JSM / OpsGenie) alerts to Home Assistant — with smart on-call routing, escalation detection, rich TTS announcements, and persistent dashboard notifications.
JSM alert created / escalated
│
▼
jsm-ha-notifier (Docker)
│
├─ Parse alert payload
├─ Deduplicate (suppress retries within 60 s)
├─ Route decision:
│ always_notify mode? → NOTIFY
│ escalated to me? → NOTIFY
│ I'm on-call? → NOTIFY (JSM API, cached 5 min)
│ none of the above → DROP
│
▼
Home Assistant REST API
├─ media_player.play_media (TTS with rich metadata / real alert title)
└─ persistent_notification (visible in HA dashboard)
On Acknowledge / Close → dismiss the persistent notification automatically
Two webhook URLs, one for each routing mode:
| JSM Webhook URL | Behaviour |
|---|---|
https://your-host:8080/alert?key=YOUR_KEY | Notify only when on-call |
https://your-host:8080/alert?mode=always&key=YOUR_KEY | Always notify regardless of schedule |
EscalateNext events always notify regardless of on-call status or dedup windowextra.metadata so HA shows the actual alert title instead of "Playing Default Media Receiver"POST /alert/{id}/acknowledge endpoint lets HA automations ack alerts without opening JSMGET /healthz verifies both JSM and HA API connectivity (returns 503 if either fails)GET /incidents API with status/priority filters, summary endpoint, and Grafana JSON datasource compatibilityENABLE_EMOJIS=false strips all emojis from notifications, metadata, and incoming alert text?key=), HTTP header (X-API-Key), or URL path prefix (/KEY/endpoint)X-Hub-Signature-256GET /metrics exposes alert counters, credential check stats, rate limit hits, and uptime for Grafana/Prometheus dashboardsLOG_FORMAT=json for Datadog, Loki, CloudWatch, ELK; default is human-readable textPOST /reload re-reads .env and applies changes without container restart/alert to prevent webhook abuse/tmp, localhost-only port bindinggit clone https://github.com/RealDougEubanks/JSM-HomeAssistant-Notifier.git
cd JSM-HomeAssistant-Notifier
cp .env.example .env
# Edit .env and fill in all required values (see Configuration below)
docker compose up -d
docker compose logs -f
Verify the service is running:
curl http://localhost:8080/health
# {"status":"ok"}
To build the Docker image locally instead of pulling from GHCR:
docker build -t ghcr.io/realdougeubanks/jsm-ha-notifier:latest .
docker compose up -d
docker compose logs -f
This tags the local build with the same image name the compose file expects.
If you've made code changes and Docker serves a cached layer, force a clean rebuild:
docker build --no-cache -t ghcr.io/realdougeubanks/jsm-ha-notifier:latest .
docker compose up -d
docker compose logs -f
Note: .env changes do not require a rebuild — just restart the container with docker compose up -d.
.envcp .env.example .env
Open .env and fill in each value. The file is fully commented with instructions for finding each value. The sections below expand on the key ones.
Your Cloud ID is a UUID that identifies your Atlassian organisation. Retrieve it with:
curl -s -u "[email protected]:YOUR_API_TOKEN" \
https://your-org.atlassian.net/_edge/tenant_info \
| python3 -m json.tool
Look for the "cloudId" field. Copy it into JSM_CLOUD_ID.
Your account ID (JSM_MY_USER_ID) is the UUID Atlassian uses internally for your user. The easiest way to find it:
curl -s -u "[email protected]:YOUR_API_TOKEN" \
"https://api.atlassian.com/jsm/ops/api/YOUR_CLOUD_ID/v1/schedules/YOUR_SCHEDULE_ID/on-calls" \
| python3 -m json.tool
Find your name in the onCallParticipants array; the "id" field is your account ID.
Schedule names are case-sensitive. List all schedules visible to your token:
curl -s -u "[email protected]:YOUR_API_TOKEN" \
"https://api.atlassian.com/jsm/ops/api/YOUR_CLOUD_ID/v1/schedules" \
| python3 -m json.tool | grep '"name"'
Copy the exact names into ALWAYS_NOTIFY_SCHEDULE_NAMES and/or CHECK_ONCALL_SCHEDULE_NAMES in .env.
JSM Notifier)HA_TOKEN in .env — it is only shown onceIn Home Assistant go to Developer Tools → States, filter by media_player. Copy the entity_id (e.g. media_player.living_room) into HA_MEDIA_PLAYER_ENTITY.
Once the container is running, check on-call status directly:
curl http://localhost:8080/status | python3 -m json.tool
You should see your schedules listed and an on_call field. If a schedule shows "error": "not found", the name in .env doesn't match — compare carefully against the output of the schedule listing curl above.
JSM's servers need to reach your webhook URL over the internet.
WARNING: Do not expose this container directly to the internet. The service runs plain HTTP without TLS. All traffic — including API keys, webhook payloads, and authentication tokens — is transmitted in cleartext. Always place a TLS-terminating proxy or tunnel in front of the service. Direct internet exposure risks credential interception, replay attacks, and unauthorized access to your Home Assistant instance.
A Cloudflare Tunnel creates an encrypted outbound connection from your network to Cloudflare's edge, with no inbound ports to open on your router or firewall. Cloudflare handles TLS termination and DDoS protection automatically.
For setup instructions, see the Cloudflare Tunnel documentation.
Run a TLS-terminating reverse proxy on the same host and forward traffic to http://127.0.0.1:8080. Detailed reverse proxy configuration is outside the scope of this README — consult your proxy's documentation for TLS certificate setup (e.g. Let's Encrypt via Certbot or Caddy's automatic HTTPS).
Configure two outgoing webhooks in JSM Ops — one for on-call schedules and one for always-notify schedules.
JSM project → Settings → Integrations → Add Integration → choose Webhook (under "Outgoing").
| Field | Value |
|---|---|
| Name | HA Notifier — On-Call |
| Webhook URL | https://your-host/alert?key=YOUR_API_KEY |
| Method | POST |
| Send alert payload | ✅ Enabled |
| Alert actions | Create, EscalateNext, Acknowledge, Close |
| Teams / Schedules filter | Your on-call schedule's team |
| Field | Value |
|---|---|
| Name | HA Notifier — Always Notify |
| Webhook URL | https://your-host/alert?mode=always&key=YOUR_API_KEY |
| Method | POST |
| Send alert payload | ✅ Enabled |
| Alert actions | Create, EscalateNext, Acknowledge, Close |
| Teams / Schedules filter | Your always-notify team/schedule |
The simplest way to secure your webhook endpoints. Set WEBHOOK_API_KEY in .env and pass the key using any of these methods:
| Method | Example | Best for |
|---|---|---|
| Query parameter | https://your-host/alert?key=YOUR_KEY | JSM webhooks (URL-only config) |
| Path prefix | https://your-host/YOUR_KEY/alert | Tools that can't add headers or query params |
| HTTP header | X-API-Key: YOUR_KEY | Scripts, HA automations, Grafana |
All three methods work on every authenticated endpoint. Generate a key: openssl rand -hex 32
Requests without a valid key receive a stealth 404 (not 401), so attackers cannot confirm that authenticated endpoints exist.
The query-parameter and path-prefix methods exist because JSM webhooks can only be configured with a bare URL — but any URL-borne secret ends up in reverse-proxy access logs, browser history, and intermediate proxies. Prefer the X-API-Key header wherever the caller supports it (scripts, HA automations, Grafana), and if you front this service with a reverse proxy:
Scrub or disable access logging for the key. nginx example:
# Redact the key query param / path segment before logging
map $request_uri $loggable_uri {
~^(?<pre>.*[?&]key=)[^&]+(?<post>.*)$ "${pre}REDACTED${post}";
default $request_uri;
}
log_format scrubbed '$remote_addr - [$time_local] "$request_method $loggable_uri" $status';
access_log /var/log/nginx/access.log scrubbed;
Caddy: add log { format filter { request>uri query { replace key REDACTED } } } to the site block.
Rotate WEBHOOK_API_KEY (and update the JSM webhook URLs) if a proxy has already logged it.
The service itself never logs request URLs, and uvicorn access logging is disabled in the container.
If set, WEBHOOK_SECRET requires every POST /alert to carry an X-Hub-Signature-256: sha256=<hex> header containing the HMAC-SHA256 of the raw request body keyed with the secret.
⚠️ JSM cannot sign requests itself. JSM / OpsGenie outgoing-webhook custom headers are static strings — there is no template function to compute a per-request HMAC over the body. If JSM posts directly to this service, leave
WEBHOOK_SECRETempty and rely onWEBHOOK_API_KEY.
Use WEBHOOK_SECRET when a signing-capable caller sits in front of the service:
| Caller | How |
|---|---|
| Forwarding proxy you control (Cloudflare Worker, AWS Lambda, nginx+Lua) | Receive the JSM webhook, compute sha256= HMAC of the body, forward with the header |
| Your own scripts / automations | Compute the HMAC at send time (e.g. openssl dgst -sha256 -hmac "$SECRET") |
You can use both WEBHOOK_API_KEY and WEBHOOK_SECRET together for defense in depth when your caller supports signing.
curl -X POST http://localhost:8080/alert \
-H "Content-Type: application/json" \
-d '{
"action": "Create",
"alert": {
"alertId": "test-001",
"message": "Test Alert — please ignore",
"priority": "P3",
"entity": "dev-server",
"description": "This is a test alert sent manually."
}
}'
If you are currently on-call, this will trigger a TTS announcement and create a persistent notification in HA.
curl -X POST "http://localhost:8080/alert?mode=always" \
-H "Content-Type: application/json" \
-d '{
"action": "Create",
"alert": {
"alertId": "always-test-001",
"message": "Infrastructure Monitor Test",
"priority": "P2",
"entity": "prod-server-01"
}
}'
This path always notifies regardless of on-call status.
curl -X POST "http://localhost:8080/alert?mode=always" \
-H "Content-Type: application/json" \
-d '{
"action": "EscalateNext",
"alert": {
"alertId": "test-001",
"message": "Test Alert — please ignore",
"priority": "P1",
"entity": "prod-db-01"
}
}'
curl http://localhost:8080/status | python3 -m json.tool
curl -X POST http://localhost:8080/cache/invalidate
If WEBHOOK_SECRET is set, generate the signature before sending:
SECRET="your-webhook-secret"
BODY='{"action":"Create","alert":{"alertId":"sig-test","message":"Signed test","priority":"P3"}}'
SIG="sha256=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')"
curl -X POST http://localhost:8080/alert \
-H "Content-Type: application/json" \
-H "X-Hub-Signature-256: $SIG" \
-d "$BODY"
The /alert endpoint accepts any JSON payload matching the OpsGenie webhook format. You don't need JSM — any monitoring system, script, or automation that can send HTTP POST requests can trigger HA alerts.
{
"action": "Create",
"alert": {
"alertId": "unique-id-123",
"message": "Your alert title here",
"priority": "P1",
"entity": "optional-system-name",
"description": "Optional longer description text"
}
}
| Field | Required | Description |
|---|---|---|
action | Yes | Create, EscalateNext, Acknowledge, or Close |
alert.alertId | Yes | Unique identifier (used for dedup and notification tracking) |
alert.message | Yes | Alert title / summary (spoken by TTS) |
alert.priority | No | P1–P5 (default: P3) |
alert.entity | No | System / host name |
alert.description | No | Longer details (first 200 chars used in TTS) |
Configure a webhook notification in Uptime Kuma with the Notification Type set to "Webhook" / custom JSON:
# Uptime Kuma → Settings → Notifications → Add → Webhook
# URL: http://your-notifier:8080/alert?mode=always&key=YOUR_KEY
# Method: POST
# Body:
{
"action": "Create",
"alert": {
"alertId": "uptime-kuma-{{ monitorJSON.id }}",
"message": "{{ monitorJSON.name }} is {{ heartbeatJSON.status == 1 ? 'UP' : 'DOWN' }}",
"priority": "P2",
"entity": "{{ monitorJSON.hostname }}"
}
}
Use a Grafana "webhook" contact point with the OpsGenie payload format:
# Grafana → Alerting → Contact Points → New → Webhook
# URL: http://your-notifier:8080/alert?mode=always&key=YOUR_KEY
# Method: POST
#
# Or use curl to forward Grafana alerts via a script:
curl -X POST "http://your-notifier:8080/alert?mode=always&key=YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "Create",
"alert": {
"alertId": "grafana-cpu-alert-prod01",
"message": "CPU usage above 95% on prod-01",
"priority": "P1",
"entity": "prod-01",
"description": "CPU has been above 95% for the last 5 minutes. Current: 98.2%."
}
}'
Use Alertmanager's webhook receiver to POST to the notifier:
# alertmanager.yml
receivers:
- name: ha-notifier
webhook_configs:
- url: "http://your-notifier:8080/alert?mode=always&key=YOUR_KEY"
send_resolved: true
Then use a small relay script or Alertmanager template to transform alerts into the expected format.
Trigger an alert from HA itself (e.g. a sensor threshold):
# HA automation action
service: rest_command.trigger_notifier_alert
data:
alert_id: "ha-temp-alert-{{ now().isoformat() }}"
message: "Temperature sensor above threshold"
priority: "P2"
entity: "sensor.living_room_temperature"
description: "Current temperature: {{ states('sensor.living_room_temperature') }}°C"
# configuration.yaml
rest_command:
trigger_notifier_alert:
url: "http://your-notifier:8080/alert?mode=always&key=YOUR_KEY"
method: POST
content_type: "application/json"
payload: >
{"action":"Create","alert":{"alertId":"{{ alert_id }}","message":"{{ message }}","priority":"{{ priority }}","entity":"{{ entity }}","description":"{{ description }}"}}
Trigger an alert from any script or cron job:
#!/bin/bash
# notify-ha.sh — send an alert to the JSM-HA Notifier
NOTIFIER_URL="http://your-notifier:8080/alert?mode=always&key=YOUR_KEY"
curl -s -X POST "$NOTIFIER_URL" \
-H "Content-Type: application/json" \
-d "{
\"action\": \"Create\",
\"alert\": {
\"alertId\": \"script-$(date +%s)\",
\"message\": \"$1\",
\"priority\": \"${2:-P3}\",
\"entity\": \"$(hostname)\"
}
}"
Usage: ./notify-ha.sh "Backup failed on NAS" P2
To dismiss the persistent HA notification and stop TTS repeats, send a Close or Acknowledge action with the same alertId:
curl -X POST "http://your-notifier:8080/alert?mode=always&key=YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"action": "Close", "alert": {"alertId": "the-original-alert-id", "message": "resolved"}}'
Or use the dedicated acknowledge endpoint:
curl -X POST "http://your-notifier:8080/alert/the-original-alert-id/acknowledge?key=YOUR_KEY"
JSM sends different action values for each alert lifecycle event. The notifier handles all of them:
| JSM Action | Notifier Behaviour | HA Webhook Config |
|---|---|---|
Create | TTS + persistent notification (if on-call/always-notify) | HA_WEBHOOK_ON_CREATE |
EscalateNext | TTS + persistent notification (always if targeted at you) | HA_WEBHOOK_ON_ESCALATE |
Acknowledge | Dismiss HA notification, cancel TTS repeat | HA_WEBHOOK_ON_ACKNOWLEDGE |
Close | Dismiss HA notification, cancel TTS repeat | HA_WEBHOOK_ON_CLOSE |
AddNote | Fire HA webhook only (no TTS) | HA_WEBHOOK_ON_UPDATE |
UnAcknowledge | Fire HA webhook only | HA_WEBHOOK_ON_UPDATE |
AssignOwnership | Fire HA webhook only | HA_WEBHOOK_ON_UPDATE |
SlaBreached | Fire HA webhook only | HA_WEBHOOK_ON_SLA_BREACH |
In JSM, configure your outgoing webhook to include all action types you want to handle:
| Field | Value |
|---|---|
| Alert actions | Create, EscalateNext, Acknowledge, Close, AddNote, UnAcknowledge, AssignOwnership |
| Webhook URL | https://your-host/alert?mode=always&key=YOUR_API_KEY |
These work with JSM, Uptime Kuma, Grafana, scripts, or any HTTP client:
New alert:
{"action": "Create", "alert": {"alertId": "inc-001", "message": "Server down", "priority": "P1", "entity": "prod-01"}}
Escalation:
{"action": "EscalateNext", "alert": {"alertId": "inc-001", "message": "Server down", "priority": "P1", "entity": "prod-01"}}
Acknowledged:
{"action": "Acknowledge", "alert": {"alertId": "inc-001", "message": "Server down"}}
Resolved / Closed:
{"action": "Close", "alert": {"alertId": "inc-001", "message": "Server down"}}
Updated (note added):
{"action": "AddNote", "alert": {"alertId": "inc-001", "message": "Server down", "description": "Restarting services..."}}
SLA Breached:
{"action": "SlaBreached", "alert": {"alertId": "inc-001", "message": "Server down", "priority": "P1"}}
Send a full alert lifecycle from the command line for testing:
URL="http://localhost:8080/alert?mode=always&key=YOUR_KEY"
ID="test-lifecycle-$(date +%s)"
# Create
curl -s -X POST "$URL" -H "Content-Type: application/json" \
-d "{\"action\":\"Create\",\"alert\":{\"alertId\":\"$ID\",\"message\":\"Test lifecycle alert\",\"priority\":\"P2\",\"entity\":\"test-server\"}}"
sleep 5
# Acknowledge
curl -s -X POST "$URL" -H "Content-Type: application/json" \
-d "{\"action\":\"Acknowledge\",\"alert\":{\"alertId\":\"$ID\",\"message\":\"Test lifecycle alert\"}}"
sleep 5
#
Content type
Image
Digest
sha256:0b38cbbef…
Size
58.4 MB
Last updated
29 days ago
docker pull dougeubanks/jsm-ha-notifier