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 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]
(print :send-sms body)
(sms.send "+447000123456" body)
"OK")
(sms:send "447000123456" body)
"Sent")
(fn handle [s]
(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))
(fn escape-for-logging [s]
(s:gsub "%c" (fn [x] (.. "{" (string.byte x) "}"))))
(macro test= [expr expected]
`(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]
(write fd s)
(print (.. ">>> " (escape-for-logging s)))
(log-xfer :>>> s)
(nanosleep {:tv_sec 0 :tv_nsec (* 10 1000 1000)}))
(fn chars [i]
@ -29,21 +41,21 @@
(fn unicode->gsm [s]
(s:gsub "." (fn [c] (chars (. gsm-char (string.byte c))))))
;(print (escape-for-logging (unicode-to-gsm "hello")))
;(print (escape-for-logging (unicode-to-gsm "{he@llo}€")))
(test= (unicode->gsm "abc123") "abc123")
(test= (unicode->gsm "abc{123}") "abc\x1b(123\x1b)")
(fn expect [fd pattern fail-pattern]
(let [b (read fd 1024)]
(if (> (# b) 0)
(do
(print (.. "<<< " (escape-for-logging b)))
(if (string.find b pattern)
(do (print "found" pattern) true)
(and fail-pattern (string.find b fail-pattern))
(error (.. "Expected " pattern ", got " (escape-for-logging b)))
(expect fd pattern)))
(if (string.find (log-xfer :<<< b) pattern)
(log-xfer :!!! pattern)
(and fail-pattern (string.find b fail-pattern))
(error (.. "Expected " pattern ", got " (escape-readably b)))
(expect fd pattern fail-pattern))
nil)))
(fn even? [x]
(= (% x 2) 0))
@ -51,8 +63,8 @@
(let [n (if (even? (# number)) number (.. number "F"))]
(n:gsub ".." (fn [s] (.. (s:sub 2 2) (s:sub 1 1))))))
(assert (= (phone-number->hex "447000123456") "440700214365"))
(assert (= (phone-number->hex "85291234567") "5892214365F7"))
(test= (phone-number->hex "85291234567") "5892214365F7")
(test= (phone-number->hex "447785016005") "447758100650")
(fn mask [start end]
(let [width (+ 1 (- end start))]
@ -123,15 +135,25 @@
;; 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]
(tx fd (.. s "\r\n"))
(expect fd "OK" "ERROR"))
(fn send-sms [number body]
(let [fd (fcntl.open "/dev/serial/by-id/usb-HUAWEI_HUAWEI_HiLink-if00-port0" posix.O_RDWR)
(fn send-message [{: fd} number body]
(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)]
(tset termios :cflag (bor termios.cflag CLOCAL CREAD))
(tset termios :lflag (band termios.lflag
@ -140,26 +162,17 @@
(bnot ECHOE)
(bnot ISIG)))
(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))
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
{:send send-message :device device :smsc smsc :fd fd}))
(tx (.. "AT+CMGS=" (string.format "%d" (/ (# pdu) 2)) "\r\n"))
(expect ">" "ERROR")
(tx payload)
(tx "\026\r\n")
(expect "OK")))))
;(send-sms "447000123456" "It is never {easy} to [send] text messages.")
{ :send send-sms }
{ :new new-sender :verbose false }