Skip to content

Add HomeKit doorbell button press event support#2149

Open
julvo712 wants to merge 2 commits intoAlexxIT:masterfrom
julvo712:doorbell-events
Open

Add HomeKit doorbell button press event support#2149
julvo712 wants to merge 2 commits intoAlexxIT:masterfrom
julvo712:doorbell-events

Conversation

@julvo712
Copy link
Copy Markdown

Summary

  • Subscribes to the HAP Programmable Switch Event characteristic (type 73) on HomeKit doorbells and forwards button presses to a configurable webhook URL
  • Enables Home Assistant automations for doorbell presses without unpairing the camera from go2rtc
  • Adds a persistent background HAP connection (separate from the video stream) with auto-reconnect and 30s keepalive pings
  • Includes an SSE endpoint at /api/homekit/events for debugging and alternative consumption

Motivation

go2rtc can pair with HomeKit doorbells (like the Aqara G4) for video streaming, but the doorbell button press event is not surfaced anywhere. Users who want to react to doorbell presses in Home Assistant currently have no local option — they would need to unpair from go2rtc and use a cloud service like IFTTT.

This adds a lightweight event listener that runs alongside the existing video stream, subscribing to HAP events over a second encrypted connection to the camera.

Configuration

New top-level events section in go2rtc.yaml:

streams:
  doorbell: homekit://...

events:
  doorbell:
    stream: "doorbell"        # references the stream name above
    webhook: "http://homeassistant.local:8123/api/webhook/doorbell_rang"

The webhook receives a JSON POST:

{
  "stream": "doorbell",
  "event": "single_press",
  "value": 0,
  "timestamp": "2026-03-12T10:30:00Z"
}

Supports single_press, double_press, and long_press events.

Home Assistant example

automation:
  - alias: "Doorbell Rang"
    trigger:
      - platform: webhook
        webhook_id: doorbell_rang
        local_only: true
    action:
      - service: notify.mobile_app_phone
        data:
          title: "Doorbell"
          message: "Someone is at the door!"

Changes

File Change
pkg/hap/client.go Add StartEventsReader() and SubscribeEvent(iid) methods
internal/homekit/events.go Persistent event listener with webhook, auto-reconnect, SSE broadcast
internal/homekit/events_test.go Unit tests for IID discovery and SSE listener logic
internal/homekit/api.go SSE endpoint at /api/homekit/events
internal/homekit/homekit.go Register endpoint and call initEvents()

Test plan

  • Compiles cleanly on master (go build ./...)
  • Passes go vet
  • Unit tests pass (6 tests covering IID lookup priority, fallbacks, and SSE listeners)
  • Tested with Aqara G4 doorbell — button press events received and webhook fires correctly
  • Video streaming unaffected (separate HAP connection)
  • Auto-reconnects after camera disconnects
  • No-op when events config section is absent (fully backwards compatible)

🤖 Generated with Claude Code

Subscribe to the Programmable Switch Event characteristic (HAP type
"73") on HomeKit doorbells and forward presses to a configurable
webhook URL, enabling Home Assistant automations without unpairing the
camera from go2rtc.

Adds a persistent background HAP connection (separate from the video
stream) with auto-reconnect, 30s keepalive pings, and an SSE endpoint
at /api/homekit/events for debugging.

New config section:

  events:
    doorbell:
      stream: "my-doorbell"
      webhook: "http://homeassistant.local:8123/api/webhook/doorbell"

Supports single_press, double_press, and long_press events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@skrashevich
Copy link
Copy Markdown
Collaborator

Missed edits in the README and in the openapi schema

Copy link
Copy Markdown
Collaborator

@skrashevich skrashevich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webhooks are a dangerous thing. It’s very easy to shoot yourself in the foot. And your neighbor's foot.

var (
sseListenersMu sync.Mutex
sseListeners []chan DoorbellEvent
)
Copy link
Copy Markdown
Collaborator

@skrashevich skrashevich Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
)
webhookHTTPClient = &http.Client{
Timeout: 10 * time.Second,
// Redirects can silently move a webhook to another host.
// Keep the configured destination explicit.
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
)

return 0
}

func fireWebhook(url string, ev DoorbellEvent) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func fireWebhook(url string, ev DoorbellEvent) {
func fireWebhook(url string, ev DoorbellEvent) {
webhookURL, err := validateWebhookURL(url)
if err != nil {
log.Error().Err(err).Str("url", url).Msg("[events] invalid webhook URL")
return
}

"net/http"
"sync"
"time"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
neturl "net/url"

} else {
log.Trace().Msgf("[events] webhook %s returned status %d", url, resp.StatusCode)
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}
func validateWebhookURL(rawURL string) (*neturl.URL, error) {
webhookURL, err := neturl.Parse(rawURL)
if err != nil {
return nil, fmt.Errorf("parse webhook URL: %w", err)
}
if webhookURL.Scheme != "http" && webhookURL.Scheme != "https" {
return nil, fmt.Errorf("unsupported webhook scheme %q", webhookURL.Scheme)
}
if webhookURL.Host == "" {
return nil, fmt.Errorf("webhook URL missing host")
}
return webhookURL, nil
}

}

httpClient := &http.Client{Timeout: 10 * time.Second}
resp, err := httpClient.Post(url, "application/json", bytes.NewReader(body))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
resp, err := httpClient.Post(url, "application/json", bytes.NewReader(body))
req, err := http.NewRequest(http.MethodPost, webhookURL.String(), bytes.NewReader(body))
if err != nil {
log.Error().Err(err).Str("url", url).Msg("[events] build webhook request")
return
}
req.Header.Set("Content-Type", "application/json")
resp, err := webhookHTTPClient.Do(req)

Comment on lines +279 to +285
_, _ = io.ReadAll(resp.Body)

if resp.StatusCode >= 400 {
log.Warn().Msgf("[events] webhook %s returned status %d", url, resp.StatusCode)
} else {
log.Trace().Msgf("[events] webhook %s returned status %d", url, resp.StatusCode)
}
Copy link
Copy Markdown
Collaborator

@skrashevich skrashevich Mar 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_, _ = io.ReadAll(resp.Body)
if resp.StatusCode >= 400 {
log.Warn().Msgf("[events] webhook %s returned status %d", url, resp.StatusCode)
} else {
log.Trace().Msgf("[events] webhook %s returned status %d", url, resp.StatusCode)
}
if _, err = io.Copy(io.Discard, io.LimitReader(resp.Body, maxWebhookResponseBody)); err != nil {
log.Debug().Err(err).Str("url", url).Msg("[events] drain webhook response")
}
switch {
case resp.StatusCode >= 400:
log.Warn().Str("url", url).Int("status", resp.StatusCode).Msg("[events] webhook returned error status")
case resp.StatusCode >= 300:
log.Warn().Str("url", url).Int("status", resp.StatusCode).Msg("[events] webhook returned redirect status")
default:
log.Trace().Str("url", url).Int("status", resp.StatusCode).Msg("[events] webhook returned status")
}

@skrashevich skrashevich added the unsafe-change Potential security regressions label Mar 17, 2026
- Shared HTTP client with redirect blocking and URL validation
- Drain response body with LimitReader, granular status logging
- Add doorbell events section to homekit README
- Add /api/homekit/events SSE endpoint to OpenAPI schema

Co-Authored-By: Sergey Krashevich <skrashevich@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@julvo712
Copy link
Copy Markdown
Author

thanks for reviewing @skrashevich. I've addressed your points and the docs in the latest commit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

module/homekit unsafe-change Potential security regressions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants