modem: add van-sms-send CLI for sending SMS ad hoc
mmcli's inline "number=...,text=..." properties string breaks on plain spaces/quotes/colons in the text — found while testing van-sms-watch. Sidesteps that entirely by passing the message via --messaging-create-sms-with-text=<tempfile> instead. Tested live (multi-word text, punctuation, multi-line stdin input all delivered). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a74257e44b
commit
db22cb50d6
@@ -156,6 +156,9 @@ install_rendered failover/van-modem-watch.service /etc/systemd/system/van-modem-
|
|||||||
# can't be fetched here (see script docstring) — notification-only paging.
|
# can't be fetched here (see script docstring) — notification-only paging.
|
||||||
install -D -m0755 modem/van-sms-watch /usr/local/sbin/van-sms-watch
|
install -D -m0755 modem/van-sms-watch /usr/local/sbin/van-sms-watch
|
||||||
install -D -m0644 modem/van-sms-watch.service /etc/systemd/system/van-sms-watch.service
|
install -D -m0644 modem/van-sms-watch.service /etc/systemd/system/van-sms-watch.service
|
||||||
|
# CLI to send an SMS ad hoc (needs sudo — ModemManager's Messaging actions
|
||||||
|
# are PolicyKit-gated). Not a service, just a tool: sudo van-sms-send <number> <text>
|
||||||
|
install -D -m0755 modem/van-sms-send /usr/local/sbin/van-sms-send
|
||||||
|
|
||||||
echo "== cockpit plugin =="
|
echo "== cockpit plugin =="
|
||||||
install -d /usr/share/cockpit/vanrouter
|
install -d /usr/share/cockpit/vanrouter
|
||||||
|
|||||||
Executable
+96
@@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""van-sms-send — send an SMS through the EC25 modem from the command line.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
van-sms-send <number> <text...>
|
||||||
|
echo "message text" | van-sms-send <number>
|
||||||
|
|
||||||
|
The message is written to a temp file and passed via mmcli's
|
||||||
|
--messaging-create-sms-with-text (rather than inline in the "number=...,
|
||||||
|
text=..." properties string) — mmcli's properties-string parser breaks on
|
||||||
|
plain spaces/quotes/colons in inline text (found the hard way testing
|
||||||
|
van-sms-watch), the file form has none of that trouble.
|
||||||
|
|
||||||
|
Needs root: ModemManager's Messaging/Device.Control D-Bus actions are
|
||||||
|
PolicyKit-gated and unauthorized for a plain user session (read-only
|
||||||
|
actions like listing SMS work unprivileged; creating/sending don't). Run
|
||||||
|
with sudo.
|
||||||
|
|
||||||
|
Stdlib only.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def die(msg):
|
||||||
|
print(f"van-sms-send: {msg}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def run_json(args):
|
||||||
|
"""Run mmcli with -J and return the parsed dict, dying on any failure."""
|
||||||
|
try:
|
||||||
|
out = subprocess.run(["mmcli", "-J", *args], capture_output=True,
|
||||||
|
text=True, timeout=20)
|
||||||
|
except Exception as e:
|
||||||
|
die(f"failed to run mmcli {' '.join(args)}: {e}")
|
||||||
|
if out.returncode != 0:
|
||||||
|
die(f"mmcli {' '.join(args)} failed: {out.stderr.strip() or out.stdout.strip()}")
|
||||||
|
try:
|
||||||
|
return json.loads(out.stdout)
|
||||||
|
except Exception as e:
|
||||||
|
die(f"mmcli {' '.join(args)} gave unparseable JSON ({e}): {out.stdout}")
|
||||||
|
|
||||||
|
|
||||||
|
def run_action(args):
|
||||||
|
"""Run mmcli for an action whose success is exit-code only (no JSON body
|
||||||
|
even with -J, e.g. --send). Dies on failure."""
|
||||||
|
try:
|
||||||
|
out = subprocess.run(["mmcli", *args], capture_output=True, text=True, timeout=20)
|
||||||
|
except Exception as e:
|
||||||
|
die(f"failed to run mmcli {' '.join(args)}: {e}")
|
||||||
|
if out.returncode != 0:
|
||||||
|
die(f"mmcli {' '.join(args)} failed: {out.stderr.strip() or out.stdout.strip()}")
|
||||||
|
|
||||||
|
|
||||||
|
def find_modem():
|
||||||
|
modems = run_json(["-L"]).get("modem-list") or []
|
||||||
|
return modems[0] if modems else None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
die("usage: van-sms-send <number> <text...> (or pipe text on stdin)")
|
||||||
|
number = sys.argv[1]
|
||||||
|
if len(sys.argv) > 2:
|
||||||
|
text = " ".join(sys.argv[2:])
|
||||||
|
elif not sys.stdin.isatty():
|
||||||
|
text = sys.stdin.read().rstrip("\n")
|
||||||
|
else:
|
||||||
|
die("no message text given as an argument or on stdin")
|
||||||
|
if not text:
|
||||||
|
die("message text is empty")
|
||||||
|
|
||||||
|
modem = find_modem()
|
||||||
|
if not modem:
|
||||||
|
die("no modem found (mmcli -L found none)")
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile("w", suffix=".txt") as f:
|
||||||
|
f.write(text)
|
||||||
|
f.flush()
|
||||||
|
created = run_json(["-m", modem, f"--messaging-create-sms=number={number}",
|
||||||
|
f"--messaging-create-sms-with-text={f.name}"])
|
||||||
|
|
||||||
|
sms_path = (created.get("modem") or {}).get("messaging", {}).get("created-sms")
|
||||||
|
if not sms_path:
|
||||||
|
die(f"couldn't find created SMS path in mmcli output: {created}")
|
||||||
|
|
||||||
|
run_action(["-m", modem, "-s", sms_path, "--send"])
|
||||||
|
print(f"sent to {number}: {text}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user