Files
vanlink/modem/van-sms-send
T
Andreas WredeandClaude Sonnet 5 db22cb50d6 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>
2026-08-21 15:40:22 -04:00

97 lines
3.1 KiB
Python
Executable File

#!/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()