battery: van-battery daemon (low-charge Pushover alerts + safe shutdown)
While on battery (AC0/online=0), sends escalating Pushover alerts at 25/20/15% and at 10% sends a final alert then `systemctl poweroff` (after shutdown_grace so the alert flushes first). Edge-triggered per discharge episode; re-arms when mains returns; plug-out already-low collapses to a single alert (no burst). Pushover token+user live only in /etc/van-battery/pushover.json (0600), seeded from pushover.json.example on first deploy and never committed. Missing/ placeholder creds disable sending but NOT the shutdown — running flat must always power down safely. Publishes /run/van-battery/state.json. Stdlib only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ef6c1ce182
commit
b75b5b16c7
@@ -40,6 +40,7 @@ This directory is the source of truth. The live system files live under `/etc`,
|
|||||||
| NAT + forwarding | **nftables** + sysctl |
|
| NAT + forwarding | **nftables** + sysctl |
|
||||||
| WAN health + failover | **van-failover** daemon |
|
| WAN health + failover | **van-failover** daemon |
|
||||||
| Temperature monitor / alert / log | **van-thermal** daemon |
|
| Temperature monitor / alert / log | **van-thermal** daemon |
|
||||||
|
| Battery monitor / low-charge alert + shutdown | **van-battery** daemon |
|
||||||
| ZeroTier DNS → resolved | **zerotier-systemd-manager** + systemd-networkd |
|
| ZeroTier DNS → resolved | **zerotier-systemd-manager** + systemd-networkd |
|
||||||
| Web UI | **Cockpit** + `vanrouter` plugin |
|
| Web UI | **Cockpit** + `vanrouter` plugin |
|
||||||
|
|
||||||
@@ -98,6 +99,10 @@ This directory is the source of truth. The live system files live under `/etc`,
|
|||||||
| `van-thermal` | `/usr/local/sbin/van-thermal` | temperature daemon (Python): publishes state, alerts, logs history |
|
| `van-thermal` | `/usr/local/sbin/van-thermal` | temperature daemon (Python): publishes state, alerts, logs history |
|
||||||
| `thermal-config.json` | `/etc/van-thermal/config.json` | sensors + warn/crit thresholds + sample/log intervals |
|
| `thermal-config.json` | `/etc/van-thermal/config.json` | sensors + warn/crit thresholds + sample/log intervals |
|
||||||
| `van-thermal.service` | `/etc/systemd/system/van-thermal.service` | `Restart=always` |
|
| `van-thermal.service` | `/etc/systemd/system/van-thermal.service` | `Restart=always` |
|
||||||
|
| `van-battery` | `/usr/local/sbin/van-battery` | battery daemon (Python): Pushover low-charge alerts + safe shutdown |
|
||||||
|
| `battery-config.json` | `/etc/van-battery/config.json` | warn levels, shutdown level, poll interval, paths |
|
||||||
|
| `van-battery.service` | `/etc/systemd/system/van-battery.service` | `Restart=always` |
|
||||||
|
| `pushover.json.example` | → `/etc/van-battery/pushover.json` (seeded if absent) | Pushover token+user **template**; real file is 0600, **not** in the repo |
|
||||||
|
|
||||||
`deploy.sh` additionally **masks** `sleep.target suspend.target hibernate.target hybrid-sleep.target`
|
`deploy.sh` additionally **masks** `sleep.target suspend.target hibernate.target hybrid-sleep.target`
|
||||||
(no repo file — symlinks to `/dev/null` under `/etc/systemd/system/`) so nothing else can suspend either.
|
(no repo file — symlinks to `/dev/null` under `/etc/systemd/system/`) so nothing else can suspend either.
|
||||||
@@ -135,6 +140,14 @@ This directory is the source of truth. The live system files live under `/etc`,
|
|||||||
- **Tuning**: edit `/etc/van-thermal/config.json` (thresholds, intervals, sensor list), then `systemctl restart van-thermal`. Defaults: CPU warn 80 / crit 95 °C (silicon crit is 100), NVMe warn 65 / crit 70 °C (drive crit ~71).
|
- **Tuning**: edit `/etc/van-thermal/config.json` (thresholds, intervals, sensor list), then `systemctl restart van-thermal`. Defaults: CPU warn 80 / crit 95 °C (silicon crit is 100), NVMe warn 65 / crit 70 °C (drive crit ~71).
|
||||||
- Status: `systemctl status van-thermal` or `cat /run/van-thermal/state.json`.
|
- Status: `systemctl status van-thermal` or `cat /run/van-thermal/state.json`.
|
||||||
|
|
||||||
|
### Battery monitor (`van-battery`)
|
||||||
|
- Watches mains vs battery via `/sys/class/power_supply/AC0/online` (0 = on battery) and charge via `BAT0/capacity`. Both resolve by `type` (Mains/Battery) if those names ever differ.
|
||||||
|
- **Only while on battery**, it sends escalating **Pushover** alerts at **25 / 20 / 15 %**, and at **10 %** sends a final alert and runs `systemctl poweroff` (after `shutdown_grace`, default 8 s, so the alert flushes first).
|
||||||
|
- Edge-triggered per discharge episode: each severity fires once; the sequence **re-arms when mains returns**. Unplugging already below a warn level fires a single alert for the current severity (no burst), then shutdown at 10 %.
|
||||||
|
- **Credentials** live in `/etc/van-battery/pushover.json` (mode 0600), seeded from `pushover.json.example` on first deploy and **never committed**. Missing/placeholder creds disable *sending* but **not** the shutdown — running flat must always power down safely (the skip is logged to the journal).
|
||||||
|
- Tune in `/etc/van-battery/config.json` (`warn_levels`, `shutdown_level`, `poll_interval`), then `systemctl restart van-battery`.
|
||||||
|
- Status: `systemctl status van-battery`, `journalctl -u van-battery -f`, or `cat /run/van-battery/state.json`.
|
||||||
|
|
||||||
### Adding the 4G/5G modem
|
### Adding the 4G/5G modem
|
||||||
1. Plug the USB modem in. ModemManager + the existing `Koodo` gsm NM connection (autoconnect) bring it up.
|
1. Plug the USB modem in. ModemManager + the existing `Koodo` gsm NM connection (autoconnect) bring it up.
|
||||||
2. It auto-joins as the `cellular` WAN at metric 300 (last resort). Nothing else to configure.
|
2. It auto-joins as the `cellular` WAN at metric 300 (last resort). Nothing else to configure.
|
||||||
@@ -171,7 +184,7 @@ cd ~/vanlink
|
|||||||
sudo ./deploy.sh # copies all files to their system locations, reloads + enables services
|
sudo ./deploy.sh # copies all files to their system locations, reloads + enables services
|
||||||
```
|
```
|
||||||
|
|
||||||
Two things `deploy.sh` does **not** do (one-time, manual):
|
Three things `deploy.sh` does **not** do (one-time, manual):
|
||||||
|
|
||||||
1. **zerotier-systemd-manager binary** (v0.4.0, hand-installed — not in the ZeroTier apt repo):
|
1. **zerotier-systemd-manager binary** (v0.4.0, hand-installed — not in the ZeroTier apt repo):
|
||||||
```bash
|
```bash
|
||||||
@@ -180,6 +193,7 @@ Two things `deploy.sh` does **not** do (one-time, manual):
|
|||||||
sudo zerotier-cli set d3ecf5726d041b2a allowDNS=1
|
sudo zerotier-cli set d3ecf5726d041b2a allowDNS=1
|
||||||
```
|
```
|
||||||
2. **hostapd unmask** (Ubuntu ships it masked): `sudo systemctl unmask hostapd`.
|
2. **hostapd unmask** (Ubuntu ships it masked): `sudo systemctl unmask hostapd`.
|
||||||
|
3. **Pushover credentials** for `van-battery` — deploy seeds `/etc/van-battery/pushover.json` (0600) with placeholders; fill in your app token + user key, then `sudo systemctl restart van-battery`. Until then low-battery alerts are skipped (logged), but the 10 % auto-shutdown still works.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,17 @@ install -D -m0755 power/van-thermal /usr/local/sbin/van-thermal
|
|||||||
install -D -m0644 power/thermal-config.json /etc/van-thermal/config.json
|
install -D -m0644 power/thermal-config.json /etc/van-thermal/config.json
|
||||||
install -D -m0644 power/van-thermal.service /etc/systemd/system/van-thermal.service
|
install -D -m0644 power/van-thermal.service /etc/systemd/system/van-thermal.service
|
||||||
|
|
||||||
|
echo "== battery monitor =="
|
||||||
|
install -D -m0755 power/van-battery /usr/local/sbin/van-battery
|
||||||
|
install -D -m0644 power/battery-config.json /etc/van-battery/config.json
|
||||||
|
install -D -m0644 power/van-battery.service /etc/systemd/system/van-battery.service
|
||||||
|
# Pushover secrets live only on the system (0600), never in the repo. Seed from the
|
||||||
|
# template on first deploy; never clobber a filled-in file on later deploys.
|
||||||
|
if [ ! -f /etc/van-battery/pushover.json ]; then
|
||||||
|
install -D -m0600 power/pushover.json.example /etc/van-battery/pushover.json
|
||||||
|
echo " -> seeded /etc/van-battery/pushover.json (EDIT IT: add Pushover token + user)"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "== power / never-sleep =="
|
echo "== power / never-sleep =="
|
||||||
install -D -m0644 power/10-vanlink-nolid.conf /etc/systemd/logind.conf.d/10-vanlink-nolid.conf
|
install -D -m0644 power/10-vanlink-nolid.conf /etc/systemd/logind.conf.d/10-vanlink-nolid.conf
|
||||||
# Belt-and-suspenders: a router must never suspend from idle, GUI, or a stray `systemctl suspend`.
|
# Belt-and-suspenders: a router must never suspend from idle, GUI, or a stray `systemctl suspend`.
|
||||||
@@ -53,8 +64,8 @@ systemctl mask systemd-networkd-wait-online.service >/dev/null 2>&1 || true
|
|||||||
# pick up the lid drop-in (re-execs logind; does NOT drop the network)
|
# pick up the lid drop-in (re-execs logind; does NOT drop the network)
|
||||||
systemctl restart systemd-logind >/dev/null 2>&1 || true
|
systemctl restart systemd-logind >/dev/null 2>&1 || true
|
||||||
systemctl unmask hostapd >/dev/null 2>&1 || true
|
systemctl unmask hostapd >/dev/null 2>&1 || true
|
||||||
systemctl enable regdomain.service hostapd van-ap-dnsmasq nftables systemd-networkd van-failover van-thermal >/dev/null 2>&1 || true
|
systemctl enable regdomain.service hostapd van-ap-dnsmasq nftables systemd-networkd van-failover van-thermal van-battery >/dev/null 2>&1 || true
|
||||||
systemctl restart van-thermal
|
systemctl restart van-thermal van-battery
|
||||||
# restart in dependency order; AP iface IP first, then hostapd/dnsmasq, then NAT/failover
|
# restart in dependency order; AP iface IP first, then hostapd/dnsmasq, then NAT/failover
|
||||||
systemctl restart systemd-networkd
|
systemctl restart systemd-networkd
|
||||||
systemctl restart hostapd van-ap-dnsmasq nftables van-failover
|
systemctl restart hostapd van-ap-dnsmasq nftables van-failover
|
||||||
@@ -65,4 +76,5 @@ echo "Deployed. Verify:"
|
|||||||
echo " iw dev wlxc83a35a4ee55 info | grep -E 'ssid|channel|width'"
|
echo " iw dev wlxc83a35a4ee55 info | grep -E 'ssid|channel|width'"
|
||||||
echo " cat /run/van-failover/state.json"
|
echo " cat /run/van-failover/state.json"
|
||||||
echo " cat /run/van-thermal/state.json # CPU + NVMe temps"
|
echo " cat /run/van-thermal/state.json # CPU + NVMe temps"
|
||||||
|
echo " cat /run/van-battery/state.json # mains/battery + charge %"
|
||||||
echo "Manual one-time steps (see README §4): zerotier-systemd-manager binary + 'zerotier-cli set <nwid> allowDNS=1'."
|
echo "Manual one-time steps (see README §4): zerotier-systemd-manager binary + 'zerotier-cli set <nwid> allowDNS=1'."
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"poll_interval": 30,
|
||||||
|
"ac_path": "/sys/class/power_supply/AC0/online",
|
||||||
|
"battery_path": "/sys/class/power_supply/BAT0",
|
||||||
|
"warn_levels": [25, 20, 15],
|
||||||
|
"shutdown_level": 10,
|
||||||
|
"shutdown_grace": 8,
|
||||||
|
"credentials_path": "/etc/van-battery/pushover.json"
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"token": "REPLACE_ME",
|
||||||
|
"user": "REPLACE_ME"
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""van-battery — battery / mains monitor for the campervan router (wayback).
|
||||||
|
|
||||||
|
While running **off mains** (AC offline, i.e. on battery) it sends escalating
|
||||||
|
Pushover alerts as the charge drops past each warn level, and at the shutdown
|
||||||
|
level it sends a final alert and powers the machine off cleanly.
|
||||||
|
|
||||||
|
Alerts are edge-triggered per discharge episode: each severity fires once, and
|
||||||
|
the whole sequence re-arms when mains power returns. Plug-out already below a
|
||||||
|
warn level fires a single alert for the current severity, not a burst.
|
||||||
|
|
||||||
|
Pushover credentials live in a separate 0600 secrets file (see credentials_path),
|
||||||
|
never in this repo. Missing/placeholder creds disable sending but NOT the
|
||||||
|
shutdown — running flat must always power down safely. Stdlib only.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
CONFIG_PATH = os.environ.get("VAN_BATTERY_CONFIG", "/etc/van-battery/config.json")
|
||||||
|
STATE_DIR = Path("/run/van-battery")
|
||||||
|
STATE_PATH = STATE_DIR / "state.json"
|
||||||
|
PSY = Path("/sys/class/power_supply")
|
||||||
|
HOST = socket.gethostname()
|
||||||
|
|
||||||
|
DEFAULTS = {
|
||||||
|
"poll_interval": 30, # seconds between reads (battery moves slowly)
|
||||||
|
"ac_path": "/sys/class/power_supply/AC0/online",
|
||||||
|
"battery_path": "/sys/class/power_supply/BAT0",
|
||||||
|
"warn_levels": [25, 20, 15], # Pushover alert only
|
||||||
|
"shutdown_level": 10, # Pushover alert + poweroff
|
||||||
|
"shutdown_grace": 8, # seconds to let the alert flush before poweroff
|
||||||
|
"credentials_path": "/etc/van-battery/pushover.json",
|
||||||
|
}
|
||||||
|
|
||||||
|
PLACEHOLDERS = {"", "REPLACE_ME", "your-token-here", "your-user-key-here"}
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg, level="info"):
|
||||||
|
pri = {"info": "<6>", "warn": "<4>", "crit": "<2>"}.get(level, "<6>")
|
||||||
|
print(pri + msg, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
cfg = dict(DEFAULTS)
|
||||||
|
try:
|
||||||
|
with open(CONFIG_PATH) as f:
|
||||||
|
cfg.update(json.load(f))
|
||||||
|
except FileNotFoundError:
|
||||||
|
log(f"config {CONFIG_PATH} not found, using built-in defaults")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"config {CONFIG_PATH} unreadable ({e}), using defaults", "warn")
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
def _by_type(kind):
|
||||||
|
"""Find a power_supply dir by its `type` (Mains / Battery) — fallback when the
|
||||||
|
configured AC0/BAT0 name isn't present on this machine."""
|
||||||
|
for d in sorted(PSY.glob("*")):
|
||||||
|
try:
|
||||||
|
if (d / "type").read_text().strip() == kind:
|
||||||
|
return d
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def read_on_battery(cfg):
|
||||||
|
"""True if running on battery (mains absent), False if on mains, None if unknown."""
|
||||||
|
p = Path(cfg["ac_path"])
|
||||||
|
if not p.exists():
|
||||||
|
d = _by_type("Mains")
|
||||||
|
p = (d / "online") if d else None
|
||||||
|
if not p or not p.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return p.read_text().strip() == "0"
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def read_capacity(cfg):
|
||||||
|
"""Battery charge percentage (int), or None."""
|
||||||
|
d = Path(cfg["battery_path"])
|
||||||
|
if not (d / "capacity").exists():
|
||||||
|
d = _by_type("Battery") or d
|
||||||
|
try:
|
||||||
|
return int((d / "capacity").read_text().strip())
|
||||||
|
except (OSError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def load_creds(cfg):
|
||||||
|
try:
|
||||||
|
c = json.loads(Path(cfg["credentials_path"]).read_text())
|
||||||
|
token, user = str(c.get("token", "")).strip(), str(c.get("user", "")).strip()
|
||||||
|
if token in PLACEHOLDERS or user in PLACEHOLDERS:
|
||||||
|
return None
|
||||||
|
return token, user
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
log(f"credentials {cfg['credentials_path']} unreadable ({e})", "warn")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def pushover(cfg, title, message, priority=0):
|
||||||
|
creds = load_creds(cfg)
|
||||||
|
if not creds:
|
||||||
|
log(f"pushover skipped (no credentials): {title} — {message}", "warn")
|
||||||
|
return False
|
||||||
|
token, user = creds
|
||||||
|
data = urllib.parse.urlencode({
|
||||||
|
"token": token, "user": user, "title": title,
|
||||||
|
"message": message, "priority": priority,
|
||||||
|
}).encode()
|
||||||
|
req = urllib.request.Request("https://api.pushover.net/1/messages.json", data=data)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||||
|
ok = resp.status == 200
|
||||||
|
if not ok:
|
||||||
|
log(f"pushover HTTP {resp.status}", "warn")
|
||||||
|
return ok
|
||||||
|
except Exception as e:
|
||||||
|
log(f"pushover send failed: {e}", "warn")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def severity(cap, levels):
|
||||||
|
"""Most-severe (lowest) threshold the capacity has reached, or None if above all.
|
||||||
|
levels: thresholds sorted ascending. cap=12, levels=[10,15,20,25] -> 15."""
|
||||||
|
reached = [t for t in levels if cap <= t]
|
||||||
|
return min(reached) if reached else None
|
||||||
|
|
||||||
|
|
||||||
|
def poweroff(cfg):
|
||||||
|
log("shutdown level reached — powering off", "crit")
|
||||||
|
time.sleep(cfg["shutdown_grace"]) # give the Pushover POST time to land first
|
||||||
|
try:
|
||||||
|
subprocess.run(["systemctl", "poweroff"], check=False)
|
||||||
|
except Exception as e:
|
||||||
|
log(f"poweroff failed: {e}", "crit")
|
||||||
|
|
||||||
|
|
||||||
|
def write_state(on_batt, cap, armed):
|
||||||
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
payload = {
|
||||||
|
"updated": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
||||||
|
"on_battery": on_batt,
|
||||||
|
"capacity": cap,
|
||||||
|
"alerted_below": armed, # lowest level alerted this discharge episode, or null
|
||||||
|
}
|
||||||
|
tmp = STATE_PATH.with_suffix(".tmp")
|
||||||
|
tmp.write_text(json.dumps(payload))
|
||||||
|
tmp.replace(STATE_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
cfg = load_config()
|
||||||
|
levels = sorted(cfg["warn_levels"] + [cfg["shutdown_level"]])
|
||||||
|
shutdown_level = cfg["shutdown_level"]
|
||||||
|
last_alerted = None # lowest threshold alerted in the current discharge episode
|
||||||
|
shutdown_issued = False
|
||||||
|
prev_on_batt = None
|
||||||
|
log(f"van-battery up: poll {cfg['poll_interval']}s, warn {cfg['warn_levels']}, "
|
||||||
|
f"shutdown {shutdown_level}%")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
on_batt = read_on_battery(cfg)
|
||||||
|
cap = read_capacity(cfg)
|
||||||
|
|
||||||
|
if on_batt is False:
|
||||||
|
# On mains: re-arm the whole sequence for the next discharge episode.
|
||||||
|
if prev_on_batt:
|
||||||
|
log("mains restored — alerts re-armed")
|
||||||
|
last_alerted = None
|
||||||
|
shutdown_issued = False
|
||||||
|
elif on_batt is True and cap is not None:
|
||||||
|
if prev_on_batt is False:
|
||||||
|
log(f"mains lost — running on battery at {cap}%", "warn")
|
||||||
|
sev = severity(cap, levels)
|
||||||
|
if sev is not None and (last_alerted is None or sev < last_alerted):
|
||||||
|
last_alerted = sev
|
||||||
|
is_shutdown = sev <= shutdown_level
|
||||||
|
if is_shutdown:
|
||||||
|
pushover(cfg, f"⚠ {HOST}: battery {cap}% — shutting down",
|
||||||
|
f"On battery at {cap}% (≤{shutdown_level}%). Powering off now to "
|
||||||
|
f"protect the system.", priority=1)
|
||||||
|
if not shutdown_issued:
|
||||||
|
shutdown_issued = True
|
||||||
|
poweroff(cfg)
|
||||||
|
else:
|
||||||
|
log(f"battery {cap}% on battery — alerting (level {sev})", "warn")
|
||||||
|
pushover(cfg, f"{HOST}: battery {cap}%",
|
||||||
|
f"Running on battery, charge down to {cap}% (alert at {sev}%). "
|
||||||
|
f"Shutdown at {shutdown_level}%.")
|
||||||
|
|
||||||
|
prev_on_batt = on_batt
|
||||||
|
try:
|
||||||
|
write_state(on_batt, cap, last_alerted)
|
||||||
|
except OSError as e:
|
||||||
|
log(f"state write failed: {e}", "warn")
|
||||||
|
time.sleep(cfg["poll_interval"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.exit(0)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Battery monitor + low-charge Pushover alerts / safe shutdown for the campervan router
|
||||||
|
After=local-fs.target network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/sbin/van-battery
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user