feat(hksv): add motion detection API and enhance server handling for consumers

This commit is contained in:
Sergey Krashevich
2026-03-07 18:54:50 +03:00
parent 9c901dc995
commit e3d1085a6d
6 changed files with 143 additions and 38 deletions
+4
View File
@@ -170,6 +170,10 @@ Set threshold between "noise" and "real motion". In this example, 2.0 is a good
**Motion API:**
```bash
# Get motion status
curl "http://localhost:1984/api/homekit/motion?id=outdoor"
# → {"id":"outdoor","motion":false}
# Trigger motion start
curl -X POST "http://localhost:1984/api/homekit/motion?id=outdoor"
+68 -15
View File
@@ -1,6 +1,7 @@
package homekit
import (
"encoding/json"
"errors"
"net"
"net/http"
@@ -178,17 +179,19 @@ func (s *go2rtcSnapshotProvider) GetSnapshot(streamName string, width, height in
// go2rtcLiveStreamHandler implements hksv.LiveStreamHandler
type go2rtcLiveStreamHandler struct {
mu sync.Mutex
consumer *homekit.Consumer
mu sync.Mutex
consumers map[string]*homekit.Consumer
lastSessionID string
}
func (h *go2rtcLiveStreamHandler) SetupEndpoints(conn net.Conn, offer *camera.SetupEndpointsRequest) (any, error) {
consumer := homekit.NewConsumer(conn, srtp.Server)
consumer.SetOffer(offer)
h.mu.Lock()
h.consumer = consumer
h.mu.Unlock()
old := h.setConsumer(offer.SessionID, consumer)
if old != nil && old != consumer {
_ = old.Stop()
}
answer := consumer.GetAnswer()
v, err := tlv8.MarshalBase64(answer)
@@ -199,9 +202,7 @@ func (h *go2rtcLiveStreamHandler) SetupEndpoints(conn net.Conn, offer *camera.Se
}
func (h *go2rtcLiveStreamHandler) GetEndpointsResponse() any {
h.mu.Lock()
consumer := h.consumer
h.mu.Unlock()
consumer := h.latestConsumer()
if consumer == nil {
return nil
}
@@ -211,9 +212,8 @@ func (h *go2rtcLiveStreamHandler) GetEndpointsResponse() any {
}
func (h *go2rtcLiveStreamHandler) StartStream(streamName string, conf *camera.SelectedStreamConfiguration, connTracker hksv.ConnTracker) error {
h.mu.Lock()
consumer := h.consumer
h.mu.Unlock()
sessionID := conf.Control.SessionID
consumer := h.getConsumer(sessionID)
if consumer == nil {
return errors.New("no consumer")
@@ -226,7 +226,12 @@ func (h *go2rtcLiveStreamHandler) StartStream(streamName string, conf *camera.Se
connTracker.AddConn(consumer)
stream := streams.Get(streamName)
if stream == nil {
connTracker.DelConn(consumer)
return errors.New("stream not found: " + streamName)
}
if err := stream.AddConsumer(consumer); err != nil {
connTracker.DelConn(consumer)
return err
}
@@ -234,22 +239,64 @@ func (h *go2rtcLiveStreamHandler) StartStream(streamName string, conf *camera.Se
_, _ = consumer.WriteTo(nil)
stream.RemoveConsumer(consumer)
connTracker.DelConn(consumer)
h.removeConsumer(sessionID, consumer)
}()
return nil
}
func (h *go2rtcLiveStreamHandler) StopStream(sessionID string, connTracker hksv.ConnTracker) error {
h.mu.Lock()
consumer := h.consumer
h.mu.Unlock()
consumer := h.getConsumer(sessionID)
if consumer != nil && consumer.SessionID() == sessionID {
if consumer != nil {
_ = consumer.Stop()
h.removeConsumer(sessionID, consumer)
}
return nil
}
func (h *go2rtcLiveStreamHandler) setConsumer(sessionID string, consumer *homekit.Consumer) *homekit.Consumer {
h.mu.Lock()
defer h.mu.Unlock()
if h.consumers == nil {
h.consumers = map[string]*homekit.Consumer{}
}
old := h.consumers[sessionID]
h.consumers[sessionID] = consumer
h.lastSessionID = sessionID
return old
}
func (h *go2rtcLiveStreamHandler) getConsumer(sessionID string) *homekit.Consumer {
h.mu.Lock()
defer h.mu.Unlock()
return h.consumers[sessionID]
}
func (h *go2rtcLiveStreamHandler) latestConsumer() *homekit.Consumer {
h.mu.Lock()
defer h.mu.Unlock()
return h.consumers[h.lastSessionID]
}
func (h *go2rtcLiveStreamHandler) removeConsumer(sessionID string, consumer *homekit.Consumer) {
h.mu.Lock()
defer h.mu.Unlock()
if h.consumers[sessionID] == consumer {
delete(h.consumers, sessionID)
if h.lastSessionID == sessionID {
h.lastSessionID = ""
for id := range h.consumers {
h.lastSessionID = id
break
}
}
}
}
func streamHandler(rawURL string) (core.Producer, error) {
if srtp.Server == nil {
return nil, errors.New("homekit: can't work without SRTP server")
@@ -316,6 +363,12 @@ func apiMotion(w http.ResponseWriter, r *http.Request) {
return
}
switch r.Method {
case "GET":
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"id": id,
"motion": srv.MotionDetected(),
})
case "POST":
srv.SetMotionDetected(true)
case "DELETE":
+3
View File
@@ -465,6 +465,9 @@ detector.Stop()
### Motion Control
```go
// Check current motion status
detected := srv.MotionDetected()
// Trigger motion detected (for "api" mode or external sensors)
srv.SetMotionDetected(true)
+19 -3
View File
@@ -107,8 +107,8 @@ type Config struct {
// Dependencies (injected by host)
Streams StreamProvider
Store PairingStore // optional, nil = no persistence
Snapshots SnapshotProvider // optional, nil = no snapshots
Store PairingStore // optional, nil = no persistence
Snapshots SnapshotProvider // optional, nil = no snapshots
LiveStream LiveStreamHandler // optional, nil = HKSV only (no live streaming)
Logger zerolog.Logger
@@ -497,8 +497,11 @@ func (s *Server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value a
resp, err := s.liveStream.SetupEndpoints(conn, &offer)
if err != nil {
s.log.Error().Err(err).Msg("[hksv] setup endpoints failed")
return
}
_ = resp // stored by the handler
// Keep the latest response in characteristic value for write-response (r=true)
// and subsequent GET /characteristics reads.
char.Value = resp
case camera.TypeSelectedStreamConfiguration:
if s.liveStream == nil {
@@ -608,6 +611,19 @@ func (s *Server) SetMotionDetected(detected bool) {
s.log.Debug().Str("stream", s.stream).Bool("motion", detected).Msg("[hksv] motion")
}
// MotionDetected returns the current motion detected state.
func (s *Server) MotionDetected() bool {
if s.accessory == nil {
return false
}
char := s.accessory.GetCharacter("22") // MotionDetected
if char == nil {
return false
}
v, _ := char.Value.(bool)
return v
}
// TriggerDoorbell triggers a doorbell press event.
func (s *Server) TriggerDoorbell() {
if s.accessory == nil {
+22 -20
View File
@@ -77,6 +77,16 @@ func ServerHandler(server Server) HandlerFunc {
}
var writeResponses []hap.JSONCharacter
findChar := func(aid uint8, iid uint64) *hap.Character {
accs := server.GetAccessories(conn)
for _, acc := range accs {
if acc.AID != aid {
continue
}
return acc.GetCharacterByID(iid)
}
return nil
}
for _, c := range v.Value {
if c.Value != nil {
@@ -84,31 +94,23 @@ func ServerHandler(server Server) HandlerFunc {
}
if c.Event != nil {
// subscribe/unsubscribe to events
accs := server.GetAccessories(conn)
for _, acc := range accs {
if char := acc.GetCharacterByID(c.IID); char != nil {
if ev, ok := c.Event.(bool); ok && ev {
char.AddListener(conn)
} else {
char.RemoveListener(conn)
}
break
if char := findChar(c.AID, c.IID); char != nil {
if ev, ok := c.Event.(bool); ok && ev {
char.AddListener(conn)
} else {
char.RemoveListener(conn)
}
}
}
if c.R != nil && *c.R {
// write-response: return updated value
accs := server.GetAccessories(conn)
for _, acc := range accs {
if char := acc.GetCharacterByID(c.IID); char != nil {
writeResponses = append(writeResponses, hap.JSONCharacter{
AID: c.AID,
IID: c.IID,
Status: 0,
Value: char.Value,
})
break
}
if char := findChar(c.AID, c.IID); char != nil {
writeResponses = append(writeResponses, hap.JSONCharacter{
AID: c.AID,
IID: c.IID,
Status: 0,
Value: char.Value,
})
}
}
}
+27
View File
@@ -1060,6 +1060,33 @@ paths:
description: Stream not found
/api/homekit/motion:
get:
summary: Get motion detection status
description: Returns current MotionDetected state for the HKSV camera.
tags: [ HomeKit ]
parameters:
- name: id
in: query
description: Stream name / server ID
required: true
schema: { type: string }
example: outdoor
responses:
"200":
description: Motion status
content:
application/json:
schema:
type: object
properties:
id:
type: string
example: outdoor
motion:
type: boolean
example: false
"404":
description: Server not found
post:
summary: Trigger motion detection for HKSV camera
description: Sets MotionDetected characteristic to true, which triggers the Home Hub to start recording.