Created
October 22, 2023 01:16
-
-
Save Miguel-Dorta/31be2264a50a151aa946acfc89595ff5 to your computer and use it in GitHub Desktop.
Simple script to check the battery in your server and notify you / turn it off
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bytes" | |
"encoding/json" | |
"errors" | |
"fmt" | |
"golang.org/x/sys/unix" | |
"io" | |
"net/http" | |
"os" | |
"os/signal" | |
"time" | |
) | |
const ( | |
checkBatteryIntervalSec = 60 | |
telegramApiTimeoutSec = 10 | |
telegramBotChatId = "12345678" | |
telegramBotToken = "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" | |
powerOffAtBatteryLevel = 20 | |
) | |
type batteryStatus struct { | |
level int | |
isPlugged bool | |
} | |
func main() { | |
notifyChan := make(chan batteryStatus, 20) | |
go startChecker(notifyChan) | |
go startNotifier(notifyChan) | |
quit := make(chan os.Signal, 2) | |
signal.Notify(quit, unix.SIGINT, unix.SIGTERM) | |
<-quit | |
os.Exit(0) | |
} | |
func startChecker(notify chan<- batteryStatus) { | |
fAdpOnline, err := os.OpenFile("/sys/class/power_supply/ADP0/online", os.O_RDONLY, os.ModePerm) | |
if err != nil { | |
panic(err) | |
} | |
fBatCapacity, err := os.OpenFile("/sys/class/power_supply/BAT0/capacity", os.O_RDONLY, os.ModePerm) | |
if err != nil { | |
panic(err) | |
} | |
prevLvlRange := (readLevel(fBatCapacity) + 1) / 10 | |
wasPlugged := readIsPlugged(fAdpOnline) | |
for { | |
time.Sleep(time.Second * checkBatteryIntervalSec) | |
batLvl := readLevel(fBatCapacity) | |
batLvlRange := (batLvl + 1) / 10 | |
isPlugged := readIsPlugged(fAdpOnline) | |
if batLvl <= powerOffAtBatteryLevel { | |
_ = unix.Reboot(unix.LINUX_REBOOT_CMD_POWER_OFF) | |
os.Exit(0) | |
} | |
if isPlugged != wasPlugged || batLvlRange < prevLvlRange { | |
fmt.Printf("NOTIFY | Battery Level: % 3d %% | Is Plugged? %s\n", | |
batLvl, ternaryOp(isPlugged, "Yes", "No")) | |
notify <- batteryStatus{ | |
level: batLvl, | |
isPlugged: isPlugged, | |
} | |
} | |
prevLvlRange = batLvlRange | |
wasPlugged = isPlugged | |
} | |
} | |
func startNotifier(queue <-chan batteryStatus) { | |
client := http.Client{Timeout: telegramApiTimeoutSec * time.Second} | |
hostname, err := os.Hostname() | |
if err != nil { | |
hostname = "<unknown>" | |
} | |
for { | |
status := <-queue | |
msg, err := json.Marshal(map[string]string{ | |
"chat_id": telegramBotChatId, | |
"parse_mode": "HTML", | |
"text": composeNotificationText(status, hostname), | |
}) | |
if err != nil { | |
panic(err) | |
} | |
for i := time.Millisecond; ; i *= 5 { | |
if i > 5*time.Minute { | |
i = 5 * time.Minute | |
} | |
time.Sleep(i) | |
resp, err := client.Post( | |
"https://api.telegram.org/bot"+telegramBotToken+"/sendMessage", | |
"application/json", bytes.NewReader(msg)) | |
if err != nil { | |
_, _ = fmt.Fprintln(os.Stderr, "error sending notification:", err) | |
continue | |
} | |
body, err := io.ReadAll(resp.Body) | |
if err != nil { | |
panic(err) | |
} | |
fmt.Println(string(body)) | |
_ = resp.Body.Close() | |
if resp.StatusCode < 200 || resp.StatusCode >= 300 { | |
_, _ = fmt.Fprintln(os.Stderr, "non-ok status returned by telegram api:", resp.Status) | |
continue | |
} | |
break | |
} | |
} | |
} | |
func readLevel(f *os.File) int { | |
buf := make([]byte, 3) | |
bytesRead, err := f.ReadAt(buf, 0) | |
if bytesRead < 1 { | |
err = errors.New("no data found in battery capacity file") | |
} | |
if err != nil && err != io.EOF { | |
panic(err) | |
} | |
return bytesToInt(buf[:bytesRead]) | |
} | |
func readIsPlugged(f *os.File) bool { | |
buf := make([]byte, 1) | |
bytesRead, err := f.ReadAt(buf, 0) | |
if bytesRead < 1 { | |
err = errors.New("no data found in power adapter online file") | |
} | |
if err != nil { | |
panic(err) | |
} | |
return (buf[0] - '0') != 0 | |
} | |
func composeNotificationText(status batteryStatus, hostname string) string { | |
return fmt.Sprintf("⚠️ Battery notification alert ⚠️\n\n"+ | |
"%s <b>Current level:</b> %d %%\n"+ | |
"%s <b>Is plugged?</b> %s\n"+ | |
"🖥 <b>Server:</b> %s\n"+ | |
"🕓 <b>Time:</b> %s", | |
ternaryOp(status.level > 50, "🔋", "\U0001FAAB"), status.level, | |
ternaryOp(status.isPlugged, "⚡️", "🔌"), ternaryOp(status.isPlugged, "Yes", "No"), | |
hostname, | |
time.Now().Format(time.DateTime)) | |
} | |
func ternaryOp(cond bool, a, b any) any { | |
if cond { | |
return a | |
} | |
return b | |
} | |
func bytesToInt(data []byte) (i int) { | |
for _, b := range data { | |
if b < '0' || b > '9' { | |
return | |
} | |
i = (i * 10) + int(b-'0') | |
} | |
return | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment