From 52f451e4c4493135dcd70558d770019168baa17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pailler?= Date: Wed, 17 Feb 2021 10:46:36 +0800 Subject: [PATCH] Add multi-part and encoded SMS support. Add new API endpoints to retrieve all SMS, retrieve/delete nth SMS, and retrieve network infos --- README.md | 191 +++++++++++++++++++++++++++++++++++++---------------- run.py | 69 +++++++++++++------ support.py | 48 ++++++++++++++ 3 files changed, 232 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index c45b5c5..58a752b 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,113 @@ # REST API SMS Gateway using gammu -Simple SMS REST API gateway for sending SMS from gammu supported devices. Gammu supports standard AT commands, which are using most of USB GSM modems. +Simple SMS REST API gateway for sending and receiving SMS from gammu supported devices. Gammu supports standard AT commands, which are using most of USB GSM modems. ![Docker Cloud Build Status](https://img.shields.io/docker/cloud/build/pajikos/sms-gammu-gateway.svg) ![Docker Automated build](https://img.shields.io/docker/automated/pajikos/sms-gammu-gateway.svg) ![GitHub](https://img.shields.io/github/license/pajikos/sms-gammu-gateway.svg) -When you run this application, you can simply send SMS using REST API: -``` -POST http://xxx.xxx.xxx.xxx:5000/sms -Content-Type: application/json -Authorization: Basic admin password -{ - "text": "Hello, how are you?", - "number": "+420xxxxxxxxx" -} -``` -example: -```bash -AUTH=$(echo -ne "admin:password" | base64 --wrap 0) -curl -H 'Content-Type: application/json' -H "Authorization: Basic $AUTH" -X POST --data '{"text":"Hello, how are you?", "number":"+420xxxxxxxxx"}' http://localhost:5000/sms -1 -``` -If you need to customize the smsc number: -``` -curl -H 'Content-Type: application/json' -H "Authorization: Basic $AUTH" -X POST --data '{"text":"Hello, how are you?", "number":"+420xxxxxxxxx","smsc": "+33695000695"}' http://localhost:5000/sms -``` -or you can simply get the current signal strength: -``` -GET http://xxx.xxx.xxx.xxx:5000/signal -``` -and the response: -``` -{ - "SignalStrength": -83, - "SignalPercent": 45, - "BitErrorRate": -1 -} -``` +#### Available REST API endpoints: + +- ##### Send a SMS :lock: + ``` + POST http://xxx.xxx.xxx.xxx:5000/sms + Content-Type: application/json + Authorization: Basic admin password + { + "text": "Hello, how are you?", + "number": "+420xxxxxxxxx" + } + ``` + Example: + ```bash + AUTH=$(echo -ne "admin:password" | base64 --wrap 0) + curl -H 'Content-Type: application/json' -H "Authorization: Basic $AUTH" -X POST --data '{"text":"Hello, how are you?", "number":"+420xxxxxxxxx"}' http://localhost:5000/sms + 1 + ``` + If you need to customize the smsc number: + ```bash + curl -H 'Content-Type: application/json' -H "Authorization: Basic $AUTH" -X POST --data '{"text":"Hello, how are you?", "number":"+420xxxxxxxxx","smsc": "+33695000695"}' http://localhost:5000/sms + ``` +- ##### Retrieve all the SMS stored on the modem/SIM Card :lock: + ``` + GET http://xxx.xxx.xxx.xxx:5000/sms + ``` + ```json + [ + { + "Date": "2021-02-17 15:20:20", + "Number": "+xxxxxxxxxxx", + "State": "UnRead", + "Text": "Hello" + }, + ... + ] + ``` + +- ##### Retrieve {n}th message stored on the modem/SIM Card :lock: + ``` + GET http://xxx.xxx.xxx.xxx:5000/sms/{n} + ``` + ```json + { + "Date": "2021-02-17 15:20:20", + "Number": "+xxxxxxxxxxx", + "State": "UnRead", + "Text": "Hello" + } + ``` + +- ##### Delete {n}th message stored on the modem/SIM Card :lock: + ``` + DELETE http://xxx.xxx.xxx.xxx:5000/sms/{n} + ``` + +- ##### Retrieve 1st message stored on the modem/SIM Card and delete it :lock: + ``` + GET http://xxx.xxx.xxx.xxx:5000/getsms + ``` + ```json + { + "Date": "2021-02-17 15:20:20", + "Number": "+xxxxxxxxxxx", + "State": "UnRead", + "Text": "Hello" + } + ``` + +- ##### Get the current signal strength :unlock: + ``` + GET http://xxx.xxx.xxx.xxx:5000/signal + ``` + ```json + { + "SignalStrength": -83, + "SignalPercent": 45, + "BitErrorRate": -1 + } + ``` + +- ##### Get the current network details :unlock: + ``` + GET http://xxx.xxx.xxx.xxx:5000/network + ``` + ```json + { + "NetworkName": "DiGi", + "State": "RoamingNetwork", + "PacketState": "RoamingNetwork", + "NetworkCode": "502 16", + "CID": "00A18B30", + "PacketCID": "00A18B30", + "GPRS": "Attached", + "PacketLAC": "7987", + "LAC": "7987" + } + ``` + +# Usage + There are two options how to run this REST API SMS Gateway: * Standalone installation * Running in Docker @@ -129,7 +198,7 @@ Try to check [gammu configuration file site](https://wammu.eu/docs/manual/config ## Integration with Home Assistant #### Signal Strength sensor -``` +```yaml - platform: rest resource: http://xxx.xxx.xxx.xxx:5000/signal name: GSM Signal @@ -139,21 +208,21 @@ Try to check [gammu configuration file site](https://wammu.eu/docs/manual/config ``` #### SMS notification -``` -notify: +```yaml +notify: - name: SMS GW platform: rest resource: http://xxx.xxx.xxx.xxx:5000/sms method: POST_JSON authentication: basic - username: admin - password: password + username: !secret sms_gateway_username + password: !secret sms_gateway_password target_param_name: number message_param_name: text ``` #### Using in Automation -``` +```yaml - alias: Alarm Entry Alert - Garage Door trigger: platform: state @@ -170,26 +239,36 @@ notify: target: '+xxxxxxxxxxxx' ``` -#### Receiving SMS +#### Receiving SMS and sending notification -``` +```yaml +sensor: - platform: rest resource: http://127.0.0.1:5000/getsms name: sms scan_interval: 20 - username: admin - password: password - - - platform: template - sensors: - sms_parsed: - friendly_name: "sms_text" - value_template: "{% set sms_state = states('sensor.sms')|from_json %}{{sms_state.Date}}" - attribute_templates: - text: >- - {% set sms_state = states('sensor.sms')|from_json %}{{sms_state.Text}} - number: >- - {% set sms_state = states('sensor.sms')|from_json %}{{sms_state.Number}} - state: >- - {% set sms_state = states('sensor.sms')|from_json %}{{sms_state.State}} + username: !secret sms_gateway_username + password: !secret sms_gateway_password + json_attributes: + - Date + - Number + - Text + - State + +automation sms_automations: + - alias: Notify on received SMS + trigger: + - platform: template + value_template: "{{state_attr('sensor.sms', 'Text') != ''}}" + action: + - service: notify.mobile_app_[DEVICE] + data: + title: SMS from {{ state_attr('sensor.sms', 'Number') }} + message: "{{ state_attr('sensor.sms', 'Text') }}" + data: + sticky: "true" + - service: persistent_notification.create + data: + title: SMS from {{ state_attr('sensor.sms', 'Number') }} + message: "{{ state_attr('sensor.sms', 'Text') }}" ``` diff --git a/run.py b/run.py index 19a481d..019dc23 100644 --- a/run.py +++ b/run.py @@ -1,10 +1,11 @@ import os -from flask import Flask +from flask import Flask, request from flask_httpauth import HTTPBasicAuth from flask_restful import reqparse, Api, Resource, abort -from support import load_user_data, init_state_machine +from support import load_user_data, init_state_machine, retrieveAllSms, deleteSms +from gammu import GSMNetworks pin = os.getenv('PIN', None) ssl = os.getenv('SSL', False) @@ -23,7 +24,6 @@ def verify(username, password): class Sms(Resource): - def __init__(self, sm): self.parser = reqparse.RequestParser() self.parser.add_argument('text') @@ -31,6 +31,12 @@ def __init__(self, sm): self.parser.add_argument('smsc') self.machine = sm + @auth.login_required + def get(self): + allSms = retrieveAllSms(machine) + list(map(lambda sms: sms.pop("Locations"), allSms)) + return allSms + @auth.login_required def post(self): args = self.parser.parse_args() @@ -51,38 +57,61 @@ def __init__(self, sm): def get(self): return machine.GetSignalQuality() -class Getsms(Resource): + +class Network(Resource): def __init__(self, sm): self.machine = sm - @auth.login_required def get(self): - status = machine.GetSMSStatus() - remain = status['SIMUsed'] + status['PhoneUsed'] + status['TemplatesUsed'] + network = machine.GetNetworkInfo() + network["NetworkName"] = GSMNetworks.get(network["NetworkCode"], 'Unknown') + return network - sms_dict = {"Date": "", "Number": "", "State": "", "Text": ""} - try: +class GetSms(Resource): + def __init__(self, sm): + self.machine = sm - sms = machine.GetNextSMS(Start=True, Folder=0) + @auth.login_required + def get(self): + allSms = retrieveAllSms(machine) + sms = {"Date": "", "Number": "", "State": "", "Text": ""} + if len(allSms) > 0: + sms = allSms[0] + deleteSms(machine, sms) + sms.pop("Locations") - except: - return sms_dict + return sms - if len(sms) > 0: - sms_dict["Date"] = str(sms[0]['DateTime']) - sms_dict["Number"] = str(sms[0]['Number']) - sms_dict["State"] = str(sms[0]['State']) - sms_dict["Text"] = str(sms[0]['Text']) +class SmsById(Resource): + def __init__(self, sm): + self.machine = sm + + @auth.login_required + def get(self, id): + allSms = retrieveAllSms(machine) + self.abort_if_id_doesnt_exist(id, allSms) + sms = allSms[id] + sms.pop("Locations") + return sms + + def delete(self, id): + allSms = retrieveAllSms(machine) + self.abort_if_id_doesnt_exist(id, allSms) + deleteSms(machine, allSms[id]) + return '', 204 - machine.DeleteSMS(Folder=0, Location=sms[0]['Location']) + def abort_if_id_doesnt_exist(self, id, allSms): + if id < 0 or id >= len(allSms): + abort(404, message = "Sms with id '{}' not found".format(id)) - return sms_dict api.add_resource(Sms, '/sms', resource_class_args=[machine]) +api.add_resource(SmsById, '/sms/', resource_class_args=[machine]) api.add_resource(Signal, '/signal', resource_class_args=[machine]) -api.add_resource(Getsms, '/getsms', resource_class_args=[machine]) +api.add_resource(Network, '/network', resource_class_args=[machine]) +api.add_resource(GetSms, '/getsms', resource_class_args=[machine]) if __name__ == '__main__': if ssl: diff --git a/support.py b/support.py index ccbe946..fcf8965 100644 --- a/support.py +++ b/support.py @@ -24,3 +24,51 @@ def init_state_machine(pin, filename='gammu.config'): else: sm.EnterSecurityCode('PIN', pin) return sm + + +def retrieveAllSms(machine): + status = machine.GetSMSStatus() + allMultiPartSmsCount = status['SIMUsed'] + status['PhoneUsed'] + status['TemplatesUsed'] + + allMultiPartSms = [] + start = True + + while len(allMultiPartSms) < allMultiPartSmsCount: + if start: + currentMultiPartSms = machine.GetNextSMS(Start = True, Folder = 0) + start = False + else: + currentMultiPartSms = machine.GetNextSMS(Location = currentMultiPartSms[0]['Location'], Folder = 0) + allMultiPartSms.append(currentMultiPartSms) + + allSms = gammu.LinkSMS(allMultiPartSms) + + results = [] + for sms in allSms: + smsPart = sms[0] + + result = { + "Date": str(smsPart['DateTime']), + "Number": smsPart['Number'], + "State": smsPart['State'], + "Locations": [smsPart['Location'] for smsPart in sms], + } + + decodedSms = gammu.DecodeSMS(sms) + if decodedSms == None: + result["Text"] = smsPart['Text'] + else: + text = "" + for entry in decodedSms['Entries']: + if entry['Buffer'] != None: + text += entry['Buffer'] + + result["Text"] = text + + results.append(result) + + return results + + +def deleteSms(machine, sms): + list(map(lambda location: machine.DeleteSMS(Folder=0, Location=location), sms["Locations"]))