Documentation
¶
Overview ¶
Package cellwatch reads battery state from the host operating system and records it, so that charge, drain rate and per-application activity can be reported over time.
The package is deliberately honest about what it cannot know. No mainstream operating system exposes true per-application battery drain, and the ones that come closest either require administrator rights or an undocumented file format. Rather than fill those gaps with invented numbers, every reading is returned alongside a Capability bitmask describing what the platform actually reported. A field the platform could not supply is left un-known; it is never reported as zero.
The package imports nothing outside the standard library and its own dependencies, so it can be split out of a host project without dragging that project along.
Package-level SQLite store for recorded battery history.
The host project already links github.com/ncruces/go-sqlite3: the SQLite engine ships inside that module and is already present in the built binary, so using it here adds no module and no bytes. What follows is a small purpose-built schema rather than a reuse of the project's file index, because battery history has a different lifecycle: a cleared file index is rebuilt in seconds, and a cleared battery history is gone.
Two measurements taken against this driver shaped the code below.
Binding time.Time into an INTEGER column fails with "datatype mismatch", so every instant is passed as an explicit int64 of Unix microseconds. The driver's default text encoding would have failed on the first insert.
Pragmas are set in the DSN rather than with a follow-up Exec. A bare "PRAGMA journal_mode=WAL" runs once on whatever connection happens to be current, which is correct only by accident of the connection count. In the DSN it is applied to every connection the pool ever creates.
Index ¶
- Variables
- type ACState
- type AppActivity
- type AppTotal
- type Attribution
- type Attributor
- type Capability
- type Coverage
- type Period
- type Point
- type Provenance
- type Range
- type Rate
- type RateEstimator
- type RateParams
- type Reader
- type Sample
- type Sampler
- type SamplerConfig
- type SamplerStats
- type Series
- type Status
- type Store
- type SystemUsage
- type Tier
- type UsageReport
- type WattageSource
Constants ¶
This section is empty.
Variables ¶
var ( // ErrUnsupported reports that the current platform has no reader. It is a // normal answer on a platform outside the supported set, not a bug. ErrUnsupported = errors.New("cellwatch: battery monitoring is not supported on this platform") // ErrNoBattery reports that the platform answered correctly but found no // battery, which is the ordinary state of a desktop. ErrNoBattery = errors.New("cellwatch: no battery present") // ErrNeedsElevation reports that the data exists but reading it requires // privileges this process does not have. ErrNeedsElevation = errors.New("cellwatch: reading this data requires elevation") )
Functions ¶
This section is empty.
Types ¶
type AppActivity ¶
type AppActivity struct {
Name string
// Procs is how many processes were folded into this name.
Procs int
// CPUSeconds is measured CPU time consumed in the window.
CPUSeconds float64
// ReadBytes and WriteBytes are measured I/O in the window.
ReadBytes uint64
WriteBytes uint64
Share float64
// PctPerHour is the drain rate apportioned to it. ESTIMATE.
PctPerHour float64
// Provenance is always ProvEstimated for Share and PctPerHour. It travels
// with the data so a stored value cannot later be read as a measurement.
Provenance Provenance `json:"provenance"`
}
AppActivity is one application's measured activity over a window, together with the share of observed drain apportioned to it.
Everything from Share onward is an estimate. No operating system reports per-application battery use to an unprivileged process, so the share is derived by weighting CPU time and I/O and dividing by the total. The field names say so, because a caller that renames them to "Watts" has removed the only warning the data carries.
type AppTotal ¶
type AppTotal struct {
Name string `json:"name"`
PctPerHour float64 `json:"pct_per_hour_estimate"`
CPUSeconds float64 `json:"cpu_seconds"`
ReadBytes uint64 `json:"read_bytes"`
WriteBytes uint64 `json:"write_bytes"`
}
AppTotal is one application's total over a query range.
type Attribution ¶
type Attribution struct {
Window time.Duration `json:"window"`
// Processes is how many processes contributed.
Processes int `json:"processes"`
// ProcessesLost is how many were dropped for a counter that moved backwards
// or an identity that could not be confirmed.
ProcessesLost int `json:"processes_lost"`
// CPUCapacityPct is the share of total machine CPU the attributed
// processes used, and is reported even when the result is Idle, because it
// is the measurement that explains why the estimate was refused.
CPUCapacityPct float64 `json:"cpu_capacity_pct"`
// Idle reports that activity was too low to apportion.
Idle bool `json:"idle"`
// Apps is the per-application table, largest share first.
Apps []AppActivity `json:"apps,omitempty"`
// Unattributed is the share of drain that no process was credited with.
Unattributed float64 `json:"unattributed_estimate"`
// Notes carry the reasons a figure is limited. They are for display.
Notes []string `json:"notes,omitempty"`
// Reason explains an empty result.
Reason string `json:"reason,omitempty"`
}
Attribution is the result of apportioning drain across applications.
type Attributor ¶
type Attributor struct {
// contains filtered or unexported fields
}
Attributor accumulates process snapshots and apportions observed drain across applications. The zero value is not usable; construct with NewAttributor.
The only state is the pair of snapshots. Nothing else is carried between Sample and Attribute, so Attribute is a pure function of the window it is given: calling it twice over the same window returns the same table, and a caller that renders the attribution and reports its coverage does not get figures that drift on every render.
func NewAttributor ¶
func NewAttributor() *Attributor
NewAttributor returns an unattributed attributor. Call Sample once per tick and Attribute to obtain the current estimate.
func (*Attributor) Attribute ¶
func (a *Attributor) Attribute(systemPctPerHour float64) Attribution
Attribute computes per-application activity over the window since the first sample, and the share of observed drain each is apportioned.
func (*Attributor) Sample ¶
func (a *Attributor) Sample() error
Sample records one instant. The first call only establishes a baseline, because a single snapshot contains no interval and therefore no rate.
type Capability ¶
type Capability uint32
Capability is a bitmask describing what the running platform was able to report. It exists so that a caller can ask rather than guess: a report that shows an empty health field is visibly a limitation, not a machine with a perfect battery.
A platform reader must set only the bits it actually read. Padding the mask to look complete would defeat the purpose of having it.
const ( // CapChargePct reports charge remaining as a percentage. CapChargePct Capability = 1 << iota // CapACState reports whether the machine is on mains. CapACState // CapStatusWord reports the platform's own status string verbatim. CapStatusWord // CapWatts reports instantaneous power draw. CapWatts // CapFullCapacity reports present full-charge capacity in watt-hours. CapFullCapacity // CapDesignCapacity reports factory design capacity in watt-hours. CapDesignCapacity // CapHealth reports full capacity as a percentage of design. CapHealth // CapCycleCount reports completed charge cycles. CapCycleCount // CapTimeRemaining reports the platform's own time-remaining estimate. CapTimeRemaining // CapTemperature reports pack temperature. CapTemperature // CapCount reports how many batteries the platform could enumerate. CapCount )
func PlatformCapabilities ¶
func PlatformCapabilities() Capability
PlatformCapabilities reports what this build of cellwatch can ever read, before any particular machine is consulted. It is the ceiling that PlatformSupport then narrows: a build for linux knows it can read sysfs, but whether the running kernel exposes power_now is a question about the machine.
CapAppEnergy is deliberately absent. No supported platform reports true per-application battery energy through any interface cellwatch can read without administrator rights. Per-application figures in this package are apportioned from CPU and I/O activity and are estimates; naming the absence here is what keeps that from being forgotten.
func (Capability) Has ¶
func (c Capability) Has(want Capability) bool
Has reports whether every bit in want is set.
func (Capability) String ¶
func (c Capability) String() string
String renders the set capabilities in a stable order, for diagnostics and for the JSON payload so that a consumer can see what the platform lacked.
func (Capability) With ¶
func (c Capability) With(bits Capability) Capability
With returns c with the given bits set, used by readers as they accumulate.
type Coverage ¶
type Coverage struct {
Samples int `json:"samples"`
From time.Time `json:"from"`
To time.Time `json:"to"`
Expected int `json:"expected"`
Pct float64 `json:"pct"`
}
Coverage records how much of a range the store actually holds. A report built from a machine that never ran the sampler is not a machine that used no power, and the two must be distinguishable in the output.
type Period ¶
type Period uint8
Period is a reporting window.
const ( // PeriodDay is the current UTC day. PeriodDay Period = iota // PeriodWeek is the current ISO week, which starts on Monday. PeriodWeek // PeriodMonth is the current calendar month. PeriodMonth // PeriodYear is the current calendar year. PeriodYear // PeriodAll is everything recorded. PeriodAll )
type Point ¶
type Point struct {
At time.Time `json:"at"`
CPUSeconds float64 `json:"cpu_seconds"`
ReadBytes uint64 `json:"read_bytes"`
WriteBytes uint64 `json:"write_bytes"`
}
Point is one sample in a per-application series.
type Provenance ¶
type Provenance uint8
Provenance records whether a figure was measured by the platform or derived by this package. It is carried in output so a consumer cannot present an apportionment as a measurement.
const ( // ProvUnknown is the zero value, deliberately. An unset provenance must not // default to ProvMeasured, because that would let a value which was never // marked read as a measurement purely by being left alone. ProvUnknown Provenance = iota // ProvMeasured came from the operating system. ProvMeasured // ProvEstimated was apportioned by this package. ProvEstimated )
func (Provenance) Estimated ¶
func (p Provenance) Estimated() bool
Estimated reports whether the value is derived rather than measured.
func (Provenance) String ¶
func (p Provenance) String() string
type Range ¶
Range is a half-open interval [From, To).
func ThisPeriod ¶
ThisPeriod returns the range for the period containing now.
Periods are cut in UTC rather than in local time. A local day is twenty-three or twenty-five hours twice a year, and a daily total that silently changes length at a daylight-saving boundary is a total nobody can compare against yesterday's. UTC costs the reader the comfort of their own midnight and buys totals that mean the same thing every day of the year.
type Rate ¶
type Rate struct {
// PctPerHour is the smoothed estimate used for display.
PctPerHour float64
// RawPctPerHour is the un-smoothed figure computed across the window
// endpoints. It is reported alongside the smoothed one so a consumer can
// see how much smoothing was applied, rather than being handed a number
// with no indication that it is a moving average.
RawPctPerHour float64
// Stable reports whether enough history accumulated to trust the figure.
Stable bool
// Span is the interval the estimate was computed over.
Span time.Duration
// Samples is how many readings contributed.
Samples int
// Reason explains a non-Stable result in a form fit for display, so a UI
// can say why it is waiting rather than showing a wrong number.
Reason string
}
Rate is an estimated rate of change of charge, in percentage points per hour. Positive means the pack is losing charge.
type RateEstimator ¶
type RateEstimator struct {
// contains filtered or unexported fields
}
RateEstimator accumulates readings and reports the rate of change of charge. It is the exported form of the window logic above, for callers that sample over time rather than analysing a slice they already hold.
The zero value is not usable; construct one with NewRateEstimator so the window parameters are explicit rather than silently defaulted.
func NewRateEstimator ¶
func NewRateEstimator(interval time.Duration) *RateEstimator
NewRateEstimator returns an estimator sampling at the given interval. The window is sized from that interval, and is deliberately long: a window shorter than the platform's charge quantisation does not average the noise away, it manufactures it.
func (*RateEstimator) Add ¶
func (e *RateEstimator) Add(s Sample)
Add records one reading, discarding the window if the gap since the previous reading indicates a sleep or a suspend. Averaging across one of those produces a rate for a machine that never existed.
func (*RateEstimator) Rate ¶
func (e *RateEstimator) Rate() Rate
Rate returns the current estimate and why it is or is not trustworthy.
func (*RateEstimator) Reset ¶
func (e *RateEstimator) Reset()
Reset drops accumulated history, which a caller does after a power source change or when it wants a fresh measurement.
type RateParams ¶
type RateParams struct {
// MinSpan is the shortest interval over which a rate is considered
// measured. Below this the result is a quantiser reading.
MinSpan time.Duration
// MinSamples is the smallest number of readings that can produce a rate.
MinSamples int
// Alpha is the weight given to a new observation in the exponential
// moving average applied to the displayed figure. Lower is smoother.
Alpha float64
}
RateParams configures the estimator. The defaults are set for the measured platform behaviour rather than for a round number.
func DefaultRateParams ¶
func DefaultRateParams(interval time.Duration) RateParams
DefaultRateParams spans roughly ten minutes at the default sample interval, which is comfortably longer than the seventy-second quantum measured on Windows hardware. An estimator tuned to react quickly is an estimator that reports fiction.
type Reader ¶
type Reader interface {
// Read returns the current state. A machine with no battery returns a
// Status with Present false and a nil error: that is an answer, not a
// failure. A read that genuinely could not be performed returns an error.
Read() (Status, error)
}
Reader reads battery state from the host. Implementations are supplied per platform by the build-tagged files in this package.
type Sampler ¶
type Sampler struct {
// contains filtered or unexported fields
}
Sampler reads battery state on an interval and records it.
The daemon question deserves a straight answer rather than a hedge. A daily, weekly, monthly or yearly total can only be complete if something is observing the machine while nobody is looking at it. A command that records only while it is open produces a history with holes in it, and a hole is indistinguishable from a period in which nothing happened. So the sampler runs until it is stopped, and it is started deliberately rather than installed as a service: an installer that silently begins observing a machine is a different decision from running a program, and only the user gets to make it.
func NewSampler ¶
func NewSampler(cfg SamplerConfig) (*Sampler, error)
NewSampler returns a sampler configured by cfg.
func (*Sampler) Interval ¶ added in v0.6.1
Interval reports the sampling period actually in force, which is the configured interval raised to the 5s floor. A caller that displays a requested interval rather than this one will misreport what it is doing.
func (*Sampler) Run ¶
Run samples until the context is cancelled or the process is interrupted, and returns the number of readings recorded.
A failed read is counted and the loop continues. A sampler that stops on the first transient failure is a sampler that has silently become a program that records nothing, and nothing in its output would say so. The failure is surfaced through Stats and in the final report instead.
func (*Sampler) Stats ¶
func (s *Sampler) Stats() SamplerStats
Stats returns a snapshot of progress.
type SamplerConfig ¶
type SamplerConfig struct {
// Reader supplies battery state. Required.
Reader Reader
// Store receives readings. Required.
Store Store
// Interval is the sampling period. Values below 5s are raised to it.
//
// The floor is not a politeness setting. The charge counter is quantised to
// whole percentage points, so sampling faster than the quantisation does
// not resolve anything; it multiplies the work and the storage by the
// sampling rate while producing identical numbers.
Interval time.Duration
// Attrib enables per-application attribution, which enumerates every
// process on the machine and is therefore markedly more expensive.
Attrib bool
// Out, when set, receives a line per pass.
Out io.Writer
}
SamplerConfig configures a Sampler.
type SamplerStats ¶
type SamplerStats struct {
Started time.Duration `json:"started_for"`
Samples int `json:"samples"`
Recorded int `json:"recorded"`
Skipped int `json:"skipped"`
LastErr string `json:"last_error,omitempty"`
}
SamplerStats reports what a sampler managed to do.
type Series ¶
type Series struct {
App string `json:"app"`
Tier Tier `json:"tier"`
Points []Point `json:"points"`
}
Series is a per-application history over a range.
type Status ¶
type Status struct {
// Present is false when the platform reported no battery, which is the
// normal answer for a desktop rather than a failure.
Present bool
// Count is the number of batteries the platform reported. Windows
// GetSystemPowerStatus reports only the first pack, so a two-battery
// machine reads 1.
Count int
// Source names the mechanism that produced this reading, for example
// "GetSystemPowerStatus". It is a diagnostic for humans and logs, not a
// stable API; callers should not switch on it.
Source string
// AC is the mains-line state.
AC ACState
// Status is the platform's own status word verbatim: "Charging",
// "Discharging", "Full", "Not charging" or "Unknown". Empty when the
// platform has no such word.
Status string
// Charging is taken from the platform's own charging flag. It is never
// inferred from the sign of a current reading, because that sign
// convention is not consistent across platforms.
Charging bool
// Full is true when the platform reports the pack fully charged.
Full bool
// ChargePct is charge remaining, 0 to 100.
ChargePct float64
ChargeKnown bool
// Watts is instantaneous power draw for the whole machine. Consult
// Wattage to judge how much it is worth.
Watts float64
WattsKnown bool
Wattage WattageSource
// FullWh is present full-charge capacity in watt-hours.
FullWh float64
FullWhKnown bool
// DesignWh is factory design capacity in watt-hours.
DesignWh float64
DesignWhKnown bool
// HealthPct is FullWh as a percentage of DesignWh.
HealthPct float64
HealthKnown bool
// Cycles is the completed charge-cycle count. Absent on most platforms.
Cycles int
CyclesKnown bool
// TimeToEmptyS is the platform's own time-remaining estimate in seconds.
// Firmware reports an unsigned sentinel rather than a negative number when
// the value is meaningless, so readers must reject anything at or above
// 1<<31 before trusting it.
TimeToEmptyS int64
TimeToEmptyKnown bool
// TemperatureC is pack temperature where the platform exposes it. It is
// cosmetic and is not used in any calculation.
TemperatureC float64
TemperatureKnown bool
// Cap is the set of fields this platform actually reported.
Cap Capability
}
Status is a single reading of the machine's battery state.
Every measured quantity has a companion Known flag. This is the central honesty invariant of the package: a platform that cannot report watts leaves WattsKnown false and Watts at zero, which is distinguishable from a machine genuinely drawing no power. Collapsing the two would let a caller print a confident "0 W" for a value nobody measured.
func (Status) Has ¶
func (s Status) Has(c Capability) bool
Has reports whether the platform supplied the given capability.
func (Status) IsCharging ¶
Charging reports whether the pack is taking charge, treating a full pack on mains as not charging, which is what the platform means by the two states.
type Store ¶
type Store interface {
// Record writes one reading and folds it into the rollup tiers.
Record(ctx context.Context, s Status) error
// Apps records per-application activity for a window. Attribution is an
// estimate derived from CPU and I/O weight, never a measurement, and every
// stored value carries that provenance.
Apps(ctx context.Context, at time.Time, apps []AppActivity) error
// TopApps returns the highest attributed applications within a range.
TopApps(ctx context.Context, r Range, n int) ([]AppTotal, error)
// Usage summarises system activity and coverage over a range.
Usage(ctx context.Context, r Range) (*UsageReport, error)
// AppHistory returns one application's attributed activity over a range.
AppHistory(ctx context.Context, app string, r Range) (Series, error)
// Close releases the database.
Close() error
}
Store is the persistence seam. It is deliberately narrow so that swapping the engine is a new implementation rather than a rewrite of everything above it.
type SystemUsage ¶
type SystemUsage struct {
ChargeStartPct float64 `json:"charge_start_pct"`
ChargeEndPct float64 `json:"charge_end_pct"`
ChargeLostPct float64 `json:"charge_lost_pct"`
DrainPctPerHour float64 `json:"drain_pct_per_hour_estimate"`
ChargingSamples int `json:"charging_samples"`
DischargingSamples int `json:"discharging_samples"`
}
SystemUsage is measured system-level activity over a range. The charge delta is measured by the platform; the rate derived from it is an estimate, and Provenance says which applies to the report as a whole.
type Tier ¶
type Tier uint8
Tier is a rollup resolution. Raw samples are kept briefly and aggregated upward, so a query costs one row per bucket at the resolution asked for rather than a scan of the whole history.
const ( // TierRaw is an individual reading. TierRaw Tier = iota // TierMinute aggregates one minute. TierMinute // TierHour aggregates one hour. TierHour // TierDay aggregates one UTC day. TierDay // TierWeek aggregates one ISO week, which starts on Monday. TierWeek // TierMonth aggregates one calendar month. TierMonth // TierYear aggregates one calendar year. TierYear )
type UsageReport ¶
type UsageReport struct {
Range Range `json:"range"`
System SystemUsage `json:"system"`
Coverage Coverage `json:"coverage"`
Provenance Provenance `json:"provenance"`
Apps []AppTotal `json:"apps,omitempty"`
Notes []string `json:"notes,omitempty"`
}
UsageReport is a summary over a range.
type WattageSource ¶
type WattageSource uint8
WattageSource says where a wattage figure came from, because the sources are not equally trustworthy. A caller that wants to weigh a number needs this.
const ( // WattageNone means no wattage was determined. WattageNone WattageSource = iota // WattageSysfs is a direct read of power_now on Linux. WattageSysfs // WattageIOReg is Voltage x Amperage from the macOS IORegistry. WattageIOReg // WattageChargeDelta is derived by dividing capacity lost over time by // elapsed time. It is only as precise as the platform's charge counter, // which on Windows is quantised to whole percentage points. WattageChargeDelta // WattageDevice is a charge rate read directly from the battery // controller. On Windows it comes from the battery class driver rather // than from the system power status call, which is why it is reported // separately: it is the one source that is a measurement rather than a // derivation, and a caller ranking sources should be able to prefer it. WattageDevice )
func (WattageSource) String ¶
func (w WattageSource) String() string