feat(hksv): add motion detection API and enhance server handling for consumers
This commit is contained in:
@@ -170,6 +170,10 @@ Set threshold between "noise" and "real motion". In this example, 2.0 is a good
|
|||||||
**Motion API:**
|
**Motion API:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Get motion status
|
||||||
|
curl "http://localhost:1984/api/homekit/motion?id=outdoor"
|
||||||
|
# → {"id":"outdoor","motion":false}
|
||||||
|
|
||||||
# Trigger motion start
|
# Trigger motion start
|
||||||
curl -X POST "http://localhost:1984/api/homekit/motion?id=outdoor"
|
curl -X POST "http://localhost:1984/api/homekit/motion?id=outdoor"
|
||||||
|
|
||||||
|
|||||||
+67
-14
@@ -1,6 +1,7 @@
|
|||||||
package homekit
|
package homekit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -179,16 +180,18 @@ func (s *go2rtcSnapshotProvider) GetSnapshot(streamName string, width, height in
|
|||||||
// go2rtcLiveStreamHandler implements hksv.LiveStreamHandler
|
// go2rtcLiveStreamHandler implements hksv.LiveStreamHandler
|
||||||
type go2rtcLiveStreamHandler struct {
|
type go2rtcLiveStreamHandler struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
consumer *homekit.Consumer
|
consumers map[string]*homekit.Consumer
|
||||||
|
lastSessionID string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *go2rtcLiveStreamHandler) SetupEndpoints(conn net.Conn, offer *camera.SetupEndpointsRequest) (any, error) {
|
func (h *go2rtcLiveStreamHandler) SetupEndpoints(conn net.Conn, offer *camera.SetupEndpointsRequest) (any, error) {
|
||||||
consumer := homekit.NewConsumer(conn, srtp.Server)
|
consumer := homekit.NewConsumer(conn, srtp.Server)
|
||||||
consumer.SetOffer(offer)
|
consumer.SetOffer(offer)
|
||||||
|
|
||||||
h.mu.Lock()
|
old := h.setConsumer(offer.SessionID, consumer)
|
||||||
h.consumer = consumer
|
if old != nil && old != consumer {
|
||||||
h.mu.Unlock()
|
_ = old.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
answer := consumer.GetAnswer()
|
answer := consumer.GetAnswer()
|
||||||
v, err := tlv8.MarshalBase64(answer)
|
v, err := tlv8.MarshalBase64(answer)
|
||||||
@@ -199,9 +202,7 @@ func (h *go2rtcLiveStreamHandler) SetupEndpoints(conn net.Conn, offer *camera.Se
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *go2rtcLiveStreamHandler) GetEndpointsResponse() any {
|
func (h *go2rtcLiveStreamHandler) GetEndpointsResponse() any {
|
||||||
h.mu.Lock()
|
consumer := h.latestConsumer()
|
||||||
consumer := h.consumer
|
|
||||||
h.mu.Unlock()
|
|
||||||
if consumer == nil {
|
if consumer == nil {
|
||||||
return 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 {
|
func (h *go2rtcLiveStreamHandler) StartStream(streamName string, conf *camera.SelectedStreamConfiguration, connTracker hksv.ConnTracker) error {
|
||||||
h.mu.Lock()
|
sessionID := conf.Control.SessionID
|
||||||
consumer := h.consumer
|
consumer := h.getConsumer(sessionID)
|
||||||
h.mu.Unlock()
|
|
||||||
|
|
||||||
if consumer == nil {
|
if consumer == nil {
|
||||||
return errors.New("no consumer")
|
return errors.New("no consumer")
|
||||||
@@ -226,7 +226,12 @@ func (h *go2rtcLiveStreamHandler) StartStream(streamName string, conf *camera.Se
|
|||||||
connTracker.AddConn(consumer)
|
connTracker.AddConn(consumer)
|
||||||
|
|
||||||
stream := streams.Get(streamName)
|
stream := streams.Get(streamName)
|
||||||
|
if stream == nil {
|
||||||
|
connTracker.DelConn(consumer)
|
||||||
|
return errors.New("stream not found: " + streamName)
|
||||||
|
}
|
||||||
if err := stream.AddConsumer(consumer); err != nil {
|
if err := stream.AddConsumer(consumer); err != nil {
|
||||||
|
connTracker.DelConn(consumer)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -234,22 +239,64 @@ func (h *go2rtcLiveStreamHandler) StartStream(streamName string, conf *camera.Se
|
|||||||
_, _ = consumer.WriteTo(nil)
|
_, _ = consumer.WriteTo(nil)
|
||||||
stream.RemoveConsumer(consumer)
|
stream.RemoveConsumer(consumer)
|
||||||
connTracker.DelConn(consumer)
|
connTracker.DelConn(consumer)
|
||||||
|
h.removeConsumer(sessionID, consumer)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *go2rtcLiveStreamHandler) StopStream(sessionID string, connTracker hksv.ConnTracker) error {
|
func (h *go2rtcLiveStreamHandler) StopStream(sessionID string, connTracker hksv.ConnTracker) error {
|
||||||
h.mu.Lock()
|
consumer := h.getConsumer(sessionID)
|
||||||
consumer := h.consumer
|
|
||||||
h.mu.Unlock()
|
|
||||||
|
|
||||||
if consumer != nil && consumer.SessionID() == sessionID {
|
if consumer != nil {
|
||||||
_ = consumer.Stop()
|
_ = consumer.Stop()
|
||||||
|
h.removeConsumer(sessionID, consumer)
|
||||||
}
|
}
|
||||||
return nil
|
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) {
|
func streamHandler(rawURL string) (core.Producer, error) {
|
||||||
if srtp.Server == nil {
|
if srtp.Server == nil {
|
||||||
return nil, errors.New("homekit: can't work without SRTP server")
|
return nil, errors.New("homekit: can't work without SRTP server")
|
||||||
@@ -316,6 +363,12 @@ func apiMotion(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch r.Method {
|
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":
|
case "POST":
|
||||||
srv.SetMotionDetected(true)
|
srv.SetMotionDetected(true)
|
||||||
case "DELETE":
|
case "DELETE":
|
||||||
|
|||||||
@@ -465,6 +465,9 @@ detector.Stop()
|
|||||||
### Motion Control
|
### Motion Control
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
// Check current motion status
|
||||||
|
detected := srv.MotionDetected()
|
||||||
|
|
||||||
// Trigger motion detected (for "api" mode or external sensors)
|
// Trigger motion detected (for "api" mode or external sensors)
|
||||||
srv.SetMotionDetected(true)
|
srv.SetMotionDetected(true)
|
||||||
|
|
||||||
|
|||||||
+17
-1
@@ -497,8 +497,11 @@ func (s *Server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value a
|
|||||||
resp, err := s.liveStream.SetupEndpoints(conn, &offer)
|
resp, err := s.liveStream.SetupEndpoints(conn, &offer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Error().Err(err).Msg("[hksv] setup endpoints failed")
|
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:
|
case camera.TypeSelectedStreamConfiguration:
|
||||||
if s.liveStream == nil {
|
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")
|
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.
|
// TriggerDoorbell triggers a doorbell press event.
|
||||||
func (s *Server) TriggerDoorbell() {
|
func (s *Server) TriggerDoorbell() {
|
||||||
if s.accessory == nil {
|
if s.accessory == nil {
|
||||||
|
|||||||
+12
-10
@@ -77,6 +77,16 @@ func ServerHandler(server Server) HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var writeResponses []hap.JSONCharacter
|
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 {
|
for _, c := range v.Value {
|
||||||
if c.Value != nil {
|
if c.Value != nil {
|
||||||
@@ -84,31 +94,23 @@ func ServerHandler(server Server) HandlerFunc {
|
|||||||
}
|
}
|
||||||
if c.Event != nil {
|
if c.Event != nil {
|
||||||
// subscribe/unsubscribe to events
|
// subscribe/unsubscribe to events
|
||||||
accs := server.GetAccessories(conn)
|
if char := findChar(c.AID, c.IID); char != nil {
|
||||||
for _, acc := range accs {
|
|
||||||
if char := acc.GetCharacterByID(c.IID); char != nil {
|
|
||||||
if ev, ok := c.Event.(bool); ok && ev {
|
if ev, ok := c.Event.(bool); ok && ev {
|
||||||
char.AddListener(conn)
|
char.AddListener(conn)
|
||||||
} else {
|
} else {
|
||||||
char.RemoveListener(conn)
|
char.RemoveListener(conn)
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if c.R != nil && *c.R {
|
if c.R != nil && *c.R {
|
||||||
// write-response: return updated value
|
// write-response: return updated value
|
||||||
accs := server.GetAccessories(conn)
|
if char := findChar(c.AID, c.IID); char != nil {
|
||||||
for _, acc := range accs {
|
|
||||||
if char := acc.GetCharacterByID(c.IID); char != nil {
|
|
||||||
writeResponses = append(writeResponses, hap.JSONCharacter{
|
writeResponses = append(writeResponses, hap.JSONCharacter{
|
||||||
AID: c.AID,
|
AID: c.AID,
|
||||||
IID: c.IID,
|
IID: c.IID,
|
||||||
Status: 0,
|
Status: 0,
|
||||||
Value: char.Value,
|
Value: char.Value,
|
||||||
})
|
})
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1060,6 +1060,33 @@ paths:
|
|||||||
description: Stream not found
|
description: Stream not found
|
||||||
|
|
||||||
/api/homekit/motion:
|
/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:
|
post:
|
||||||
summary: Trigger motion detection for HKSV camera
|
summary: Trigger motion detection for HKSV camera
|
||||||
description: Sets MotionDetected characteristic to true, which triggers the Home Hub to start recording.
|
description: Sets MotionDetected characteristic to true, which triggers the Home Hub to start recording.
|
||||||
|
|||||||
Reference in New Issue
Block a user