Skip to content

Commit 0efe5b2

Browse files
jpescecharlie0129
andauthored
feat: add --json flag to batt status for machine-readable output (#122)
* feat: add --json flag to status command for machine-readable output * chore: fix code formatting Signed-off-by: Charlie Chiang <[email protected]> --------- Signed-off-by: Charlie Chiang <[email protected]> Co-authored-by: Charlie Chiang <[email protected]>
1 parent 268ee37 commit 0efe5b2

2 files changed

Lines changed: 215 additions & 27 deletions

File tree

cmd/batt/status.go

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,39 @@ type statusData struct {
2121
config *config.RawFileConfig
2222
}
2323

24+
// computeTimeToLimit calculates the estimated minutes until the charge limit is
25+
// reached. Returns nil when not applicable (not charging, limit >= 100, charge
26+
// already at/above limit, or result is zero).
27+
func computeTimeToLimit(data *statusData, cfg *config.File) *int {
28+
if data.batteryInfo.State != powerinfo.Charging || cfg.UpperLimit() >= 100 || data.currentCharge >= cfg.UpperLimit() {
29+
return nil
30+
}
31+
32+
// Work in mAh directly (no Wh conversions)
33+
designCapacitymAh := float64(data.batteryInfo.Design)
34+
targetCapacitymAh := float64(cfg.UpperLimit()) / 100.0 * designCapacitymAh
35+
currentCapacitymAh := float64(data.currentCharge) / 100.0 * designCapacitymAh
36+
capacityToChargemAh := targetCapacitymAh - currentCapacitymAh
37+
38+
// Convert charge rate (mW) to mA using V: mA = mW / V
39+
var chargeRatemA float64
40+
if data.batteryInfo.DesignVoltage > 0 {
41+
chargeRatemA = float64(data.batteryInfo.ChargeRate) / data.batteryInfo.DesignVoltage
42+
}
43+
44+
if chargeRatemA <= 0 || capacityToChargemAh <= 0 {
45+
return nil
46+
}
47+
48+
timeToLimitHours := capacityToChargemAh / chargeRatemA
49+
minutes := int(timeToLimitHours * 60)
50+
if minutes <= 0 {
51+
return nil
52+
}
53+
54+
return &minutes
55+
}
56+
2457
// fetchStatusData gathers all data required for the status command from the daemon.
2558
func fetchStatusData() (*statusData, error) {
2659
charging, err := apiClient.GetCharging()
@@ -65,7 +98,9 @@ func fetchStatusData() (*statusData, error) {
6598

6699
//nolint:gocyclo
67100
func NewStatusCommand() *cobra.Command {
68-
return &cobra.Command{
101+
var jsonOutput bool
102+
103+
cmd := &cobra.Command{
69104
Use: "status",
70105
GroupID: gBasic,
71106
Short: "Get the current status of batt",
@@ -79,6 +114,10 @@ func NewStatusCommand() *cobra.Command {
79114

80115
cfg := config.NewFileFromConfig(data.config, "")
81116

117+
if jsonOutput {
118+
return printStatusJSON(cmd, data, cfg)
119+
}
120+
82121
// Charging status.
83122
cmd.Println(bold("Charging status:"))
84123

@@ -133,41 +172,26 @@ func NewStatusCommand() *cobra.Command {
133172

134173
cmd.Printf(" Current charge: %s\n", bold("%d%%", data.currentCharge))
135174

136-
if data.batteryInfo.State == powerinfo.Charging && cfg.UpperLimit() < 100 && data.currentCharge < cfg.UpperLimit() {
137-
// Work in mAh directly (no Wh conversions)
138-
designCapacitymAh := float64(data.batteryInfo.Design)
139-
targetCapacitymAh := float64(cfg.UpperLimit()) / 100.0 * designCapacitymAh
140-
currentCapacitymAh := float64(data.currentCharge) / 100.0 * designCapacitymAh
141-
capacityToChargemAh := targetCapacitymAh - currentCapacitymAh
142-
143-
// Convert charge rate (mW) to mA using V: mA = mW / V
144-
var chargeRatemA float64
145-
if data.batteryInfo.DesignVoltage > 0 {
146-
chargeRatemA = float64(data.batteryInfo.ChargeRate) / data.batteryInfo.DesignVoltage
147-
}
148-
149-
if chargeRatemA > 0 && capacityToChargemAh > 0 {
150-
timeToLimitHours := capacityToChargemAh / chargeRatemA
151-
timeToLimitMinutes := float64(timeToLimitHours * 60)
152-
153-
if timeToLimitMinutes > 0.00 {
154-
cmd.Printf(" Time to limit (%d%%): %s\n", cfg.UpperLimit(), bold("~%d minutes", int(timeToLimitMinutes)))
155-
}
156-
}
175+
if ttl := computeTimeToLimit(data, cfg); ttl != nil {
176+
cmd.Printf(" Time to limit (%d%%): %s\n", cfg.UpperLimit(), bold("~%d minutes", *ttl))
157177
}
158178

159-
state := "not charging"
179+
var displayState string
160180
switch data.batteryInfo.State {
161181
case powerinfo.Charging:
162-
state = color.GreenString("charging")
182+
displayState = color.GreenString("charging")
163183
case powerinfo.Discharging:
164184
if data.batteryInfo.ChargeRate != 0 {
165-
state = color.RedString("discharging")
185+
displayState = color.RedString("discharging")
186+
} else {
187+
displayState = "not charging"
166188
}
167189
case powerinfo.Full:
168-
state = "full"
190+
displayState = "full"
191+
default:
192+
displayState = "not charging"
169193
}
170-
cmd.Printf(" State: %s\n", bold("%s", state))
194+
cmd.Printf(" State: %s\n", bold("%s", displayState))
171195
cmd.Printf(" Full capacity: %s\n", bold("%d mAh", data.batteryInfo.Design))
172196
// Show charge rate in Watts with sign (+ charging, - discharging) and bright color (bold)
173197
watts := float64(data.batteryInfo.ChargeRate) / 1e3
@@ -227,6 +251,10 @@ func NewStatusCommand() *cobra.Command {
227251
return nil
228252
},
229253
}
254+
255+
cmd.Flags().BoolVar(&jsonOutput, "json", false, "Output status in JSON format")
256+
257+
return cmd
230258
}
231259

232260
func bool2Text(b bool) string {

cmd/batt/status_json.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"math"
6+
"time"
7+
8+
"github.com/spf13/cobra"
9+
10+
"github.com/charlie0129/batt/pkg/calibration"
11+
"github.com/charlie0129/batt/pkg/config"
12+
"github.com/charlie0129/batt/pkg/powerinfo"
13+
)
14+
15+
type statusJSON struct {
16+
Charging statusChargingJSON `json:"charging"`
17+
Battery statusBatteryJSON `json:"battery"`
18+
Configuration statusConfigJSON `json:"configuration"`
19+
// Calibration is omitted when telemetry data is unavailable (e.g. API error).
20+
Calibration *statusCalibrationJSON `json:"calibration,omitempty"`
21+
}
22+
23+
type statusChargingJSON struct {
24+
AllowCharging bool `json:"allowCharging"`
25+
UseAdapter bool `json:"useAdapter"`
26+
PluggedIn bool `json:"pluggedIn"`
27+
}
28+
29+
type statusBatteryJSON struct {
30+
CurrentChargePercent int `json:"currentChargePercent"`
31+
State string `json:"state"`
32+
TimeToLimitMinutes *int `json:"timeToLimitMinutes"`
33+
FullCapacityMah int `json:"fullCapacityMah"`
34+
ChargeRateWatts float64 `json:"chargeRateWatts"`
35+
VoltageVolts float64 `json:"voltageVolts"`
36+
}
37+
38+
type statusConfigJSON struct {
39+
Enabled bool `json:"enabled"`
40+
UpperLimitPercent int `json:"upperLimitPercent"`
41+
LowerLimitPercent int `json:"lowerLimitPercent"`
42+
PreventIdleSleep bool `json:"preventIdleSleep"`
43+
DisableChargingPreSleep bool `json:"disableChargingPreSleep"`
44+
PreventSystemSleep bool `json:"preventSystemSleep"`
45+
AllowNonRootAccess bool `json:"allowNonRootAccess"`
46+
ControlMagSafeLed statusMagSafeLedJSON `json:"controlMagSafeLed"`
47+
}
48+
49+
type statusMagSafeLedJSON struct {
50+
Enabled bool `json:"enabled"`
51+
Mode string `json:"mode"`
52+
}
53+
54+
type statusCalibrationJSON struct {
55+
Phase string `json:"phase"`
56+
StartedAt *time.Time `json:"startedAt"`
57+
Paused bool `json:"paused"`
58+
CanPause bool `json:"canPause"`
59+
CanCancel bool `json:"canCancel"`
60+
Message string `json:"message"`
61+
Schedule statusCalibrationSchedJSON `json:"schedule"`
62+
}
63+
64+
type statusCalibrationSchedJSON struct {
65+
Enabled bool `json:"enabled"`
66+
Cron string `json:"cron"`
67+
ScheduledAt *time.Time `json:"scheduledAt"`
68+
}
69+
70+
// batteryStateString returns a camelCase string for the battery state.
71+
func batteryStateString(state powerinfo.BatteryState, chargeRate int) string {
72+
switch state {
73+
case powerinfo.Charging:
74+
return "charging"
75+
case powerinfo.Discharging:
76+
if chargeRate != 0 {
77+
return "discharging"
78+
}
79+
return "notCharging"
80+
case powerinfo.Full:
81+
return "full"
82+
default:
83+
return "notCharging"
84+
}
85+
}
86+
87+
func printStatusJSON(cmd *cobra.Command, data *statusData, cfg *config.File) error {
88+
mode := cfg.ControlMagSafeLED()
89+
upperLimit := cfg.UpperLimit()
90+
enabled := upperLimit < 100
91+
92+
// When batt is disabled (limit=100%), lower limit equals upper limit
93+
// because the battery is always allowed to charge freely.
94+
lowerLimit := cfg.LowerLimit()
95+
if !enabled {
96+
lowerLimit = upperLimit
97+
}
98+
99+
out := statusJSON{
100+
Charging: statusChargingJSON{
101+
AllowCharging: data.charging,
102+
UseAdapter: data.adapter,
103+
PluggedIn: data.pluggedIn,
104+
},
105+
Battery: statusBatteryJSON{
106+
CurrentChargePercent: data.currentCharge,
107+
State: batteryStateString(data.batteryInfo.State, data.batteryInfo.ChargeRate),
108+
TimeToLimitMinutes: computeTimeToLimit(data, cfg),
109+
FullCapacityMah: data.batteryInfo.Design,
110+
ChargeRateWatts: math.Round(float64(data.batteryInfo.ChargeRate)/1e3*10) / 10,
111+
VoltageVolts: math.Round(data.batteryInfo.DesignVoltage*100) / 100,
112+
},
113+
Configuration: statusConfigJSON{
114+
Enabled: enabled,
115+
UpperLimitPercent: upperLimit,
116+
LowerLimitPercent: lowerLimit,
117+
PreventIdleSleep: cfg.PreventIdleSleep(),
118+
DisableChargingPreSleep: cfg.DisableChargingPreSleep(),
119+
PreventSystemSleep: cfg.PreventSystemSleep(),
120+
AllowNonRootAccess: cfg.AllowNonRootAccess(),
121+
ControlMagSafeLed: statusMagSafeLedJSON{
122+
Enabled: mode != config.ControlMagSafeModeDisabled,
123+
Mode: string(mode),
124+
},
125+
},
126+
}
127+
128+
tr, err := apiClient.GetTelemetry(false, true)
129+
if err == nil && tr.Calibration != nil {
130+
cal := tr.Calibration
131+
132+
var startedAt *time.Time
133+
if cal.Phase != calibration.PhaseIdle && !cal.StartedAt.IsZero() {
134+
startedAt = &cal.StartedAt
135+
}
136+
137+
cron := cfg.Cron()
138+
sched := statusCalibrationSchedJSON{
139+
Enabled: cron != "",
140+
Cron: cron,
141+
}
142+
if cron != "" && !cal.ScheduledAt.IsZero() {
143+
sched.ScheduledAt = &cal.ScheduledAt
144+
}
145+
146+
out.Calibration = &statusCalibrationJSON{
147+
Phase: string(cal.Phase),
148+
StartedAt: startedAt,
149+
Paused: cal.Paused,
150+
CanPause: cal.CanPause,
151+
CanCancel: cal.CanCancel,
152+
Message: cal.Message,
153+
Schedule: sched,
154+
}
155+
}
156+
157+
enc := json.NewEncoder(cmd.OutOrStdout())
158+
enc.SetIndent("", " ")
159+
return enc.Encode(out)
160+
}

0 commit comments

Comments
 (0)