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>
97 lines
3.1 KiB
Python
Executable File
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()
|