Import UniFi Protect live views + optional auto-resync
views ls/dump/import copy a Protect live view (cameras + grid) into a layout; grid inferred from slot count (asymmetric presets TBD, see views dump). Imported layouts link back via protect_view. Daemon re-syncs the active linked view every view_refresh_seconds (0= off), so Protect-side edits appear on the wall. Needs the controller password at runtime; off by default keeps the wall credential-free. Shared viewmap package used by CLI and daemon. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -167,6 +167,53 @@ func (c *Client) StreamURL(alias string) string {
|
||||
return fmt.Sprintf("rtsps://%s:%d/%s?enableSrtp", c.host, c.rtspPort, alias)
|
||||
}
|
||||
|
||||
// LiveView is a saved Protect "Live View": an ordered list of slots plus a
|
||||
// layout preset id. Each slot holds one or more cameras (cyclable).
|
||||
type LiveView struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Layout int `json:"layout"` // Protect grid-preset id
|
||||
Slots []LiveViewSlot `json:"slots"`
|
||||
}
|
||||
|
||||
// LiveViewSlot is one cell of a live view.
|
||||
type LiveViewSlot struct {
|
||||
Cameras []string `json:"cameras"` // Protect camera ids
|
||||
CycleMode string `json:"cycleMode"`
|
||||
CycleInterval int `json:"cycleInterval"`
|
||||
}
|
||||
|
||||
// LiveViews returns the controller's saved live views (from the bootstrap
|
||||
// document, which always includes them) plus the raw liveviews JSON, pretty-
|
||||
// printed, so callers can inspect the exact `layout`/`slots` shape.
|
||||
func (c *Client) LiveViews(ctx context.Context) ([]LiveView, []byte, error) {
|
||||
resp, err := c.do(ctx, http.MethodGet, "/proxy/protect/api/bootstrap", nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
|
||||
return nil, nil, fmt.Errorf("bootstrap failed (%s): %s", resp.Status, strings.TrimSpace(string(snippet)))
|
||||
}
|
||||
var b struct {
|
||||
LiveViews []json.RawMessage `json:"liveviews"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&b); err != nil {
|
||||
return nil, nil, fmt.Errorf("decoding bootstrap: %w", err)
|
||||
}
|
||||
views := make([]LiveView, 0, len(b.LiveViews))
|
||||
for _, raw := range b.LiveViews {
|
||||
var v LiveView
|
||||
if err := json.Unmarshal(raw, &v); err != nil {
|
||||
continue
|
||||
}
|
||||
views = append(views, v)
|
||||
}
|
||||
pretty, _ := json.MarshalIndent(b.LiveViews, "", " ")
|
||||
return views, pretty, nil
|
||||
}
|
||||
|
||||
// BestEnabledChannel returns the highest-resolution channel that has RTSP
|
||||
// enabled, or nil if none are enabled. Preferring the top channel gives the
|
||||
// sharpest wall tile; callers can pick a lower one for dense grids.
|
||||
|
||||
Reference in New Issue
Block a user