But... why?
I wanted to be able to alert in problematic situations, such as power loss at home. It has never happened before for longer than minutes, but if it would, it's a nasty surprise: I once managed to accidentally power off my freezer before going away for holiday. I never want to feel that smell ever again. If I don't have power I don't have internet, so I had to come up with something that has backup power and can still alert me.
Europe is small and we have a ridiculous amount of mobile service providers. Before reasonable international roaming prices the only viable option between countries was SMS, so text messages got quite big in Europe.
But there is a deeper reason: text messages are essentially GSM level service status messages and they are small enough to travel through the moment you have any connectivity, including 2G. Rural England has a terrible coverage when it comes to mobile signals and yes, there are places where you're happy to have a plain old GSM connection running. Connecting to web services on GSM is painful, truly, really painful - an experience everyone should try once, especially if you forgot or never tried what is was like with 56K modems -, therefore alerting via SMS is a simple, surprisingly reliable way here. It's also cheap: I never had to pay to receive text messages (it's an outrageous idea) and for 5GBP/month I can send unlimited texts within network.
This setup is a little more complicated than it could be. Originally I ran a gammu-smsd and used shell scripts to communicate with it, but that requires an ssh access from remote processes. This way I can use the MQTT as a central hub, allowing other services to send alerts as well. For example if I ever manage to put a Z-Wave sensor network together, I can alert the smoke alarm, the motion sensors, etc as well with the system.
Get the server
First of all we need a hub: in our case, my home server is a Lenovo ThinkPad T400. In case you need HDMI, get either something newer or a T500, because the T400, the X200 and the X201 doesn't come with digital video port. If you want something small, quiet, really cheap, and you don't care about the video signal, look for and X200; it's cheaper, than a Raspberry Pi with a battery backup and a GSM shield, and you get a full-blown home server for peanuts.
They usually come with either a 3G modem installed already or a 3G option. Unfortunately not all of them so for the modem or for '3G ready' options, otherwise they won't have the necessary antenna cables. Also check the battery life.
I'm running a Debian Stretch on the machine, so the instructions are according to this. I'm not going to cover how to install linux; there are nice tutorials out there for this.
Optional: Linrunner TLP1
TLP is an advanced power management utility that lets you, for example, undervolt your Core2Duo CPU and limit the battery charge/discharge thresholds. This latter is important, it will help your battery to live significantly longer.
apt install tlp
Part of /etc/default/tlp
for battery thresholds:
START_CHARGE_THRESH_BAT0=60
STOP_CHARGE_THRESH_BAT0=80
WARNING: with newer ThinkPads keep the
SATA_LINKPWR_ON_AC
at maximum_performance
and
the SATA_LINKPWR_ON_BAT
on medium_power
. If
you let them go lower, sometime the SATA devices vanishes, which is
really unhealthy. It should look like this:
SATA_LINKPWR_ON_AC=maximum_performance
SATA_LINKPWR_ON_BAT=medium_power
Set up the SIM card and Gammu
Getting a SIM card
In case you're in the UK, I'd recommend getting a giffgaff pre-paid SIM2. It's cheap and it's working.
Setting up Gammu
Gammu is the software that communicates with the modem.
apt install gammu
Create udev
rules:
/etc/udev/rules.d/99-ericsson.rules
# Ericsson F3507g
ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1900", ENV{ID_USB_INTERFACE_NUM}=="01", SYMLINK+="f3507g_modem", GROUP="dialout", MODE="0660"
ATTRS{idVendor}=="0bdb", ATTRS{idProduct}=="1900", ENV{ID_USB_INTERFACE_NUM}=="03", SYMLINK+="f3507g_data", GROUP="dialout", MODE="0660"
and restart udev:
sudo udevadm trigger
Configure Gammu
/etc/gammurc
[gammu]
port = /dev/f3507g_modem
model =
connection = at
synchronizetime = yes
logfile = /var/log/ericssonf3507g
logformat = text
use_locking =
gammuloc =
Initialise the device
Apparently you need to enable the device and the network before you can use it. You need gammu 1.38.1 at least for this to work.
If you don't do this, the modem might refuse to start and start switching between being available on ttyACM0 and ttyACM1.
gammu -c /etc/gammurc setautonetworklogin
gammu -c /etc/gammurc setpower ON
These lines are added to the systemd unit file below, but if you prefer not to use that, you need to take care of this.
Set up Mosquitto MQTT server
MQTT is a lightweight messaging protocol: it has a server (a hub), which collects all the incoming data; publishers pushing the data, and subscribers, reading topics. I already have this as part of my central bus for sensor data publishing, so extending with alert messages seemed trivial.
Install Mosquitto
sudo apt install mosquitto mosquitto-clients
sudo systemctl enable mosquitto
sudo systemctl start mosquitto
Keep in mind that this will load your MQTT server without
authentication and authorization on port 1883
, so
don't ever do this on an internet facing device. For that protect it
with password and maybe with TLS encryption as well.
To test it:
mosquitto_sub -h 127.0.0.1 -p 1883 -u 'your-mqtt-user-if-any' -P 'your-mqtt-password-if-any' -t '#' -v
The #
is to subscribe to everything.
Add an ini for the MQTT clients
This is not part of Mosquitto, but we're going to use it for our publishing and subscribing Python and Bash scripts.
/etc/mqtt.ini
[mqtt]
host = 127.0.01
port = 1883
user = your-mqtt-user-if-any
password = your-mqtt-password-if-any
Glue it together with Python
Required packages
apt install python3 python3-setuptools python3-pip python3-wheel python3-gammu python3-dev
pip3 install paho-mqtt
Service script
/usr/local/bin/mqtt2sms.py
#!/usr/bin/env python3
import paho.mqtt.client as mqtt
import json
import configparser
import gammu
import os
import time
import logging
import sys
class SMSGateway(object):
def __init__(self):
self.sm = gammu.StateMachine()
self.sm.ReadConfig(Filename='/etc/gammurc')
self.sm.Init()
def send(self, text, number):
message = {
'Text': '%s' % text,
'SMSC': {'Location': 1},
'Number': '%s' % number,
}
try:
self.sm.SendSMS(message)
print("sending SMS to %s with text %s" % (number, text), file=sys.stdout)
return True
except Exception as e:
print("SMS sending failed: %s" % e, file=sys.stderr)
return False
class MQTTSMSListener(mqtt.Client):
def on_message(self, mqttc, obj, msg):
try:
data = json.loads(msg.payload.decode("utf-8"))
message = data.get('message', None)
if not message:
print('no message body to send', file=sys.stderr)
return False
number = data.get('number', None)
if not number:
print('no number to send to', file=sys.stderr)
return False
self.sms.send(message, number)
except Exception as e:
print('failed to decode JSON, reason: %s, string: %s' % (e, msg.payload), file=sys.stderr)
def run(self):
self.sms = SMSGateway()
mqttconf = configparser.ConfigParser()
mqttconf.read('/etc/mqtt.ini')
self.username_pw_set(
mqttconf.get('mqtt', 'user'),
mqttconf.get('mqtt', 'password')
)
self.connect(
mqttconf.get('mqtt', 'host'),
mqttconf.getint('mqtt', 'port'),
60
)
self.subscribe("sms")
rc = 0
while rc == 0:
rc = self.loop()
return rc
mqttc = MQTTSMSListener(clean_session=True)
rc = mqttc.run()
systemd unit file
If you have systemd, you can save this as a simple unit file and run the service with it:
/lib/systemd/system/mqtt2sms.service
[Unit]
Description=start Python MQTT 2 SMS gateway
After=network.target
[Service]
Type=simple
ExecStartPre=/usr/bin/gammu -c /etc/gammurc setautonetworklogin
ExecStartPre=/usr/bin/gammu -c /etc/gammurc setpower ON
ExecStart=/usr/local/bin/mqtt2sms.py
[Install]
WantedBy=multi-user.target
Once done, do:
systemctl daemon-reload
systemctl enable mqtt2sms
systemctl start mqtt2sms
And you're good to go. Anything that goes into the topic
sms
, JSON encoded and has a message
and a
number
field will be forwarded as SMS.
Alerting for the local server conditions
These are a few checks that are running on my local home server to poke me if something goes wrong.
Required packages
ThinkPads have a few special packages: these can handle all the extra hardware ThinkPad have.
apt install acpi-call-dkms tp-smapi-dkms
for mod in "tp_smapi" "thinkpad_ec" "thinkpad_acpi"; do
modprobe "$mod"
echo "$mod" >> /etc/modules
done
A simple Bash wrapper for sending messages to MQTT
/usr/local/bin/alert
#!/usr/bin/env bash
message="$1"
number="${2-[your default phone number comes here]}"
function mqttconf {
grep -i "$1" /etc/mqtt.ini | awk '{print $3}'
}
mosquitto_pub -h "$(mqttconf host)" -p "$(mqttconf port)" -u "$(mqttconf user)" -P "$(mqttconf password)" -t "sms" -m "{\"message\": \"$message\", \"number\": \"$number\"}"
ThinkPad specific checks to send alerts
/usr/local/bin/alerts
#!/usr/bin/env bash
function original_to_previous {
original_path="$1"
echo "/tmp/__$(basename "$original_path")"
}
function get_previous_status {
path="$1"
previous_path="$(original_to_previous $path)"
if [ ! -f "$previous_path" ]; then
touch "$previous_path"
fi
cat "$previous_path"
}
function set_previous_status {
path="$1"
previous_path="$(original_to_previous $path)"
status="$2"
echo "$status" > "$previous_path"
}
# ac status
path="/sys/devices/platform/smapi/ac_connected"
curr="$(cat $path)"
prev="$(get_previous_status $path)"
#echo "AC: $curr, previous: $prev"
if [ "$curr" != "$prev" ]; then
if [ "$curr" != "1" ]; then
/usr/local/bin/alert "WARNING: $(hostname -f) running on battery"
else
/usr/local/bin/alert "OK: $(hostname -f) running on AC"
fi
set_previous_status "$path" "$curr"
fi
# battery level
path="/sys/devices/platform/smapi/BAT0/remaining_percent"
curr="$(cat $path)"
prev="$(get_previous_status $path)"
#echo "Battery level: $curr, previous: $prev"
if [ $curr -lt 30 ]; then
if [ "$curr" -lt "$prev" ]; then
/usr/local/bin/alert "WARNING: $(hostname -f) low battery at $curr %"
fi
fi
set_previous_status "$path" "$curr"
# internet connectivity
path="internet_connectivity"
curr="$(nc -z google.com 443 && echo "ok" || echo "failed")"
prev="$(get_previous_status $path)"
#echo "Internet connectivity: $curr, previous: $prev"
if [ "$curr" != "$prev" ]; then
if [ "$curr" == "failed" ]; then
/usr/local/bin/alert "WARNING: $(hostname -f) can't connect to google.com"
else
/usr/local/bin/alert "OK: $(hostname -f) can connect to google.com"
fi
set_previous_status "$path" "$curr"
fi
Run it as a cron job
/etc/cron.d/alerts
* * * * * root /usr/local/bin/alerts
Enjoy your text messages!
(Oh, by the way: this entry was written by Peter Molnar, and originally posted on petermolnar dot net.)