Compare commits

...

5 Commits

4 changed files with 84 additions and 40 deletions

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# Grafana SMS alert
Send Grafana alerts via SMS to a mobile phone, using a GSM modem that
understands AT commads, such as the Huawei E3131 broadband USB dongle.
Fancy SaaS alerting services are great, but what if you want to know
that the internet is down?
This runs as a service on `localhost:8201`: once you've started it,
create a "Webhook" type contact point in your Grafana instance
with the url `http://localhost:8201`.
See Grafana [alerticing contact points](https://grafana.com/docs/grafana/latest/alerting/contact-points/) documentation for background
## Installation (Nix)
TBD
## Testing
You can simulate the Grafana webhook invocation check it's operational
with the sample-alert.json in this repo:
curl -v --data @sample-alert.json http://localhost:8201

View File

@ -1,12 +1,18 @@
(local json (require :dkjson)) (local json (require :dkjson))
(local unistd (require :posix.unistd)) (local unistd (require :posix.unistd))
(local sms (require :sms)) (local sms
((. (require :sms) :new)
{
:smsc "+447958879879"
:device "/dev/serial/by-id/usb-HUAWEI_HUAWEI_HiLink-if00-port0"
:verbose true
}))
(fn send-sms [body] (fn send-sms [body]
(print :send-sms body) (print :send-sms body)
(sms.send "+447000123456" body) (sms:send "447000123456" body)
"OK") "Sent")
(fn handle [s] (fn handle [s]
(let [(m pos err) (json.decode s 1 nil)] (let [(m pos err) (json.decode s 1 nil)]

1
sample-alert.json Normal file
View File

@ -0,0 +1 @@
{"receiver":"","status":"firing","alerts":[{"status":"firing","labels":{"alertname":"TestAlert","instance":"Grafana"},"annotations":{"summary":"Notification test"},"startsAt":"2022-09-13T21:14:29.77765882+01:00","endsAt":"0001-01-01T00:00:00Z","generatorURL":"","fingerprint":"57c6d9296de2ad39","silenceURL":"http://garf.telent.net:3002/alerting/silence/new?alertmanager=grafana\u0026matcher=alertname%3DTestAlert\u0026matcher=instance%3DGrafana","dashboardURL":"","panelURL":"","valueString":"[ metric='foo' labels={instance=bar} value=10 ]"}],"groupLabels":{},"commonLabels":{"alertname":"TestAlert","instance":"Grafana"},"commonAnnotations":{"summary":"Notification test"},"externalURL":"http://garf.telent.net:3002/","version":"1","groupKey":"{alertname=\"TestAlert\", instance=\"Grafana\"}2022-09-13 21:14:29.77765882 +0100 BST m=+644632.184951212","truncatedAlerts":0,"orgId":1,"title":"[FIRING:1] (TestAlert Grafana)","state":"alerting","message":"**Firing**\n\nValue: [ metric='foo' labels={instance=bar} value=10 ]\nLabels:\n - alertname = TestAlert\n - instance = Grafana\nAnnotations:\n - summary = Notification test\nSilence: http://garf.telent.net:3002/alerting/silence/new?alertmanager=grafana\u0026matcher=alertname%3DTestAlert\u0026matcher=instance%3DGrafana\n"}

87
sms.fnl
View File

@ -11,12 +11,24 @@
(local gsm-char (require :unicode-to-gsm)) (local gsm-char (require :unicode-to-gsm))
(fn escape-for-logging [s] (macro test= [expr expected]
(s:gsub "%c" (fn [x] (.. "{" (string.byte x) "}")))) `(let [a# ,expr
e# ,expected]
(if (not (= a# e#))
(assert false (.. "expected " ,(view expr) " = " e#
", actual " a#)))))
(fn escape-readably [s]
(s:gsub "%c" (fn [x] (string.format "\\u{%.3x}" (string.byte x)))))
(fn log-xfer [direction message]
(when _G.modem-spew
(_G.modem-spew:write direction " " (escape-readably message) "\n"))
message)
(fn tx [fd s] (fn tx [fd s]
(write fd s) (write fd s)
(print (.. ">>> " (escape-for-logging s))) (log-xfer :>>> s)
(nanosleep {:tv_sec 0 :tv_nsec (* 10 1000 1000)})) (nanosleep {:tv_sec 0 :tv_nsec (* 10 1000 1000)}))
(fn chars [i] (fn chars [i]
@ -29,21 +41,21 @@
(fn unicode->gsm [s] (fn unicode->gsm [s]
(s:gsub "." (fn [c] (chars (. gsm-char (string.byte c)))))) (s:gsub "." (fn [c] (chars (. gsm-char (string.byte c))))))
;(print (escape-for-logging (unicode-to-gsm "hello"))) (test= (unicode->gsm "abc123") "abc123")
;(print (escape-for-logging (unicode-to-gsm "{he@llo}€"))) (test= (unicode->gsm "abc{123}") "abc\x1b(123\x1b)")
(fn expect [fd pattern fail-pattern] (fn expect [fd pattern fail-pattern]
(let [b (read fd 1024)] (let [b (read fd 1024)]
(if (> (# b) 0) (if (> (# b) 0)
(do (if (string.find (log-xfer :<<< b) pattern)
(print (.. "<<< " (escape-for-logging b))) (log-xfer :!!! pattern)
(if (string.find b pattern) (and fail-pattern (string.find b fail-pattern))
(do (print "found" pattern) true) (error (.. "Expected " pattern ", got " (escape-readably b)))
(and fail-pattern (string.find b fail-pattern)) (expect fd pattern fail-pattern))
(error (.. "Expected " pattern ", got " (escape-for-logging b)))
(expect fd pattern)))
nil))) nil)))
(fn even? [x] (fn even? [x]
(= (% x 2) 0)) (= (% x 2) 0))
@ -51,8 +63,8 @@
(let [n (if (even? (# number)) number (.. number "F"))] (let [n (if (even? (# number)) number (.. number "F"))]
(n:gsub ".." (fn [s] (.. (s:sub 2 2) (s:sub 1 1)))))) (n:gsub ".." (fn [s] (.. (s:sub 2 2) (s:sub 1 1))))))
(assert (= (phone-number->hex "447000123456") "440700214365")) (test= (phone-number->hex "85291234567") "5892214365F7")
(assert (= (phone-number->hex "85291234567") "5892214365F7")) (test= (phone-number->hex "447785016005") "447758100650")
(fn mask [start end] (fn mask [start end]
(let [width (+ 1 (- end start))] (let [width (+ 1 (- end start))]
@ -123,15 +135,25 @@
;; per worked example at https://www.developershome.com/sms/cmgsCommand4.asp ;; per worked example at https://www.developershome.com/sms/cmgsCommand4.asp
(assert (= (message->pdu "85291234567" "It is easy to send text messages.") "01000B915892214365F7000021493A283D0795C3F33C88FE06CDCB6E32885EC6D341EDF27C1E3E97E72E")) (test= (message->pdu "85291234567" "It is easy to send text messages.") "01000B915892214365F7000021493A283D0795C3F33C88FE06CDCB6E32885EC6D341EDF27C1E3E97E72E")
(fn command [fd s] (fn command [fd s]
(tx fd (.. s "\r\n")) (tx fd (.. s "\r\n"))
(expect fd "OK" "ERROR")) (expect fd "OK" "ERROR"))
(fn send-sms [number body] (fn send-message [{: fd} number body]
(let [fd (fcntl.open "/dev/serial/by-id/usb-HUAWEI_HUAWEI_HiLink-if00-port0" posix.O_RDWR) (let [pdu (message->pdu number (unicode->gsm body))
payload (.. "00" pdu)]
(doto fd
(tx (.. "AT+CMGS=" (string.format "%d" (/ (# pdu) 2)) "\r\n"))
(expect ">" "ERROR")
(tx payload)
(tx "\026\r\n")
(expect "OK"))))
(fn new-sender [{: device : smsc : verbose}]
(let [fd (fcntl.open device posix.O_RDWR)
termios (tcgetattr fd)] termios (tcgetattr fd)]
(tset termios :cflag (bor termios.cflag CLOCAL CREAD)) (tset termios :cflag (bor termios.cflag CLOCAL CREAD))
(tset termios :lflag (band termios.lflag (tset termios :lflag (band termios.lflag
@ -140,26 +162,17 @@
(bnot ECHOE) (bnot ECHOE)
(bnot ISIG))) (bnot ISIG)))
(tset termios :oflag (band termios.oflag ( bnot OPOST))) (tset termios :oflag (band termios.oflag ( bnot OPOST)))
(tcsetattr fd 0 termios) (doto fd
(tcsetattr 0 termios)
(tcdrain)
(tcdrain fd) (command "AT")
(command "AT&F") ; revert to defaults
(command "ATE0") ; disable command echo
(command "AT+CMEE=1") ;print CME errors
(command (.. "AT+CSCA=\"" smsc "\",145\r\n")) ;set SMSC
(command "AT+CMGF=0")) ;SMS PDU mode
(let [pdu (message->pdu number (unicode->gsm body)) {:send send-message :device device :smsc smsc :fd fd}))
payload (.. "00" pdu)]
(doto fd
(command "AT")
(command "AT&F") ; revert to defaults
(command "ATE0") ; disable command echo
(command "AT+CMEE=1") ;print CME errors
(command "AT+CSCA=\"+447958879879\",145\r\n") ;set SMSC
(command "AT+CMGF=0") ;SMS PDU mode
(tx (.. "AT+CMGS=" (string.format "%d" (/ (# pdu) 2)) "\r\n")) { :new new-sender :verbose false }
(expect ">" "ERROR")
(tx payload)
(tx "\026\r\n")
(expect "OK")))))
;(send-sms "447000123456" "It is never {easy} to [send] text messages.")
{ :send send-sms }