package agentapi import ( "context" "encoding/json" "fmt" "time" ) // LineTestState mirrors the agent's GET /host/line-test (agent ≥ 0.164.0; felhom.eu R-1247): the host's weekly upload // line test — a 10-second iperf3 upload to ep0 inside the offsite tunnel, best of three runs, as ep0's server counted // it. Present=false: never measured (no tunnel, not yet due, or disabled). An agent before the route answers 404 (a // *StatusError); all three mean UNKNOWN, and the controller then sets no qBittorrent limit. type LineTestState struct { Present bool `json:"present"` Mbps float64 `json:"mbps,omitempty"` MeasuredAt string `json:"measured_at,omitempty"` // RFC3339 UTC Method string `json:"method,omitempty"` // WaitingForQuiet (agent ≥ 0.165.0, R-1250): a test is due and could start now, but the host's uplink has not been // quiet for 10 minutes. With a seeding torrent app the controller then holds the app's limit and calls LineTestRun. WaitingForQuiet bool `json:"waiting_for_quiet,omitempty"` } // LineTestRunResult mirrors POST /host/line-test/run. OK=false with Refused: nothing ran (not-due, outside-window, // tries-used, no-tunnel, heavy-op, not-quiet, server-busy); OK=false without it: the runs failed. type LineTestRunResult struct { OK bool `json:"ok"` Mbps float64 `json:"mbps,omitempty"` RunsMbps []float64 `json:"runs_mbps,omitempty"` Refused string `json:"refused,omitempty"` Error string `json:"error,omitempty"` } // LineTestRunTimeout bounds the synchronous request: about 55 s on the agent (5 s settle, 10 s quiet check, three // 10-second runs 5 s apart), each run itself bounded at 25 s. const LineTestRunTimeout = 2 * time.Minute // LineTestRun calls POST /host/line-test/run (R-1250): a test on request while the caller holds its torrent app's // upload down. An agent before the route answers 404 (*StatusError). func (c *Client) LineTestRun(ctx context.Context) (LineTestRunResult, error) { var out LineTestRunResult body, err := c.postSlow(ctx, "/host/line-test/run", struct{}{}, LineTestRunTimeout) if err != nil { return out, err } if err := json.Unmarshal(body, &out); err != nil { return out, fmt.Errorf("agentapi: decode /host/line-test/run: %w", err) } return out, nil } // LineTest calls GET /host/line-test. func (c *Client) LineTest(ctx context.Context) (LineTestState, error) { var out LineTestState body, err := c.get(ctx, "/host/line-test") if err != nil { return out, err } if err := json.Unmarshal(body, &out); err != nil { return out, fmt.Errorf("agentapi: decode /host/line-test: %w", err) } return out, nil } // MeasuredAtTime parses MeasuredAt (zero time when absent or garbled). func (s LineTestState) MeasuredAtTime() time.Time { t, _ := time.Parse(time.RFC3339, s.MeasuredAt) return t }