// Package local talks to the Python recognition engine running on this PC. // // The desktop app is a CLIENT of the engine and never imports it. The engine // owns the cameras, the models and the SQLite gallery; two processes touching // one webcam or one WAL is the failure this separation exists to prevent. package local import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" ) type Client struct { Base string User string Password string http *http.Client } func New(base, user, password string) *Client { return &Client{ Base: strings.TrimRight(base, "/"), User: user, Password: password, // Generous: a camera Test opens an RTSP stream and can legitimately // take ten seconds against a slow NVR. http: &http.Client{Timeout: 45 * time.Second}, } } func (c *Client) do(ctx context.Context, method, path string, body, out any) error { var rdr io.Reader if body != nil { b, err := json.Marshal(body) if err != nil { return err } rdr = bytes.NewReader(b) } req, err := http.NewRequestWithContext(ctx, method, c.Base+path, rdr) if err != nil { return err } if body != nil { req.Header.Set("Content-Type", "application/json") } if c.User != "" { req.SetBasicAuth(c.User, c.Password) } resp, err := c.http.Do(req) if err != nil { // The single most common state on a fresh install: the engine has not // been started yet. Say that, rather than surfacing a dial error the // user cannot act on. return fmt.Errorf("engine not reachable at %s (is it running?): %w", c.Base, err) } defer resp.Body.Close() if resp.StatusCode >= 400 { msg, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) return fmt.Errorf("engine %s %s: %s: %s", method, path, resp.Status, strings.TrimSpace(string(msg))) } if out == nil { return nil } return json.NewDecoder(io.LimitReader(resp.Body, 8<<20)).Decode(out) } func (c *Client) Health(ctx context.Context) (map[string]any, error) { var out map[string]any return out, c.do(ctx, http.MethodGet, "/api/health", nil, &out) } func (c *Client) Stats(ctx context.Context) (map[string]any, error) { var out map[string]any return out, c.do(ctx, http.MethodGet, "/api/stats", nil, &out) } func (c *Client) Events(ctx context.Context, limit int) ([]map[string]any, error) { var out []map[string]any return out, c.do(ctx, http.MethodGet, fmt.Sprintf("/api/events?limit=%d", limit), nil, &out) } func (c *Client) Cameras(ctx context.Context) ([]map[string]any, error) { var out []map[string]any return out, c.do(ctx, http.MethodGet, "/api/cameras", nil, &out) } func (c *Client) AddCamera(ctx context.Context, cam map[string]any) (map[string]any, error) { var out map[string]any return out, c.do(ctx, http.MethodPost, "/api/cameras", cam, &out) } func (c *Client) UpdateCamera(ctx context.Context, id string, cam map[string]any) (map[string]any, error) { var out map[string]any return out, c.do(ctx, http.MethodPatch, "/api/cameras/"+id, cam, &out) } func (c *Client) DeleteCamera(ctx context.Context, id string) error { return c.do(ctx, http.MethodDelete, "/api/cameras/"+id, nil, nil) } func (c *Client) TestCamera(ctx context.Context, cam map[string]any) (map[string]any, error) { var out map[string]any return out, c.do(ctx, http.MethodPost, "/api/cameras/test", cam, &out) } func (c *Client) StartPlacementCheck(ctx context.Context, id string, seconds float64) (map[string]any, error) { var out map[string]any return out, c.do(ctx, http.MethodPost, "/api/cameras/"+id+"/commission", map[string]any{"seconds": seconds}, &out) } func (c *Client) PlacementResult(ctx context.Context, id string) (map[string]any, error) { var out map[string]any return out, c.do(ctx, http.MethodGet, "/api/cameras/"+id+"/commission", nil, &out) } func (c *Client) Identities(ctx context.Context, limit int) ([]map[string]any, error) { var out []map[string]any return out, c.do(ctx, http.MethodGet, fmt.Sprintf("/api/identities?limit=%d", limit), nil, &out) } func (c *Client) Sightings(ctx context.Context, limit int) ([]map[string]any, error) { var out []map[string]any return out, c.do(ctx, http.MethodGet, fmt.Sprintf("/api/sightings?limit=%d", limit), nil, &out) }