79 lines
3.7 KiB
Go
79 lines
3.7 KiB
Go
package upwatch
|
||
|
||
import (
|
||
"context"
|
||
"math"
|
||
"time"
|
||
)
|
||
|
||
// ── qBittorrent's default upload limit (operator ruling 2026-10-23 19:22; the size is decide row R-1245) ─────────
|
||
//
|
||
// qBittorrent seeds at whatever the line gives (measured on 9202: 131 Mbit/s until the receiver capped it), so a
|
||
// family's uploads — a video call, a photo sync — wait behind it. The box sets qBittorrent's GLOBAL upload limit to a
|
||
// share of the measured line, and leaves it alone the moment the household touches it:
|
||
//
|
||
// - never set by the box, qBittorrent unlimited (0) → the box sets it
|
||
// - never set by the box, qBittorrent already has a limit → the household's: never touched
|
||
// - set by the box, qBittorrent still holds exactly that value → the box's: re-set when the line moves > 20 %
|
||
// - set by the box, qBittorrent holds anything else (0 included) → the household changed it: never touched again
|
||
//
|
||
// The value compared is the one qBittorrent STORED (it keeps KiB/s: 2 500 000 reads back as 2 499 584, measured), so
|
||
// the box's own rounding is never mistaken for a household's change. The limit survives a qBittorrent restart
|
||
// (measured: `Session\GlobalUPSpeedLimit` in its config). The line is the host's weekly line TEST (the agent's
|
||
// GET /host/line-test, R-1247), never the off-site push's floor; no test yet → nothing is set (Service.QBLimitNow).
|
||
//
|
||
// Edge: an app reinstalled from scratch starts unlimited while the record still says „set by the box"; that reads as
|
||
// the household removing the limit and is left alone. Recorded in the design (`08` §9.7), not guarded.
|
||
|
||
// QBLimitState is what the box remembers (settings.json `upwatch.qb_limit`).
|
||
type QBLimitState struct {
|
||
BoxSetBytes int64 `json:"box_set_bytes,omitempty"` // the value qBittorrent stored when the box last set it
|
||
LineMbps float64 `json:"line_mbps,omitempty"` // the line the box set it from
|
||
SetAt string `json:"set_at,omitempty"`
|
||
Household bool `json:"household,omitempty"` // the household owns the limit: never touched again
|
||
HouseholdAt string `json:"household_at,omitempty"`
|
||
}
|
||
|
||
// QBLimiter is the qBittorrent side the rule needs (the QBittorrent explainer satisfies it).
|
||
type QBLimiter interface {
|
||
UploadLimit(ctx context.Context, h map[string]string) (int64, error)
|
||
SetUploadLimit(ctx context.Context, bytesPerSec int64) (int64, error)
|
||
}
|
||
|
||
// QBLimitTarget is share × line, in bytes/s.
|
||
func QBLimitTarget(lineMbps, share float64) int64 {
|
||
return int64(math.Round(lineMbps * share * 1e6 / 8))
|
||
}
|
||
|
||
// ApplyQBLimit runs the rule once. It returns the new state and what happened ("set", "kept", "household", "no-line",
|
||
// "error"). It never lowers or raises a limit the household owns.
|
||
func ApplyQBLimit(ctx context.Context, q QBLimiter, st QBLimitState, lineMbps float64, lineKnown bool, share float64, now time.Time) (QBLimitState, string, error) {
|
||
if st.Household {
|
||
return st, "household", nil
|
||
}
|
||
cur, err := q.UploadLimit(ctx, nil)
|
||
if err != nil {
|
||
return st, "error", err
|
||
}
|
||
if st.BoxSetBytes == 0 {
|
||
if cur != 0 {
|
||
st.Household, st.HouseholdAt = true, now.UTC().Format(time.RFC3339)
|
||
return st, "household", nil
|
||
}
|
||
} else if cur != st.BoxSetBytes {
|
||
st.Household, st.HouseholdAt = true, now.UTC().Format(time.RFC3339)
|
||
return st, "household", nil
|
||
} else if lineKnown && st.LineMbps > 0 && math.Abs(lineMbps-st.LineMbps)/st.LineMbps <= 0.2 {
|
||
return st, "kept", nil
|
||
}
|
||
if !lineKnown {
|
||
return st, "no-line", nil
|
||
}
|
||
stored, err := q.SetUploadLimit(ctx, QBLimitTarget(lineMbps, share))
|
||
if err != nil {
|
||
return st, "error", err
|
||
}
|
||
st.BoxSetBytes, st.LineMbps, st.SetAt = stored, lineMbps, now.UTC().Format(time.RFC3339)
|
||
return st, "set", nil
|
||
}
|