feat: integrate bettervent.me device database and add BTU/kW unit switcher
Add bettervent.me provider with lazy-cached device list (~6,700 Eurovent-certified heat pumps), search API endpoint, device search UI with auto-populate, BTU/kW unit switcher for European users, and extended AC fields (SEER, SCOP, COP, TOL, Tbiv, refrigerant). Closes #2.
This commit is contained in:
@@ -311,6 +311,25 @@ func (s *Server) handleLLMConfig(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleBetterventSearch(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
q := r.URL.Query().Get("q")
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := s.betterventProvider.Search(ctx, q)
|
||||
if err != nil {
|
||||
jsonError(w, err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
|
||||
jsonResponse(w, resp)
|
||||
}
|
||||
|
||||
func jsonResponse(w http.ResponseWriter, data any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(data)
|
||||
|
||||
@@ -10,18 +10,20 @@ import (
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/cnachtigall/heatwave-autopilot/internal/bettervent"
|
||||
"github.com/cnachtigall/heatwave-autopilot/internal/config"
|
||||
"github.com/cnachtigall/heatwave-autopilot/internal/llm"
|
||||
)
|
||||
|
||||
// Server holds the HTTP server state.
|
||||
type Server struct {
|
||||
mux *http.ServeMux
|
||||
trans *translations
|
||||
cfg config.Config
|
||||
llmProvider llm.Provider
|
||||
devMode bool
|
||||
ready atomic.Bool
|
||||
mux *http.ServeMux
|
||||
trans *translations
|
||||
cfg config.Config
|
||||
llmProvider llm.Provider
|
||||
betterventProvider *bettervent.Provider
|
||||
devMode bool
|
||||
ready atomic.Bool
|
||||
}
|
||||
|
||||
// Options configures the server.
|
||||
@@ -73,6 +75,9 @@ func New(opts Options) (*Server, error) {
|
||||
// Set up LLM provider
|
||||
s.llmProvider = buildLLMProvider(s.cfg)
|
||||
|
||||
// Set up bettervent provider
|
||||
s.betterventProvider = bettervent.New(nil)
|
||||
|
||||
// Static assets
|
||||
if opts.DevMode {
|
||||
s.mux.Handle("/assets/", http.StripPrefix("/assets/", http.FileServer(http.Dir("web"))))
|
||||
@@ -96,6 +101,7 @@ func New(opts Options) (*Server, error) {
|
||||
s.mux.HandleFunc("/api/llm/summarize", s.handleLLMSummarize)
|
||||
s.mux.HandleFunc("/api/llm/actions", s.handleLLMActions)
|
||||
s.mux.HandleFunc("/api/llm/config", s.handleLLMConfig)
|
||||
s.mux.HandleFunc("/api/bettervent/search", s.handleBetterventSearch)
|
||||
|
||||
s.ready.Store(true)
|
||||
return s, nil
|
||||
|
||||
@@ -188,6 +188,46 @@ func TestReadyz(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBetterventSearch(t *testing.T) {
|
||||
s := testServer(t)
|
||||
req := httptest.NewRequest("GET", "/api/bettervent/search?q=test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.Handler().ServeHTTP(w, req)
|
||||
// The test server won't reach bettervent.me, but the handler should still
|
||||
// return valid JSON (either results or a 502 error).
|
||||
if ct := w.Header().Get("Content-Type"); ct != "application/json" {
|
||||
t.Errorf("got content-type %q, want application/json", ct)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBetterventSearch_EmptyQuery(t *testing.T) {
|
||||
s := testServer(t)
|
||||
req := httptest.NewRequest("GET", "/api/bettervent/search?q=", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.Handler().ServeHTTP(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("got status %d, want 200", w.Code)
|
||||
}
|
||||
var resp struct {
|
||||
Devices []any `json:"devices"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
json.NewDecoder(w.Body).Decode(&resp)
|
||||
if resp.Total != 0 {
|
||||
t.Errorf("expected 0 results for empty query, got %d", resp.Total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBetterventSearch_MethodNotAllowed(t *testing.T) {
|
||||
s := testServer(t)
|
||||
req := httptest.NewRequest("POST", "/api/bettervent/search?q=test", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.Handler().ServeHTTP(w, req)
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Errorf("got status %d, want 405", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLLMSummarize_Noop(t *testing.T) {
|
||||
s := testServer(t)
|
||||
body := `{"date":"2025-07-15","peakTempC":35,"riskLevel":"high"}`
|
||||
|
||||
Reference in New Issue
Block a user