diff --git a/internal/camera/discovery/scanner.go b/internal/camera/discovery/scanner.go index df2fff9..81b4bb7 100644 --- a/internal/camera/discovery/scanner.go +++ b/internal/camera/discovery/scanner.go @@ -411,7 +411,14 @@ func (s *Scanner) testStreamsConcurrently(ctx context.Context, streams []models. defer cancelProgress() go func() { - ticker := time.NewTicker(1 * time.Second) + // Use longer interval for Ingress mode to reduce traffic (padding is ~64KB per event) + // Normal mode: 1 second, Ingress mode: 3 seconds + progressInterval := 1 * time.Second + if streamWriter.IsIngress() { + progressInterval = 3 * time.Second + } + + ticker := time.NewTicker(progressInterval) defer ticker.Stop() for { @@ -419,7 +426,7 @@ func (s *Scanner) testStreamsConcurrently(ctx context.Context, streams []models. case <-progressCtx.Done(): return case <-ticker.C: - // Send progress every second to prevent WriteTimeout + // Send progress to prevent WriteTimeout and show scanning activity _ = streamWriter.SendJSON("progress", models.ProgressMessage{ Tested: int(atomic.LoadInt32(&tested)), Found: int(atomic.LoadInt32(&found)), diff --git a/pkg/sse/sse.go b/pkg/sse/sse.go index 9dd79fa..de9985a 100644 --- a/pkg/sse/sse.go +++ b/pkg/sse/sse.go @@ -5,9 +5,20 @@ import ( "encoding/json" "fmt" "net/http" + "strings" "time" ) +const ( + // IngressPaddingSize is the padding size for Home Assistant Ingress mode. + // HA Supervisor uses aiohttp with 64KB buffer for StreamResponse. + // We need to fill this buffer to force immediate delivery of SSE events. + IngressPaddingSize = 64 * 1024 // 64KB + + // IngressHeader is the header that Home Assistant Ingress adds to requests + IngressHeader = "X-Ingress-Path" +) + // Event represents a Server-Sent Event type Event struct { ID string @@ -253,8 +264,9 @@ func generateClientID() string { // StreamWriter provides a simple interface for writing SSE events type StreamWriter struct { - client *Client - server *Server + client *Client + server *Server + isIngress bool // True when running through Home Assistant Ingress proxy } // NewStreamWriter creates a new stream writer for a client @@ -275,6 +287,9 @@ func (s *Server) NewStreamWriter(w http.ResponseWriter, r *http.Request) (*Strea // Send initial flush to establish connection flusher.Flush() + // Detect Home Assistant Ingress mode by checking for X-Ingress-Path header + isIngress := r.Header.Get(IngressHeader) != "" + // Create client ctx, cancel := context.WithCancel(r.Context()) client := &Client{ @@ -287,8 +302,9 @@ func (s *Server) NewStreamWriter(w http.ResponseWriter, r *http.Request) (*Strea } return &StreamWriter{ - client: client, - server: s, + client: client, + server: s, + isIngress: isIngress, }, nil } @@ -304,7 +320,48 @@ func (sw *StreamWriter) SendEvent(eventType string, data interface{}) error { return fmt.Errorf("response does not support flushing") } - return sw.server.writeEvent(sw.client.Response, flusher, event) + // Use Ingress-aware write method + return sw.writeEventWithIngress(sw.client.Response, flusher, event) +} + +// writeEventWithIngress writes an event and adds padding for Ingress mode +func (sw *StreamWriter) writeEventWithIngress(w http.ResponseWriter, flusher http.Flusher, event Event) error { + // Write the event using standard method + if err := sw.server.writeEvent(w, flusher, event); err != nil { + return err + } + + // In Ingress mode, add padding to fill the 64KB buffer and force immediate delivery + if sw.isIngress { + if err := sw.writePadding(w, flusher); err != nil { + return err + } + } + + return nil +} + +// writePadding writes SSE comment padding to fill proxy buffers. +// SSE comments (lines starting with ':') are ignored by clients. +func (sw *StreamWriter) writePadding(w http.ResponseWriter, flusher http.Flusher) error { + // Create padding using SSE comments which are ignored by clients + // Each line is ": " + padding content + "\n" + // We need ~64KB to fill the aiohttp StreamResponse buffer + const lineSize = 1024 // 1KB per line + const numLines = 64 // 64 lines = 64KB + + paddingLine := ": " + strings.Repeat(".", lineSize-4) + "\n" // -4 for ": " and "\n" + + for i := 0; i < numLines; i++ { + if _, err := fmt.Fprint(w, paddingLine); err != nil { + return err + } + } + + // Flush the padding + flusher.Flush() + + return nil } // SendJSON sends JSON data as an event @@ -312,6 +369,11 @@ func (sw *StreamWriter) SendJSON(eventType string, v interface{}) error { return sw.SendEvent(eventType, v) } +// IsIngress returns true if running through Home Assistant Ingress proxy +func (sw *StreamWriter) IsIngress() bool { + return sw.isIngress +} + // SendMessage sends a simple message func (sw *StreamWriter) SendMessage(message string) error { return sw.SendEvent("message", map[string]string{"message": message})