Skip to content

Commit

Permalink
feat: make charging status in batt status more user-friendly
Browse files Browse the repository at this point in the history
  • Loading branch information
charlie0129 committed Sep 26, 2023
1 parent b9420f9 commit de5eabc
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 70 deletions.
18 changes: 12 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

# batt

[![Go Checks](https://github.com/charlie0129/batt/actions/workflows/gochecks.yml/badge.svg)](https://github.com/charlie0129/batt/actions/workflows/gochecks.yml)[![Buind Test Binary](https://github.com/charlie0129/batt/actions/workflows/build-test-binary.yml/badge.svg)](https://github.com/charlie0129/batt/actions/workflows/build-test-binary.yml)

`batt` is a tool to control battery charging on Apple Silicon MacBooks.

## Why do you need this?
Expand Down Expand Up @@ -42,18 +44,18 @@ Yes, macOS have optimized battery charging. It will try to find out your chargin

> Currently, it is command-line only. Some knowledge of the command-line is required. A native GUI is possible but not planned. If you want to build a GUI, you can ask me to put a link here to your project
1. (Optional) If you are lazy, there is an install script to help you get the first 3 steps done: `curl -fsSL https://github.com/charlie0129/batt/raw/master/hack/install.sh | bash`. This will download and install the latest _stable_ version for you, then you can skip to step 5.
1. (Optional) If you are lazy, there is an installation script to help you get the first 3 steps done (Internet connection required): `curl -fsSL https://github.com/charlie0129/batt/raw/master/hack/install.sh | bash`. This will download and install the latest _stable_ version for you, then you can skip to step 5.
2. Get the binary. For _stable_ and _beta_ releases, you can find the download link in the [release page](https://github.com/charlie0129/batt/releases). If you want development versions with the latest features and bug fixes, you can download prebuilt binaries from [GitHub Actions](https://github.com/charlie0129/batt/actions/workflows/build-test-binary.yml) (has a retention period of 3 months and you need to `chmod +x batt` after extracting the archive) or [build it yourself](#building) .
3. Put the binary somewhere safe. You don't want to move it after installation :). It is recommended to save it in your `$PATH`, e.g., `/usr/local/bin`, so you can directly call `batt` on the command-line. In this case, the binary location will be `/usr/local/bin/batt`.
4. Install daemon using `sudo batt install`. If you do not want to use `sudo` every time after installation, add the `--allow-non-root-access` flag: `sudo batt install --allow-non-root-access`. To uninstall: please refer to [How to uninstall?](#how-to-uninstall)
5. In case you have GateKeeper turned on, you will see something like _"batt is can't be opened because it was not downloaded from the App Store"_ or _"batt cannot be opened because the developer cannot be verified"_. To solve this, you can either 1. (recommended) Go to System Preferences -> Security & Privacy -> Open Anyway; or 2. run `sudo spctl --master-disable` to disable GateKeeper entirely.
5. In case you have GateKeeper turned on, you will see something like _"batt is can't be opened because it was not downloaded from the App Store"_ or _"batt cannot be opened because the developer cannot be verified"_. If you don't see it, you can skip this step. To solve this, you can either 1. (recommended) Go to System Preferences -> Security & Privacy -> General -> Open Anyway; or 2. run `sudo spctl --master-disable` to disable GateKeeper entirely.
6. Test if it works by running `sudo batt status`. If you see some status info, you are good to go!
7. Time to customize. By default `batt` will set a charge limit to 60%. For example, to set the charge limit to 80%, run `sudo batt limit 80`.
8. As said before, it is highly recommended to disable macOS's optimized charging when using `batt`. To do so, open _System Preferences_, go to _Battery_, and uncheck _Optimized battery charging_.
8. As said before, it is _highly_ recommended to disable macOS's optimized charging when using `batt`. To do so, open _System Preferences_, go to _Battery_, and uncheck _Optimized battery charging_.

Notes:

- If your current charge is above the limit, your computer will just stop charging and stays at your current charge level, which is by design. You can use your battery until it is below the limit to see the effects.
- If your current charge is above the limit, your computer will just stop charging and use power from the wall. It will stay at your current charge level, which is by design. You can use your battery until it is below the limit to see the effects.
- You can refer to [Usage](#usage) for additional configurations. Don't know what a command does? Run `batt help` to see all available commands. To see help for a specific command, run `batt help <command>`.
- To disable the charge limit, run `sudo batt limit 100`.
- [How to uninstall?](#how-to-uninstall) [How to upgrade?](#how-to-upgrade)
Expand Down Expand Up @@ -185,12 +187,16 @@ Manual:

### Why is it Apple Silicon only?

Simply because you don't need this on Intel :p.
You probably don't need this on Intel :p

On Intel MacBooks, you can control battery charging in a much, much easier way, simply setting the `BCLM` key in Apple SMC to the limit you need, and you are all done. There are many tools available. For example, you can use [smc-command](https://github.com/hholtmann/smcFanControl/tree/master/smc-command) to set SMC keys.
On Intel MacBooks, you can control battery charging in a much, much easier way, simply setting the `BCLM` key in Apple SMC to the limit you need, and you are all done. There are many tools available. For example, you can use [smc-command](https://github.com/hholtmann/smcFanControl/tree/master/smc-command) to set SMC keys. Of course, you will lose some advanced features like upper and lower limit.

However, on Apple Silicon, the way how charging is controlled changed. There is no such key. Therefore, we have to use a much more complicated way to achieve the same goal, and handle more edge cases, hence `batt`.

### Will there be an Intel version?

Probably not. `batt` was made Apple-Silicon-only after some early development. I have tested batt on Intel during development (you can probably find some traces from the code :). Even though some features in batt are known to work on Intel, some are not. Development and testing on Intel requires additional effort, especially those feature that are not working. Considering the fact that Intel MacBooks are going to be obsolete in a few years and some similar tools already exist (without some advanced features), I don't think it is worth the effort. If you are interested in developing an Intel version, feel free to raise a PR.

### Why does my MacBook stop charging after I close the lid?

TL,DR; This is intended, and is the default behavior. It is described [here](#disabling-charging-before-sleep). You can turn this feature off by running `sudo batt disable-charging-pre-sleep disable` (not recommended, keep reading).
Expand Down
140 changes: 85 additions & 55 deletions cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -408,57 +408,42 @@ func NewStatusCommand() *cobra.Command {
Use: "status",
Short: "Get the current status of batt",
RunE: func(cmd *cobra.Command, args []string) error {
// Get charging status.
cmd.Println(boldWhite("Charging status:"))

// Get various info first.
ret, err := get("/charging")
if err != nil {
return fmt.Errorf("failed to get charging status: %v", err)
}
charging, err := strconv.ParseBool(ret)
if err != nil {
return err
}

additionalMsg := " (updates can take up to 5 minutes)"
switch ret {
case "true":
cmd.Println(" Allow charging: " + bool2Text(true) + additionalMsg)
cmd.Println(" Your Mac will charge if plugged in.")
case "false":
cmd.Println(" Allow charging: " + bool2Text(false) + additionalMsg)
cmd.Println(" Your Mac will not charge if plugged in.")
default:
cmd.Println(" Charging: unknown")
ret, err = get("/plugged-in")
if err != nil {
return fmt.Errorf("failed to check if you are plugged in: %v", err)
}
pluggedIn, err := strconv.ParseBool(ret)
if err != nil {
return err
}

ret, err = get("/adapter")
if err != nil {
return fmt.Errorf("failed to get power adapter status: %v", err)
}

switch ret {
case "true":
cmd.Println(" Use power adapter: " + bool2Text(true))
cmd.Println(" Your Mac will use power from the wall, if it is plugged in.")
case "false":
cmd.Println(" Use power adapter: " + bool2Text(false))
cmd.Println(" Your Mac will not use power from the wall, even if it is plugged in.")
default:
cmd.Println(" Power adapter: unknown")
adapter, err := strconv.ParseBool(ret)
if err != nil {
return err
}

cmd.Println()

// Get Battery Info.
cmd.Println(boldWhite("Battery status:"))

ret, err = get("/current-charge")
if err != nil {
return fmt.Errorf("failed to get current charge: %v", err)
}
var currentCharge int
currentCharge, err = strconv.Atoi(ret)
currentCharge, err := strconv.Atoi(ret)
if err != nil {
return fmt.Errorf("failed to unmarshal current charge: %v", err)
}
cmd.Printf(" Current charge: %s\n", boldWhite("%d%%", currentCharge))

ret, err = get("/battery-info")
if err != nil {
Expand All @@ -470,6 +455,63 @@ func NewStatusCommand() *cobra.Command {
return fmt.Errorf("failed to unmarshal battery info: %v", err)
}

ret, err = get("/config")
if err != nil {
return fmt.Errorf("failed to get config: %v", err)
}

conf := Config{}
err = json.Unmarshal([]byte(ret), &conf)
if err != nil {
return fmt.Errorf("failed to unmarshal config: %v", err)
}

// Charging status.
cmd.Println(bold("Charging status:"))

additionalMsg := " (status updates can take up to 5 minutes)"
if charging {
cmd.Println(" Allow charging: " + bool2Text(true) + additionalMsg)
cmd.Print(" Your Mac will charge")
if !pluggedIn {
cmd.Print(", but you are not plugged in yet.") // not plugged in but charging is allowed.
} else {
cmd.Print(".") // plugged in and charging is allowed.
}
cmd.Println()
} else if conf.Limit < 100 {
cmd.Println(" Allow charging: " + bool2Text(false) + additionalMsg)
cmd.Print(" Your Mac will not charge")
if pluggedIn {
cmd.Print(" even if you plug in")
}
low := conf.Limit - conf.LowerLimitDelta
if currentCharge >= conf.Limit {
cmd.Print(", because your current charge is above the limit.")
} else if currentCharge < conf.Limit && currentCharge >= low {
cmd.Print(", because your current charge is above the lower limit. Charging will be allowed after current charge drops below the lower limit.")
}
if pluggedIn && currentCharge < low {
cmd.Print(" If no manual intervention is involved, charging should be allowed soon. Wait for a few minutes and come back.")
}
cmd.Println()
}

if adapter {
cmd.Println(" Use power adapter: " + bool2Text(true))
cmd.Println(" Your Mac will use power from the wall (to operate or charge), if it is plugged in.")
} else {
cmd.Println(" Use power adapter: " + bool2Text(false))
cmd.Println(" Your Mac will not use power from the wall (to operate or charge), even if it is plugged in.")
}

cmd.Println()

// Battery Info.
cmd.Println(bold("Battery status:"))

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

state := "not charging"
switch bat.State {
case battery.Charging:
Expand All @@ -479,32 +521,20 @@ func NewStatusCommand() *cobra.Command {
case battery.Full:
state = "full"
}
cmd.Printf(" State: %s\n", boldWhite(state))
cmd.Printf(" Full capacity: %s\n", boldWhite("%.1f Wh", bat.Design/1e3))
cmd.Printf(" charge rate: %s\n", boldWhite("%.1f W", bat.ChargeRate/1e3))
cmd.Printf(" voltage: %s\n", boldWhite("%.2f V", bat.DesignVoltage))
cmd.Printf(" State: %s\n", bold(state))
cmd.Printf(" Full capacity: %s\n", bold("%.1f Wh", bat.Design/1e3))
cmd.Printf(" Charge rate: %s\n", bold("%.1f W", bat.ChargeRate/1e3))
cmd.Printf(" voltage: %s\n", bold("%.2f V", bat.DesignVoltage))

cmd.Println()

// Get Config.
cmd.Println(boldWhite("Batt configuration:"))

ret, err = get("/config")
if err != nil {
return fmt.Errorf("failed to get config: %v", err)
}

conf := Config{}
err = json.Unmarshal([]byte(ret), &conf)
if err != nil {
return fmt.Errorf("failed to unmarshal config: %v", err)
}

// Config.
cmd.Println(bold("Batt configuration:"))
if conf.Limit < 100 {
cmd.Printf(" Upper limit: %s\n", boldWhite("%d%%", conf.Limit))
cmd.Printf(" Lower limit: %s (%d-%d)\n", boldWhite("%d%%", conf.Limit-conf.LowerLimitDelta), conf.Limit, conf.LowerLimitDelta)
cmd.Printf(" Upper limit: %s\n", bold("%d%%", conf.Limit))
cmd.Printf(" Lower limit: %s (%d-%d)\n", bold("%d%%", conf.Limit-conf.LowerLimitDelta), conf.Limit, conf.LowerLimitDelta)
} else {
cmd.Printf(" Charge limit: %s\n", boldWhite("100%% (disabled)"))
cmd.Printf(" Charge limit: %s\n", bold("100%% (disabled)"))
}
cmd.Printf(" Prevent idle-sleep when charging: %s\n", bool2Text(conf.PreventIdleSleep))
cmd.Printf(" Disable charging before sleep if charge limit is enabled: %s\n", bool2Text(conf.DisableChargingPreSleep))
Expand Down Expand Up @@ -613,9 +643,9 @@ func bool2Text(b bool) string {
if b {
return color.New(color.Bold, color.FgGreen).Sprint("✔")
}
return boldWhite("✘")
return bold("✘")
}

func boldWhite(format string, a ...interface{}) string {
return color.New(color.Bold, color.FgWhite).Sprintf(format, a...)
func bold(format string, a ...interface{}) string {
return color.New(color.Bold).Sprintf(format, a...)
}
1 change: 1 addition & 0 deletions daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func setupRoutes() *gin.Engine {
router.GET("/battery-info", getBatteryInfo)
router.PUT("/magsafe-led", setControlMagSafeLED)
router.GET("/current-charge", getCurrentCharge)
router.GET("/plugged-in", getPluggedIn)

return router
}
Expand Down
12 changes: 12 additions & 0 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,15 @@ func getCurrentCharge(c *gin.Context) {

c.IndentedJSON(http.StatusOK, charge)
}

func getPluggedIn(c *gin.Context) {
pluggedIn, err := smcConn.IsPluggedIn()
if err != nil {
logrus.Errorf("getCurrentCharge failed: %v", err)
c.IndentedJSON(http.StatusInternalServerError, err.Error())
_ = c.AbortWithError(http.StatusInternalServerError, err)
return
}

c.IndentedJSON(http.StatusOK, pluggedIn)
}
4 changes: 2 additions & 2 deletions makefiles/targets.mk
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ package: build
tar czf "$(PKG_OUTPUT)" -C "$(DIST)" "$(BIN_BASENAME)" LICENSE;
cd "$(PKG_OUTPUT_DIR)" && sha256sum "$(PKG_FULLNAME)" >> "$(CHECKSUM_FULLNAME)";
echo "# PACKAGE checksum saved to $(PKG_OUTPUT_DIR)/$(CHECKSUM_FULLNAME)"
echo "# PACKAGE linking $(DIST)/$(BIN)-packages-latest <==> $(PKG_OUTPUT_DIR)"
ln -snf "$(BIN)-$(VERSION)/packages" "$(DIST)/$(BIN)-packages-latest"

# INTERNAL: package-<os>_<arch> to build and package for a specific platform
package-%:
Expand All @@ -69,8 +71,6 @@ all-package: $(addprefix package-, $(subst /,_, $(BIN_PLATFORMS)))
cd "$(PKG_OUTPUT_DIR)" && shopt -s nullglob && \
sha256sum *.{tar.gz,zip} > "$(CHECKSUM_FULLNAME)"
echo "# PACKAGE all checksums saved to $(PKG_OUTPUT_DIR)/$(CHECKSUM_FULLNAME)"
echo "# PACKAGE linking $(DIST)/$(BIN)-packages-latest <==> $(PKG_OUTPUT_DIR)s"
ln -snf "$(BIN)-$(VERSION)/packages" "$(DIST)/$(BIN)-packages-latest"

# ===== MISC =====

Expand Down
13 changes: 6 additions & 7 deletions smc/consts_arm64.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package smc

// Various SMC keys for arm64 (Apple Silicon)
const (
MagSafeLedKey = "ACLC"
ACPowerKey = "AC-W"
ChargingKey1 = "CH0B"
ChargingKey2 = "CH0C"
AdapterKey = "CH0I" // CH0K on Intel, if we need it later
BatteryChargeKey = "BUIC"
BatteryChargeKeyIntel = "BBIF" // TODO: separate Intel and Apple keys using go build tags
MagSafeLedKey = "ACLC"
ACPowerKey = "AC-W"
ChargingKey1 = "CH0B"
ChargingKey2 = "CH0C"
AdapterKey = "CH0I" // CH0K on Intel, if we need it later
BatteryChargeKey = "BUIC"
)

0 comments on commit de5eabc

Please sign in to comment.