├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── changelog-drafter.yml ├── dependabot.yml └── workflows │ ├── changelog.yml │ ├── publish.yml │ └── testlint.yml ├── .gitignore ├── .pylintrc ├── AUTHORS.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Exceptions ├── HologramError.py └── __init__.py ├── Hologram ├── Api │ ├── Api.py │ └── __init__.py ├── Authentication │ ├── AES │ │ └── AESCipher.py │ ├── Authentication.py │ ├── CSRPSKAuthentication.py │ ├── HologramAuthentication.py │ └── __init__.py ├── Cloud.py ├── CustomCloud.py ├── Event │ ├── Event.py │ └── __init__.py ├── HologramCloud.py ├── Network │ ├── BLE.py │ ├── Cellular.py │ ├── Ethernet.py │ ├── Modem │ │ ├── BG96.py │ │ ├── DriverLoader.py │ │ ├── E303.py │ │ ├── E372.py │ │ ├── EC21.py │ │ ├── IModem.py │ │ ├── MS2131.py │ │ ├── MockModem.py │ │ ├── Modem.py │ │ ├── ModemMode │ │ │ ├── IPPP.py │ │ │ ├── MockPPP.py │ │ │ ├── ModemMode.py │ │ │ ├── PPP.py │ │ │ ├── __init__.py │ │ │ └── pppd.py │ │ ├── Nova.py │ │ ├── NovaM.py │ │ ├── Nova_U201.py │ │ ├── Quectel.py │ │ ├── __init__.py │ │ └── chatscripts │ │ │ ├── __init__.py │ │ │ └── default-script │ ├── Network.py │ ├── NetworkManager.py │ ├── Route.py │ ├── Wifi.py │ └── __init__.py └── __init__.py ├── ISSUE_TEMPLATE.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── UtilClasses ├── UtilClasses.py └── __init__.py ├── credentials-empty.json ├── examples ├── example-average-max-signal-strength.py ├── example-cellular-send.py ├── example-hardware-auth.py ├── example-periodic-send.py ├── example-receive-sms-at-commands.py ├── example-send-msg.py ├── example-serial-interface.py └── example-sms-at-commands.py ├── install.sh ├── requirements-test.txt ├── requirements.txt ├── scripts ├── __init__.py ├── hologram ├── hologram_activate.py ├── hologram_heartbeat.py ├── hologram_modem.py ├── hologram_network.py ├── hologram_receive.py ├── hologram_send.py ├── hologram_spacebridge.py └── hologram_util.py ├── setup.py ├── setup_helper.py ├── tests ├── API │ └── test_API.py ├── Authentication │ ├── test_Authentication.py │ ├── test_CSRPSKAuthentication.py │ └── test_HologramAuthentication.py ├── Event │ └── test_Event.py ├── MessageMode │ ├── test_Cloud.py │ ├── test_CustomCloud.py │ └── test_HologramCloud.py ├── Modem │ ├── test_BG96.py │ ├── test_EC21.py │ ├── test_MS2131.py │ ├── test_Modem.py │ ├── test_Nova.py │ ├── test_NovaM.py │ ├── test_NovaU201.py │ └── test_Quectel.py ├── ModemMode │ ├── test_ModemMode.py │ └── test_PPP.py └── Network │ ├── test_Cellular.py │ ├── test_Ethernet.py │ ├── test_Network.py │ └── test_NetworkManager.py ├── update.sh └── version.txt /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Help us stomp out bugs! 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 12 | ### Describe the problem 13 | 14 | 15 | ### Expected behavior 16 | 17 | 18 | 19 | ### Actual behavior 20 | 22 | 23 | 24 | ### Steps to reproduce the behavior 25 | 27 | 28 | ### System information 29 | - **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: 30 | - **Python SDK installed via PyPI or GitHub**: 31 | - **SDK version (use command below)**: 32 | - **Python version**: 33 | - **Hardware (modem) model**: 34 | 35 | 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | **Describe alternatives you've considered** 17 | 18 | 19 | **Additional context** 20 | 21 | -------------------------------------------------------------------------------- /.github/changelog-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION 🌈' 2 | 3 | tag-template: 'v$RESOLVED_VERSION' 4 | 5 | categories: 6 | - title: '🚀 Features' 7 | labels: 8 | - 'feature' 9 | - 'enhancement' 10 | - title: '🐛 Bug Fixes' 11 | labels: 12 | - 'fix' 13 | - 'bugfix' 14 | - 'bug' 15 | - title: '🧰 Maintenance' 16 | labels: 17 | - 'chore' 18 | - 'debt' 19 | - title: '🛠 Dependency Updates' 20 | labels: 21 | - 'dependencies' 22 | 23 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 24 | 25 | version-resolver: 26 | major: 27 | labels: 28 | - 'major' 29 | - 'breaking' 30 | minor: 31 | labels: 32 | - 'minor' 33 | - 'feature' 34 | patch: 35 | labels: 36 | - 'patch' 37 | - 'bugfix' 38 | - 'debt' 39 | - 'dependencies' 40 | default: patch 41 | 42 | exclude-labels: 43 | - 'skip-changelog' 44 | 45 | template: | 46 | ## Changes 47 | 48 | $CHANGES 49 | 50 | **Release Date:** 51 | **Release Engineer:** $CONTRIBUTORS 52 | **Previous Release:** $PREVIOUS_TAG -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Basic dependabot.yml file with 2 | # minimum configuration for two package managers 3 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 4 | 5 | version: 2 6 | updates: 7 | 8 | # Maintain dependencies for GitHub Actions 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | 14 | 15 | # Enable version updates for python pip 16 | - package-ecosystem: "pip" 17 | directory: "/" 18 | schedule: 19 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: Changelog Drafter 2 | 3 | on: 4 | push: 5 | # develop is our tracking branch 6 | branches: 7 | - develop 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: release-drafter/release-drafter@v5 15 | with: 16 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 17 | config-name: changelog-drafter.yml 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Python 3.9 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: '3.9' 17 | 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install setuptools wheel twine 22 | - name: Build and publish 23 | env: 24 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 25 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 26 | run: | 27 | python setup.py sdist bdist_wheel 28 | twine upload dist/* 29 | -------------------------------------------------------------------------------- /.github/workflows/testlint.yml: -------------------------------------------------------------------------------- 1 | name: Python Test and Lint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: [3.9] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install -r requirements-test.txt 24 | pip install -r requirements.txt 25 | 26 | - name: Lint with flake8 27 | run: | 28 | pip install flake8 29 | # stop the build if there are Python syntax errors or undefined names 30 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 31 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 32 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 33 | 34 | - name: Test with coverage and coveralls 35 | env: 36 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 37 | run: | 38 | pip install coveralls pytest-cov 39 | pytest --cov=Hologram tests/ 40 | coveralls 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Jetbrains IDE stuff 53 | .idea 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # IPython Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | 91 | # Rope project settings 92 | .ropeproject 93 | 94 | # ctags 95 | tags 96 | 97 | # Credentials file 98 | credentials.json 99 | 100 | .DS_Store 101 | 102 | [._]*.s[a-w][a-z] 103 | [._]s[a-w][a-z] 104 | *.un~ 105 | Session.vim 106 | .netrwhist 107 | *~ 108 | 109 | # Sublime SFTP plugin config file 110 | sftp-config.json 111 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # List of official Hologram contributors 2 | 3 | * Zheng Hao Tan 4 | * Reuben Balik 5 | * Erik Larson 6 | * Jeremy Tidemann 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@hologram.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | How to contribute 2 | ================= 3 | 4 | Go ahead and open an issue/pull request. We'll review them, and if it looks good, 5 | we'll merge in into the development branch! 6 | -------------------------------------------------------------------------------- /Exceptions/HologramError.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2016 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # HologramError.py - This file contains a list of custom Exception implementations. 8 | 9 | class HologramError(Exception): 10 | def __repr__(self): 11 | return '%s: %s' % (type(self).__name__, str(self)) 12 | 13 | class ApiError(HologramError): 14 | pass 15 | 16 | class AuthenticationError(HologramError): 17 | pass 18 | 19 | class NetworkError(HologramError): 20 | pass 21 | 22 | class ModemError(HologramError): 23 | pass 24 | 25 | class PPPError(NetworkError): 26 | pass 27 | 28 | class PPPConnectionError(PPPError): 29 | 30 | _PPPD_RETURNCODES = { 31 | 1: 'Fatal error occured', 32 | 2: 'Error processing options', 33 | 3: 'Not executed as root or setuid-root', 34 | 4: 'No kernel support, PPP kernel driver not loaded', 35 | 5: 'Received SIGINT, SIGTERM or SIGHUP', 36 | 6: 'Modem could not be locked', 37 | 7: 'Modem could not be opened', 38 | 8: 'Connect script failed', 39 | 9: 'pty argument command could not be run', 40 | 10: 'PPP negotiation failed', 41 | 11: 'Peer failed (or refused) to authenticate', 42 | 12: 'The link was terminated because it was idle', 43 | 13: 'The link was terminated because the connection time limit was reached', 44 | 14: 'Callback negotiated', 45 | 15: 'The link was terminated because the peer was not responding to echo requests', 46 | 16: 'The link was terminated by the modem hanging up', 47 | 17: 'PPP negotiation failed because serial loopback was detected', 48 | 18: 'Init script failed', 49 | 19: 'Failed to authenticate to the peer', 50 | } 51 | 52 | def __init__(self, code, output=None): 53 | self.code = code 54 | self.message = self._PPPD_RETURNCODES.get(code, 'Undocumented error occured') 55 | self.output = output 56 | 57 | super().__init__(code, output) 58 | 59 | class SerialError(HologramError): 60 | pass 61 | -------------------------------------------------------------------------------- /Exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /Hologram/Api/Api.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2016 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # Api.py - This file contains the Hologram REST API class implementation. 8 | 9 | import logging 10 | from logging import NullHandler 11 | 12 | from Exceptions.HologramError import ApiError 13 | import requests 14 | 15 | HOLOGRAM_REST_API_BASEURL = 'https://dashboard.hologram.io/api/1' 16 | 17 | class Api: 18 | 19 | def __init__(self, apikey='', username='', password=''): 20 | # Logging setup. 21 | self.logger = logging.getLogger(__name__) 22 | self.logger.addHandler(NullHandler()) 23 | self.authtype = None 24 | 25 | self.__enforce_auth_method(apikey, username, password) 26 | 27 | self.apikey = apikey 28 | self.username = username 29 | self.password = password 30 | 31 | # REQUIRES: a SIM number and a plan id. 32 | # EFFECTS: Activates a SIM. Returns a tuple of a success flag and 33 | # more info about the response. 34 | def activateSIM(self, sim='', plan=None, zone=1, preview=False): 35 | 36 | endpoint = HOLOGRAM_REST_API_BASEURL + '/links/cellular/sim_' + str(sim) + '/claim' 37 | 38 | args = self.__populate_auth_payload() 39 | args['data'] = {'plan': plan, 'tier': zone} 40 | 41 | if preview: 42 | args['params']['preview'] = 1 43 | 44 | response = requests.post(endpoint, **args) 45 | #pylint: disable=no-member 46 | if response.status_code != requests.codes.ok: 47 | return (False, response.text) 48 | 49 | response = response.json() 50 | if not response['success']: 51 | return (response['success'], response['data'][str(sim)]) 52 | return (response['success'], response['order_data']) 53 | 54 | # EFFECTS: Returns a list of plans. Returns a tuple of a success flag and 55 | # more info about the response. 56 | def getPlans(self): 57 | endpoint = HOLOGRAM_REST_API_BASEURL + '/plans' 58 | args = self.__populate_auth_payload() 59 | 60 | response = requests.get(endpoint, **args) 61 | #pylint: disable=no-member 62 | if response.status_code != requests.codes.ok: 63 | response.raise_for_status() 64 | 65 | response = response.json() 66 | return (response['success'], response['data']) 67 | 68 | # EFFECTS: Gets the SIM state 69 | def getSIMState(self, sim): 70 | endpoint = HOLOGRAM_REST_API_BASEURL + '/links/cellular' 71 | 72 | args = self.__populate_auth_payload() 73 | args['params']['sim'] = str(sim) 74 | 75 | response = requests.get(endpoint, **args) 76 | #pylint: disable=no-member 77 | if response.status_code != requests.codes.ok: 78 | response.raise_for_status() 79 | 80 | response = response.json() 81 | return (response['success'], response['data'][0]['state']) 82 | 83 | # EFFECTS: Populates and returns a dictionary with the proper HTTP 84 | # authentication credentials. 85 | def __populate_auth_payload(self): 86 | 87 | args = dict() 88 | args['params'] = dict() 89 | 90 | if self.authtype == 'basic_auth': 91 | args['auth'] = (self.username, self.password) 92 | elif self.authtype == 'apikey': 93 | args['params'] = {'apikey' : self.apikey} 94 | else: 95 | raise ApiError('Invalid HTTP Authentication type') 96 | 97 | return args 98 | 99 | # EFFECTS: Checks to make sure that the valid authentication parameters are being used 100 | # correctly, throws an Exception if there's an issue with it. 101 | def __enforce_auth_method(self, apikey, username, password): 102 | if apikey == '' and (username == '' or password == ''): 103 | raise ApiError('Must specify valid HTTP authentication credentials') 104 | elif apikey == '': 105 | self.authtype = 'basic_auth' 106 | else: 107 | self.authtype = 'apikey' 108 | -------------------------------------------------------------------------------- /Hologram/Api/__init__.py: -------------------------------------------------------------------------------- 1 | from .Api import * 2 | -------------------------------------------------------------------------------- /Hologram/Authentication/AES/AESCipher.py: -------------------------------------------------------------------------------- 1 | # AESCipher.py - Hologram Python SDK AESCipher interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | 11 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 12 | from cryptography.hazmat.primitives import padding 13 | from cryptography.hazmat.backends import default_backend 14 | 15 | class AESCipher: 16 | 17 | # EFFECTS: Constructor that sets the IV to 18 | def __init__(self, iv, key): 19 | self.iv = iv 20 | backend = default_backend() 21 | self.cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend) 22 | self.encryptor = self.cipher.encryptor() 23 | self.decryptor = self.cipher.decryptor() 24 | 25 | # EFFECTS: Encrypts the given plaintext in AES CBC mode. returns a cipher text. 26 | def AES_cbc_encrypt(self, plaintext): 27 | plaintext = self.padPKCS7(plaintext) 28 | ciphertext = self.encryptor.update(plaintext) 29 | return ciphertext 30 | 31 | # EFFECTS: Encrypts the given ciphertext in AES CBC mode. returns a plaintext. 32 | def AES_cbc_decrypt(self, ciphertext): 33 | plaintext = self.decryptor.update(ciphertext) 34 | return plaintext 35 | 36 | # EFFECTS: Pads the PKCS7 37 | def padPKCS7(self, newPlainText): 38 | padder = padding.PKCS7(128).padder() 39 | padded_data = padder.update(newPlainText) 40 | padded_data += padder.finalize() 41 | return padded_data 42 | -------------------------------------------------------------------------------- /Hologram/Authentication/Authentication.py: -------------------------------------------------------------------------------- 1 | # Authentication.py - Hologram Python SDK Authentication interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | import logging 11 | from logging import NullHandler 12 | 13 | class Authentication: 14 | 15 | def __init__(self, credentials): 16 | self.credentials = credentials 17 | 18 | # Logging setup. 19 | self.logger = logging.getLogger(__name__) 20 | self.logger.addHandler(NullHandler()) 21 | 22 | @property 23 | def credentials(self): 24 | return self._credentials 25 | 26 | @credentials.setter 27 | def credentials(self, credentials): 28 | self._credentials = credentials 29 | -------------------------------------------------------------------------------- /Hologram/Authentication/CSRPSKAuthentication.py: -------------------------------------------------------------------------------- 1 | # CSRPSKAuthentication.py - Hologram Python SDK CSRPSKAuthentication interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # This CSRPSKAuthentication file implements the CSRPSK authentication interface. 8 | # 9 | # LICENSE: Distributed under the terms of the MIT License 10 | # 11 | import json 12 | from Exceptions.HologramError import AuthenticationError 13 | from Hologram.Authentication.HologramAuthentication import HologramAuthentication 14 | 15 | DEVICE_KEY_LEN = 8 16 | 17 | class CSRPSKAuthentication(HologramAuthentication): 18 | 19 | def __init__(self, credentials): 20 | self._data = {} 21 | super().__init__(credentials=credentials) 22 | 23 | def buildPayloadString(self, messages, topics=None, modem_type=None, 24 | modem_id=None, version=None): 25 | 26 | self.enforceValidDeviceKey() 27 | 28 | super().buildPayloadString(messages, 29 | topics=topics, 30 | modem_type=modem_type, 31 | modem_id=modem_id, 32 | version=version) 33 | 34 | payload = json.dumps(self._data) + "\r\r" 35 | return payload.encode() 36 | 37 | def buildSMSPayloadString(self, destination_number, message): 38 | 39 | self.enforceValidDeviceKey() 40 | 41 | send_data = 'S' + self.credentials['devicekey'] 42 | send_data += destination_number + ' ' + message 43 | send_data += "\r\r" 44 | 45 | return send_data.encode() 46 | 47 | def buildAuthString(self, timestamp=None, sequence_number=None): 48 | self._data['k'] = self.credentials['devicekey'] 49 | 50 | def buildMetadataString(self, modem_type, modem_id, version): 51 | 52 | formatted_string = f"{self.build_modem_type_id_str(modem_type, modem_id)}-{version}" 53 | self._data['m'] = self.metadata_version.decode() + formatted_string 54 | 55 | def buildTopicString(self, topics): 56 | self._data['t'] = topics 57 | 58 | def buildMessageString(self, messages): 59 | self._data['d'] = messages 60 | 61 | def enforceValidDeviceKey(self): 62 | if not isinstance(self.credentials, dict): 63 | raise AuthenticationError('Credentials is not a dictionary') 64 | elif not self.credentials['devicekey']: 65 | raise AuthenticationError('Must set devicekey to use CSRPSKAuthentication') 66 | elif len(self.credentials['devicekey']) != DEVICE_KEY_LEN: 67 | raise AuthenticationError('Device key must be %d characters long' % DEVICE_KEY_LEN) 68 | -------------------------------------------------------------------------------- /Hologram/Authentication/HologramAuthentication.py: -------------------------------------------------------------------------------- 1 | # HologramAuthentication.py - Hologram Python SDK HologramAuthentication interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | from Hologram.Authentication.Authentication import Authentication 11 | 12 | class HologramAuthentication(Authentication): 13 | 14 | def buildPayloadString(self, messages, topics=None, modem_type=None, 15 | modem_id=None, version=None): 16 | 17 | self.buildAuthString() 18 | 19 | self.buildMetadataString(modem_type, modem_id, version) 20 | 21 | # Attach topic(s) 22 | if topics is not None: 23 | self.buildTopicString(topics) 24 | 25 | # Attach message(s) 26 | self.buildMessageString(messages) 27 | 28 | def buildAuthString(self, timestamp=None, sequence_number=None): 29 | raise NotImplementedError('Must instantiate a subclass of HologramAuthentication') 30 | 31 | def buildMetadataString(self, modem_type, modem_id, version): 32 | raise NotImplementedError('Must instantiate a subclass of HologramAuthentication') 33 | 34 | def buildTopicString(self, topics): 35 | raise NotImplementedError('Must instantiate a subclass of HologramAuthentication') 36 | 37 | def buildMessageString(self, messages): 38 | raise NotImplementedError('Must instantiate a subclass of HologramAuthentication') 39 | 40 | # EFFECTS: Builds the encoded modem type + id string. 41 | # Used to build out metadata string. 42 | def build_modem_type_id_str(self, modem_type, modem_id): 43 | 44 | # Handle agnostic cases separately. 45 | if modem_type is None: 46 | return 'agnostic' 47 | 48 | payload = modem_type.lower() 49 | 50 | if modem_type == 'Nova': 51 | payload += ('-' + modem_id) 52 | return payload 53 | 54 | @property 55 | def metadata_version(self): 56 | return b'\x01' 57 | -------------------------------------------------------------------------------- /Hologram/Authentication/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hologram-io/hologram-python/053c4848ef83dc3a287017269acd1497874ebf76/Hologram/Authentication/__init__.py -------------------------------------------------------------------------------- /Hologram/Cloud.py: -------------------------------------------------------------------------------- 1 | # Cloud.py - Hologram Python SDK Cloud interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | 10 | import logging 11 | from logging import NullHandler 12 | from Hologram.Event import Event 13 | from typing import Union 14 | from Hologram.Network.Modem.Modem import Modem 15 | from Hologram.Network import NetworkManager 16 | from Hologram.Authentication import * 17 | 18 | __version__ = '0.9.1' 19 | 20 | class Cloud: 21 | 22 | def __repr__(self): 23 | return type(self).__name__ 24 | 25 | def __init__(self, credentials, send_host = '', send_port = 0, 26 | receive_host = '', receive_port = 0, network = '', modem: Union[None, Modem] = None): 27 | 28 | # Logging setup. 29 | self.logger = logging.getLogger(__name__) 30 | self.logger.addHandler(NullHandler()) 31 | 32 | self.authentication = None 33 | 34 | # Host and port configuration 35 | self.__initialize_host_and_port(send_host, send_port, 36 | receive_host, receive_port) 37 | 38 | self.initializeNetwork(network, modem) 39 | 40 | def __initialize_host_and_port(self, send_host, send_port, receive_host, receive_port): 41 | self.send_host = send_host 42 | self.send_port = send_port 43 | self.receive_host = receive_host 44 | self.receive_port = receive_port 45 | 46 | def initializeNetwork(self, network, modem): 47 | 48 | self.event = Event() 49 | self.__message_buffer = [] 50 | 51 | # Network Configuration 52 | self._networkManager = NetworkManager.NetworkManager(self.event, network, modem=modem) 53 | 54 | # This registers the message buffering feature based on network availability. 55 | self.event.subscribe('network.connected', self.__clear_payload_buffer) 56 | self.event.subscribe('network.disconnected', self._networkManager.networkDisconnected) 57 | 58 | # EFFECTS: Adds the given payload to the buffer 59 | def addPayloadToBuffer(self, payload): 60 | self.__message_buffer.append(payload) 61 | 62 | # EFFECTS: Tells the network manager that it is connected and clears all buffered 63 | # messages by sending them to the cloud. 64 | def __clear_payload_buffer(self): 65 | self._networkManager.networkConnected() 66 | for payload in self.__message_buffer: 67 | 68 | recv = self.sendMessage(payload) 69 | self.logger.info("A buffered message has been sent since an active connection is established") 70 | self.logger.debug("The buffered message sent is: %s", str(payload)) 71 | self.logger.info("The buffered response is: %s", str(recv)) 72 | 73 | def sendMessage(self, messages, topics = None): 74 | raise NotImplementedError('Must instantiate a Cloud type') 75 | 76 | # EFFECTS: Sends the SMS to the destination number specified. 77 | def sendSMS(self, destination_number, message): 78 | raise NotImplementedError('Must instantiate a Cloud type') 79 | 80 | @property 81 | def authentication(self): 82 | return self._authentication 83 | 84 | @authentication.setter 85 | def authentication(self, authentication): 86 | self._authentication = authentication 87 | 88 | @property 89 | def credentials(self): 90 | return self.authentication.credentials 91 | 92 | @credentials.setter 93 | def credentials(self, credentials): 94 | self.authentication.credentials = credentials 95 | 96 | @property 97 | def version(self): 98 | return __version__ 99 | 100 | @property 101 | def send_host(self): 102 | return self._send_host 103 | 104 | @send_host.setter 105 | def send_host(self, send_host): 106 | self._send_host = send_host 107 | 108 | @property 109 | def send_port(self): 110 | return self._send_port 111 | 112 | @send_port.setter 113 | def send_port(self, send_port): 114 | try: 115 | self._send_port = int(send_port) 116 | except ValueError as e: 117 | raise ValueError('Invalid port parameter. Unable to convert port to a valid integer') 118 | 119 | @property 120 | def receive_host(self): 121 | return self._receive_host 122 | 123 | @receive_host.setter 124 | def receive_host(self, receive_host): 125 | self._receive_host = receive_host 126 | 127 | @property 128 | def receive_port(self): 129 | return self._receive_port 130 | 131 | @receive_port.setter 132 | def receive_port(self, receive_port): 133 | self._receive_port = int(receive_port) 134 | 135 | @property 136 | def event(self): 137 | return self._event 138 | 139 | @event.setter 140 | def event(self, event): 141 | self._event = event 142 | 143 | @property 144 | def network_type(self): 145 | return repr(self._networkManager) 146 | 147 | # Returns the network instance itself. 148 | @property 149 | def network(self): 150 | return self._networkManager.network 151 | -------------------------------------------------------------------------------- /Hologram/Event/Event.py: -------------------------------------------------------------------------------- 1 | # Event.py - Hologram Python SDK Event interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | import logging 10 | 11 | class Event: 12 | _funcLookupTable = {} 13 | def __init__(self): 14 | self.__dict__ = self._funcLookupTable 15 | 16 | def subscribe(self, event, callback): 17 | 18 | if not self.__dict__.get(event): 19 | self.__dict__[event] = [callback] 20 | else: 21 | if callback not in self.__dict__[event]: 22 | self.__dict__[event].append(callback) 23 | else: 24 | logging.debug("Callback already subscribed: event[%s] callback[%s]", event, str(callback)) 25 | 26 | def unsubscribe(self, event, callback): 27 | 28 | if not self.__dict__.get(event): 29 | return 30 | 31 | # Iterate over the list of callbacks and remove the specified one. 32 | for temp in self.__dict__[event][:]: 33 | if temp is callback: 34 | self.__dict__[event].remove(temp) 35 | 36 | # EFFECTS: Broadcasts the triggered event to all subscribers. 37 | def broadcast(self, event): 38 | 39 | callbacks = self.__dict__.get(event) 40 | 41 | if callbacks: 42 | for callback in callbacks: 43 | callback() 44 | -------------------------------------------------------------------------------- /Hologram/Event/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['Event'] 2 | from .Event import * 3 | -------------------------------------------------------------------------------- /Hologram/HologramCloud.py: -------------------------------------------------------------------------------- 1 | # HologramCloud.py - Hologram Python SDK Cloud interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | 11 | import binascii 12 | import json 13 | import sys 14 | from typing import Union 15 | from Hologram.Network.Modem.Modem import Modem 16 | from Hologram.CustomCloud import CustomCloud 17 | from HologramAuth import TOTPAuthentication, SIMOTPAuthentication 18 | from Hologram.Authentication import CSRPSKAuthentication 19 | from Exceptions.HologramError import HologramError, AuthenticationError 20 | 21 | DEFAULT_SEND_MESSAGE_TIMEOUT = 5 22 | HOLOGRAM_HOST_SEND = 'cloudsocket.hologram.io' 23 | HOLOGRAM_PORT_SEND = 9999 24 | HOLOGRAM_HOST_RECEIVE = '0.0.0.0' 25 | HOLOGRAM_PORT_RECEIVE = 4010 26 | MAX_SMS_LENGTH = 160 27 | 28 | 29 | # Hologram error codes 30 | ERR_OK = 0 31 | ERR_CONNCLOSED = 1 # Connection was closed before a terminating character 32 | # but message might be fine 33 | ERR_MSGINVALID = 2 # Couldn't parse the message 34 | ERR_AUTHINVALID = 3 # Auth section of message was invalid 35 | ERR_PAYLOADINVALID = 4 # Payload type was invalid 36 | ERR_PROTINVALID = 5 # Protocol type was invalid 37 | ERR_INTERNAL = 6 # An internal error occurred 38 | ERR_METADATA = 7 # Metadata was formatted incorrectly 39 | ERR_TOPICINVALID = 8 # Topic was formatted incorrectly 40 | ERR_UNKNOWN = -1 # Unknown error 41 | 42 | class HologramCloud(CustomCloud): 43 | 44 | _authentication_handlers = { 45 | 'csrpsk' : CSRPSKAuthentication.CSRPSKAuthentication, 46 | 'totp' : TOTPAuthentication.TOTPAuthentication, 47 | 'sim-otp' : SIMOTPAuthentication.SIMOTPAuthentication, 48 | } 49 | 50 | _errorCodeDescription = { 51 | 52 | ERR_OK: 'Message sent successfully', 53 | ERR_CONNCLOSED: 'Connection was closed so we couldn\'t read the whole message', 54 | ERR_MSGINVALID: 'Failed to parse the message', 55 | ERR_AUTHINVALID: 'Auth section of the message was invalid', 56 | ERR_PAYLOADINVALID: 'Payload type was invalid', 57 | ERR_PROTINVALID: 'Protocol type was invalid', 58 | ERR_INTERNAL: 'Internal error in Hologram Cloud', 59 | ERR_METADATA: 'Metadata was formatted incorrectly', 60 | ERR_TOPICINVALID: 'Topic was formatted incorrectly', 61 | ERR_UNKNOWN: 'Unknown error' 62 | } 63 | 64 | def __init__(self, credentials, enable_inbound=False, network='', 65 | authentication_type='totp', modem: Union[None, Modem] = None): 66 | super().__init__(credentials, 67 | send_host=HOLOGRAM_HOST_SEND, 68 | send_port=HOLOGRAM_PORT_SEND, 69 | receive_host=HOLOGRAM_HOST_RECEIVE, 70 | receive_port=HOLOGRAM_PORT_RECEIVE, 71 | enable_inbound=enable_inbound, 72 | network=network, 73 | modem=modem 74 | ) 75 | 76 | self.setAuthenticationType(credentials, authentication_type=authentication_type) 77 | 78 | if self.authenticationType == 'totp': 79 | self.__populate_totp_credentials() 80 | 81 | # EFFECTS: Authentication Configuration 82 | def setAuthenticationType(self, credentials, authentication_type='csrpsk'): 83 | 84 | if authentication_type not in HologramCloud._authentication_handlers: 85 | raise HologramError('Invalid authentication type: %s' % authentication_type) 86 | 87 | self.authenticationType = authentication_type 88 | 89 | self.authentication = HologramCloud._authentication_handlers[self.authenticationType](credentials) 90 | 91 | # EFFECTS: Sends the message to the cloud. 92 | def sendMessage(self, message, topics=None, timeout=DEFAULT_SEND_MESSAGE_TIMEOUT): 93 | 94 | if not self.is_ready_to_send(): 95 | self.addPayloadToBuffer(message) 96 | return '' 97 | 98 | # Set the appropriate credentials required for sim otp authentication. 99 | if self.authenticationType == 'sim-otp': 100 | self.__populate_sim_otp_credentials() 101 | 102 | modem_type = None 103 | modem_id = None 104 | if self.network is not None: 105 | modem_id = self.network.modem_id 106 | modem_type = str(self.network.modem) 107 | 108 | output = self.authentication.buildPayloadString(message, 109 | topics=topics, 110 | modem_type=modem_type, 111 | modem_id=modem_id, 112 | version=self.version) 113 | 114 | result = super().sendMessage(output, timeout) 115 | return self.__parse_result(result) 116 | 117 | def __parse_result(self, result): 118 | resultList = None 119 | if self.authenticationType == 'csrpsk': 120 | resultList = self.__parse_hologram_json_result(result) 121 | else: 122 | resultList = self.__parse_hologram_compact_result(result) 123 | 124 | return resultList[0] 125 | 126 | def __populate_totp_credentials(self): 127 | try: 128 | self.authentication.credentials['device_id'] = self.network.iccid 129 | self.authentication.credentials['private_key'] = self.network.imsi 130 | except Exception as e: 131 | self.logger.error('Unable to fetch device id or private key') 132 | raise AuthenticationError('Unable to fetch device id or private key for TOTP authenication') 133 | 134 | def __populate_sim_otp_credentials(self): 135 | nonce = self.request_hex_nonce() 136 | command = self.authentication.generate_sim_otp_command(imsi=self.network.imsi, 137 | iccid=self.network.iccid, 138 | nonce=nonce) 139 | modem_response = self.network.get_sim_otp_response(command) 140 | self.authentication.generate_sim_otp_token(modem_response) 141 | 142 | def sendSMS(self, destination_number, message): 143 | 144 | self.__enforce_authentication_type_supported_for_sms() 145 | self.__enforce_valid_destination_number(destination_number) 146 | self.__enforce_max_sms_length(message) 147 | 148 | output = self.authentication.buildSMSPayloadString(destination_number, 149 | message) 150 | 151 | self.logger.debug('Destination number: %s', destination_number) 152 | self.logger.debug('SMS: %s', message) 153 | 154 | result = super().sendMessage(output) 155 | 156 | resultList = self.__parse_hologram_compact_result(result) 157 | return resultList[0] 158 | 159 | # REQUIRES: Called only when sim otp authentication is required. 160 | # EFFECTS: Request for a hex nonce. 161 | def request_hex_nonce(self): 162 | 163 | self.open_send_socket() 164 | 165 | # build nonce request payload string 166 | nonce_request = self.authentication.buildNonceRequestPayloadString() 167 | 168 | self.logger.debug("Sending nonce request with body of length %d", len(nonce_request)) 169 | self.logger.debug('Send: %s', nonce_request) 170 | 171 | nonce = super().sendMessage(message=nonce_request, timeout=10, close_socket=False) 172 | self.logger.debug('Nonce request sent.') 173 | 174 | resultbuf_hex = binascii.b2a_hex(nonce) 175 | 176 | if resultbuf_hex is None: 177 | raise HologramError('Internal nonce error') 178 | 179 | return resultbuf_hex 180 | 181 | def enableSMS(self): 182 | return self.network.enableSMS() 183 | 184 | def disableSMS(self): 185 | return self.network.disableSMS() 186 | 187 | def popReceivedSMS(self): 188 | return self.network.popReceivedSMS() 189 | 190 | # EFFECTS: Parses the hologram send response. 191 | def __parse_hologram_json_result(self, result): 192 | try: 193 | resultList = json.loads(result) 194 | if isinstance(resultList, bytes): 195 | resultList[0] = int(chr(resultList[0])) 196 | else: 197 | resultList[0] = int(resultList[0]) 198 | except ValueError: 199 | self.logger.error('Server replied with invalid JSON [%s]', result) 200 | resultList = [ERR_UNKNOWN] 201 | return resultList 202 | 203 | 204 | def __parse_hologram_compact_result(self, result): 205 | 206 | # convert the returned response to formatted list. 207 | if result is None: 208 | return [ERR_UNKNOWN] 209 | 210 | resultList = [] 211 | if isinstance(result, bytes): 212 | for x in result: 213 | resultList.append(int(chr(x))) 214 | else: 215 | for x in result: 216 | resultList.append(int(x)) 217 | 218 | if len(resultList) == 0: 219 | resultList = [ERR_UNKNOWN] 220 | 221 | return resultList 222 | 223 | def __enforce_max_sms_length(self, message): 224 | if len(message) > MAX_SMS_LENGTH: 225 | raise HologramError('SMS cannot be more than %d characters long' % MAX_SMS_LENGTH) 226 | 227 | def __enforce_valid_destination_number(self, destination_number): 228 | if not destination_number.startswith('+'): 229 | raise HologramError('SMS destination number must start with a \'+\' sign') 230 | 231 | def __enforce_authentication_type_supported_for_sms(self): 232 | if self.authenticationType != 'csrpsk': 233 | raise HologramError('%s does not support SDK SMS features' % self.authenticationType) 234 | 235 | # REQUIRES: A result code (int). 236 | # EFFECTS: Returns a translated string based on the given hologram result code. 237 | def getResultString(self, result_code): 238 | if result_code not in self._errorCodeDescription: 239 | return 'Unknown response code' 240 | return self._errorCodeDescription[result_code] 241 | 242 | def resultWasSuccess(self, result_code): 243 | return result_code in (ERR_OK, ERR_CONNCLOSED) 244 | -------------------------------------------------------------------------------- /Hologram/Network/BLE.py: -------------------------------------------------------------------------------- 1 | from Hologram.Event import Event 2 | from Hologram.Network import Network 3 | 4 | class BLE(Network): 5 | 6 | def __init__(self): 7 | self.event = Event() 8 | super().__init__() 9 | 10 | def connect(self): 11 | self.event.broadcast('ble.connected') 12 | return True 13 | 14 | def disconnect(self): 15 | self.event.broadcast('ble.disconnected') 16 | return True 17 | 18 | def getConnectionStatus(self): 19 | raise Exception('BLE mode doesn\'t support this call yet') 20 | 21 | def reconnect(self): 22 | return True 23 | -------------------------------------------------------------------------------- /Hologram/Network/Ethernet.py: -------------------------------------------------------------------------------- 1 | from Hologram.Network import Network 2 | from Hologram.Event import Event 3 | import time 4 | import os 5 | 6 | class Ethernet(Network): 7 | 8 | def __init__(self, interfaceName = 'eth0'): 9 | self.interfaceName = interfaceName 10 | # TODO(zheng): change to ethernet library 11 | # self.ethernet = Wireless(interfaceName) 12 | super().__init__() 13 | 14 | def getBitRate(self): 15 | # bitrate = self.ethernet.wireless_info.getBitrate() 16 | # return "Bit Rate :%s" % self.ethernet.getBitrate() 17 | return '' 18 | 19 | def disconnect(self): 20 | os.system("ifconfig " + self.interfaceName + " down") 21 | self.event.broadcast('ethernet.disconnected') 22 | super().disconnect() 23 | 24 | def connect(self): 25 | os.system("ifconfig " + self.interfaceName + " up") 26 | self.event.broadcast('ethernet.connected') 27 | super().connect() 28 | 29 | def isConnected(self): 30 | return True 31 | 32 | def getConnectionStatus(self): 33 | raise Exception('Ethernet mode doesn\'t support this call') 34 | 35 | def getSignalStrength(self): 36 | raise Exception('Ethernet mode doesn\'t support this call') 37 | 38 | def getAvgSignalStrength(self): 39 | raise Exception('Ethernet mode doesn\'t support this call') 40 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/BG96.py: -------------------------------------------------------------------------------- 1 | # BG96.py - Hologram Python SDK Quectel BG96 modem interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | 11 | from Hologram.Network.Modem.Quectel import Quectel 12 | from UtilClasses import ModemResult 13 | 14 | DEFAULT_BG96_TIMEOUT = 200 15 | 16 | class BG96(Quectel): 17 | usb_ids = [('2c7c', '0296')] 18 | 19 | def connect(self, timeout=DEFAULT_BG96_TIMEOUT): 20 | return super().connect(timeout) 21 | 22 | def _tear_down_pdp_context(self): 23 | if not self._is_pdp_context_active(): return True 24 | self.logger.info('Tearing down PDP context') 25 | ok, _ = self.set('+QIACT', '0', timeout=30) 26 | if ok != ModemResult.OK: 27 | self.logger.error('PDP Context tear down failed') 28 | else: 29 | self.logger.info('PDP context deactivated') 30 | 31 | @property 32 | def description(self): 33 | return 'Quectel BG96' 34 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/DriverLoader.py: -------------------------------------------------------------------------------- 1 | # DriverLoader.py - Class that wraps around different module loading 2 | # operations that are used for the R410 and maybe other modems in the 3 | # future 4 | # Author: Hologram 5 | # 6 | # 7 | # Copyright 2018 - Hologram (Konekt, Inc.) 8 | # 9 | # 10 | # LICENSE: Distributed under the terms of the MIT License 11 | # 12 | 13 | import subprocess 14 | 15 | 16 | class DriverLoader: 17 | # I would much rather use python-kmod for all this 18 | # but it doesn't seem to build properly on the Pi and 19 | # hasn't been updated in years. It's possible we need to update 20 | # to python 3 for it to work correctly 21 | 22 | 23 | def is_module_loaded(self, module): 24 | output = subprocess.check_output(['lsmod']) 25 | lines = output.splitlines() 26 | for line in lines: 27 | splitline = line.split() 28 | if splitline[0] == "option": 29 | return True 30 | return False 31 | 32 | 33 | def load_module(self, module): 34 | subprocess.call(['sudo', 'modprobe', module]) 35 | 36 | 37 | def force_driver_for_device(self, syspath, vid, pid): 38 | with open(syspath, "w") as f: 39 | f.write("%s %s"%(vid, pid)) 40 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/E303.py: -------------------------------------------------------------------------------- 1 | # E303.py - Hologram Python SDK Huawei E303 modem interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | 11 | from Hologram.Network.Modem import Modem 12 | from Hologram.Event import Event 13 | 14 | DEFAULT_E303_TIMEOUT = 200 15 | 16 | class E303(Modem): 17 | usb_ids = [('12d1','1001')] 18 | 19 | def __init__(self, device_name=None, baud_rate='9600', 20 | chatscript_file=None, event=Event()): 21 | 22 | super().__init__(device_name=device_name, baud_rate=baud_rate, 23 | chatscript_file=chatscript_file, event=event) 24 | 25 | def connect(self, timeout = DEFAULT_E303_TIMEOUT): 26 | return super().connect(timeout) 27 | 28 | def set_network_registration_status(self): 29 | self.command("+CREG", "2") 30 | self.command("+CGREG", "2") 31 | 32 | @property 33 | def iccid(self): 34 | return self._basic_command('^ICCID?').lstrip('^ICCID: ')[:-1] 35 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/E372.py: -------------------------------------------------------------------------------- 1 | # E372.py - Based on Hologram Python SDK Huawei MS2131 modem interface 2 | # 3 | # 4 | # 5 | # 6 | # 7 | # LICENSE: Distributed under the terms of the MIT License 8 | # 9 | 10 | from Hologram.Network.Modem import Modem 11 | from Hologram.Event import Event 12 | 13 | DEFAULT_E372_TIMEOUT = 200 14 | 15 | class E372(Modem): 16 | 17 | usb_ids = [('12d1', '14c6')] 18 | 19 | def __init__(self, device_name=None, baud_rate='9600', 20 | chatscript_file=None, event=Event()): 21 | 22 | super().__init__(device_name=device_name, baud_rate=baud_rate, 23 | chatscript_file=chatscript_file, event=event) 24 | 25 | def connect(self, timeout = DEFAULT_E372_TIMEOUT): 26 | return super().connect(timeout) 27 | 28 | def set_network_registration_status(self): 29 | self.command("+CREG", "2") 30 | self.command("+CGREG", "2") 31 | 32 | def disable_at_sockets_mode(self): 33 | pass 34 | 35 | @property 36 | def iccid(self): 37 | return self._basic_command('^ICCID?').lstrip('^ICCID: ')[:-1] 38 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/EC21.py: -------------------------------------------------------------------------------- 1 | # EC21.py - Hologram Python SDK Quectel EC21 modem interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | 11 | from Hologram.Network.Modem.Quectel import Quectel 12 | from UtilClasses import ModemResult 13 | 14 | DEFAULT_EC21_TIMEOUT = 200 15 | 16 | class EC21(Quectel): 17 | usb_ids = [('2c7c', '0121')] 18 | 19 | def connect(self, timeout=DEFAULT_EC21_TIMEOUT): 20 | success = super().connect(timeout) 21 | return success 22 | 23 | def _tear_down_pdp_context(self): 24 | if not self._is_pdp_context_active(): return True 25 | self.logger.info('Tearing down PDP context') 26 | ok, _ = self.set('+QIDEACT', '1', timeout=30) 27 | if ok != ModemResult.OK: 28 | self.logger.error('PDP Context tear down failed') 29 | else: 30 | self.logger.info('PDP context deactivated') 31 | 32 | @property 33 | def description(self): 34 | return 'Quectel EC21' 35 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/IModem.py: -------------------------------------------------------------------------------- 1 | # Modem.py - Hologram Python SDK Modem interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | 10 | import logging 11 | from logging import NullHandler 12 | from Hologram.Event import Event 13 | 14 | # Modem error codes - this is similar to what we have in Dash system firmware. 15 | MODEM_NO_MATCH = -3 16 | MODEM_ERROR = -2 17 | MODEM_TIMEOUT = -1 18 | MODEM_OK = 0 19 | 20 | class IModem: 21 | 22 | usb_ids = [] 23 | # module needed by modem 24 | module = '' 25 | # system path to write usb IDs to to force use of a driver 26 | syspath = '' 27 | 28 | _error_code_description = { 29 | 30 | MODEM_NO_MATCH: 'Modem response doesn\'t match expected return value', 31 | MODEM_ERROR: 'Modem error', 32 | MODEM_TIMEOUT: 'Modem timeout', 33 | MODEM_OK: 'Modem returned OK' 34 | } 35 | 36 | def __init__(self, device_name='/dev/ttyUSB0', baud_rate='9600', event=Event()): 37 | # Logging setup. 38 | self.logger = logging.getLogger(__name__) 39 | self.logger.addHandler(NullHandler()) 40 | 41 | self.event = event 42 | self.device_name = device_name 43 | self.baud_rate = baud_rate 44 | 45 | def __repr__(self): 46 | return type(self).__name__ 47 | 48 | # REQUIRES: A result code (int). 49 | # EFFECTS: Returns a translated string based on the given modem result code. 50 | def getResultString(self, result_code): 51 | if result_code not in self._error_code_description: 52 | return 'Unknown response code' 53 | return self._error_code_description[result_code] 54 | 55 | def isConnected(self): 56 | raise NotImplementedError('Must instantiate a Modem type') 57 | 58 | def connect(self): 59 | raise NotImplementedError('Must instantiate a Modem type') 60 | 61 | def disconnect(self): 62 | raise NotImplementedError('Must instantiate a Modem type') 63 | 64 | def reset(self): 65 | raise NotImplementedError('Must instantiate a Modem type') 66 | 67 | def radio_power(self, power_mode): 68 | raise NotImplementedError('Must instantiate a Modem type') 69 | 70 | def enableSMS(self): 71 | raise NotImplementedError('Must instantiate a Modem type') 72 | 73 | def disableSMS(self): 74 | raise NotImplementedError('Must instantiate a Modem type') 75 | 76 | def popReceivedSMS(self): 77 | raise NotImplementedError('Must instantiate a Modem type') 78 | 79 | @property 80 | def description(self): 81 | return self.__repr__() 82 | 83 | @property 84 | def localIPAddress(self): 85 | raise NotImplementedError('Must instantiate a Modem type') 86 | 87 | @property 88 | def remoteIPAddress(self): 89 | raise NotImplementedError('Must instantiate a Modem type') 90 | 91 | # EFFECTS: Returns the Received Signal Strength Indication (RSSI) value of the modem 92 | @property 93 | def signal_strength(self): 94 | raise NotImplementedError('Must instantiate a Modem type') 95 | 96 | @property 97 | def modem_id(self): 98 | raise NotImplementedError('Must instantiate a Modem type') 99 | 100 | @property 101 | def imsi(self): 102 | raise NotImplementedError('Must instantiate a Modem type') 103 | 104 | @property 105 | def iccid(self): 106 | raise NotImplementedError('Must instantiate a Modem type') 107 | 108 | @property 109 | def location(self): 110 | raise NotImplementedError('Must instantiate a Modem type') 111 | 112 | @property 113 | def operator(self): 114 | raise NotImplementedError('Must instantiate a Modem type') 115 | 116 | @property 117 | def mode(self): 118 | raise NotImplementedError('Must instantiate a Modem type') 119 | 120 | @property 121 | def device_name(self): 122 | return self._device_name 123 | 124 | @device_name.setter 125 | def device_name(self, device_name): 126 | self._device_name = device_name 127 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/MS2131.py: -------------------------------------------------------------------------------- 1 | # MS2131.py - Hologram Python SDK Huawei MS2131 modem interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | 11 | from Hologram.Network.Modem import Modem 12 | from Hologram.Event import Event 13 | 14 | DEFAULT_MS2131_TIMEOUT = 200 15 | 16 | class MS2131(Modem): 17 | 18 | usb_ids = [('12d1', '1506')] 19 | 20 | def __init__(self, device_name=None, baud_rate='9600', 21 | chatscript_file=None, event=Event()): 22 | 23 | super().__init__(device_name=device_name, baud_rate=baud_rate, 24 | chatscript_file=chatscript_file, event=event) 25 | 26 | def connect(self, timeout = DEFAULT_MS2131_TIMEOUT): 27 | return super().connect(timeout) 28 | 29 | def set_network_registration_status(self): 30 | self.command("+CREG", "2") 31 | self.command("+CGREG", "2") 32 | 33 | @property 34 | def iccid(self): 35 | return self._basic_command('^ICCID?').lstrip('^ICCID: ')[:-1] 36 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/MockModem.py: -------------------------------------------------------------------------------- 1 | # MockModem.py - Hologram Python SDK mock modem interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | 11 | from Hologram.Network.Modem import IModem 12 | 13 | E303_DEVICE_NAME = '/dev/ttyUSB0' 14 | DEFAULT_E303_TIMEOUT = 200 15 | 16 | class MockModem(IModem): 17 | 18 | def __init__(self, device_name=E303_DEVICE_NAME, baud_rate='9600', 19 | chatscript_file=None): 20 | super().__init__(device_name=device_name, baud_rate=baud_rate) 21 | 22 | def _get_attached_devices(self): 23 | return '/dev/test1, /dev/test2, /dev/ttyACM0' 24 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/ModemMode/IPPP.py: -------------------------------------------------------------------------------- 1 | # IPPP.py - Hologram Python SDK Modem PPP interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | from Hologram.Network.Modem.ModemMode.ModemMode import ModemMode 11 | 12 | class IPPP(ModemMode): 13 | 14 | def __init__(self, device_name='/dev/ttyUSB0', baud_rate='9600', 15 | chatscript_file=None): 16 | 17 | super().__init__(device_name=device_name, baud_rate=baud_rate) 18 | 19 | self.chatscript_file = chatscript_file 20 | 21 | if self.chatscript_file is None: 22 | raise Exception('Must specify chatscript file') 23 | 24 | @property 25 | def connect_script(self): 26 | return '/usr/sbin/chat -v -f ' + self.chatscript_file 27 | 28 | @property 29 | def chatscript_file(self): 30 | return self._chatscript_file 31 | 32 | @chatscript_file.setter 33 | def chatscript_file(self, chatscript_file): 34 | self._chatscript_file = chatscript_file 35 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/ModemMode/MockPPP.py: -------------------------------------------------------------------------------- 1 | # MockPPP.py - Hologram Python SDK Modem PPP mock interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | from Hologram.Network.Modem.ModemMode.IPPP import IPPP 11 | 12 | class MockPPP(IPPP): 13 | 14 | def __init__(self, device_name='/dev/ttyUSB0', baud_rate='9600', 15 | chatscript_file=None): 16 | 17 | super().__init__(device_name=device_name, baud_rate=baud_rate, 18 | chatscript_file=chatscript_file) 19 | 20 | @property 21 | def localIPAddress(self): 22 | return None 23 | 24 | @property 25 | def remoteIPAddress(self): 26 | return None 27 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/ModemMode/ModemMode.py: -------------------------------------------------------------------------------- 1 | # ModemMode.py - Hologram Python SDK modem mode interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | import logging 11 | from logging import NullHandler 12 | from Hologram.Event import Event 13 | 14 | class ModemMode: 15 | 16 | def __repr__(self): 17 | return type(self).__name__ 18 | 19 | def __init__(self, device_name='/dev/ttyUSB0', baud_rate='9600', event=Event()): 20 | 21 | # Logging setup. 22 | self.logger = logging.getLogger(__name__) 23 | self.logger.addHandler(NullHandler()) 24 | 25 | self.event = event 26 | self.device_name = device_name 27 | self.baud_rate = baud_rate 28 | 29 | @property 30 | def device_name(self): 31 | return self._device_name 32 | 33 | @device_name.setter 34 | def device_name(self, device_name): 35 | self._device_name = device_name 36 | 37 | @property 38 | def baud_rate(self): 39 | return self._baud_rate 40 | 41 | @baud_rate.setter 42 | def baud_rate(self, baud_rate): 43 | self._baud_rate = baud_rate 44 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/ModemMode/PPP.py: -------------------------------------------------------------------------------- 1 | # PPP.py - Hologram Python SDK Modem PPP interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | import psutil 11 | from Hologram.Network.Modem.ModemMode.pppd import PPPConnection 12 | from Hologram.Network.Modem.ModemMode.IPPP import IPPP 13 | from Hologram.Network.Route import Route 14 | from Exceptions.HologramError import PPPError 15 | 16 | DEFAULT_PPP_TIMEOUT = 200 17 | DEFAULT_PPP_INTERFACE = 'ppp0' 18 | MAX_PPP_INTERFACE_UP_RETRIES = 10 19 | MAX_REROUTE_PACKET_RETRIES = 15 20 | 21 | 22 | class PPP(IPPP): 23 | 24 | def __init__(self, device_name='/dev/ttyUSB0', all_attached_device_names=[], 25 | baud_rate='9600', chatscript_file=None): 26 | 27 | super().__init__(device_name=device_name, baud_rate=baud_rate, 28 | chatscript_file=chatscript_file) 29 | 30 | self.route = Route() 31 | self.all_attached_device_names = all_attached_device_names 32 | self._ppp = PPPConnection(self.device_name, self.baud_rate, 'noipdefault', 33 | 'usepeerdns', 'persist', 'noauth', 34 | connect=self.connect_script) 35 | 36 | def isConnected(self): 37 | return self._ppp.connected() 38 | 39 | # EFFECTS: Establishes a PPP connection. If this is successful, it will also 40 | # reroute packets to ppp0 interface. 41 | def connect(self, timeout=DEFAULT_PPP_TIMEOUT): 42 | 43 | self.__enforce_no_existing_ppp_session() 44 | 45 | result = self._ppp.connect(timeout=timeout) 46 | 47 | if result : 48 | if not self.route.wait_for_interface(DEFAULT_PPP_INTERFACE, 49 | MAX_PPP_INTERFACE_UP_RETRIES): 50 | self.logger.error('Unable to find interface %s. Disconnecting', 51 | DEFAULT_PPP_INTERFACE) 52 | self._ppp.disconnect() 53 | return False 54 | return True 55 | else: 56 | return False 57 | 58 | 59 | def disconnect(self): 60 | self._ppp.disconnect() 61 | PPP.shut_down_existing_ppp_session(self.logger) 62 | return True 63 | 64 | # EFFECTS: Makes sure that there are no existing PPP instances on the same 65 | # device interface. 66 | def __enforce_no_existing_ppp_session(self): 67 | 68 | pid_list = PPP.check_for_existing_ppp_sessions(self.logger) 69 | 70 | if len(pid_list) > 0: 71 | raise PPPError('Existing PPP session(s) are established by pid(s) %s. Please close/kill these processes first' 72 | % pid_list) 73 | 74 | @staticmethod 75 | def shut_down_existing_ppp_session(logger): 76 | pid_list = PPP.check_for_existing_ppp_sessions(logger) 77 | 78 | # Process this only if it is a valid PID integer. 79 | for pid in pid_list: 80 | logger.info('Killing pid %s that currently have an active PPP session', 81 | pid) 82 | process = psutil.Process(pid) 83 | process.terminate() 84 | # Wait at least 10 seconds for the process to terminate 85 | process.wait(10) 86 | 87 | @staticmethod 88 | def check_for_existing_ppp_sessions(logger): 89 | 90 | existing_ppp_pids = [] 91 | logger.info('Checking for existing PPP sessions') 92 | 93 | for proc in psutil.process_iter(): 94 | try: 95 | pinfo = proc.as_dict(attrs=['pid', 'name']) 96 | except: 97 | raise PPPError('Failed to check for existing PPP sessions') 98 | 99 | if 'pppd' in pinfo['name']: 100 | logger.info('Found existing PPP session on pid: %s', pinfo['pid']) 101 | existing_ppp_pids.append(pinfo['pid']) 102 | 103 | return existing_ppp_pids 104 | 105 | @property 106 | def localIPAddress(self): 107 | return self._ppp.raddr 108 | 109 | @property 110 | def remoteIPAddress(self): 111 | return self._ppp.laddr 112 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/ModemMode/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['ModemMode', 'IPPP', 'PPP'] 2 | from .PPP import PPP 3 | from .ModemMode import ModemMode 4 | from .IPPP import IPPP -------------------------------------------------------------------------------- /Hologram/Network/Modem/ModemMode/pppd.py: -------------------------------------------------------------------------------- 1 | # pppd.py - Modified to satisfy Hologram's PPP requirements. 2 | # 3 | # Copyright (c) 2016 Michael de Villiers 4 | # 5 | # Original Author: Michael de Villiers 6 | # Original source code: https://github.com/cour4g3/python-pppd 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | 10 | import fcntl 11 | import logging 12 | from logging import NullHandler 13 | import os 14 | import re 15 | import signal 16 | import time 17 | import threading 18 | import errno 19 | from subprocess import Popen, PIPE, STDOUT 20 | from Exceptions.HologramError import PPPError, PPPConnectionError 21 | 22 | 23 | __version__ = '1.0.3' 24 | DEFAULT_CONNECT_TIMEOUT = 200 25 | 26 | class PPPConnection: 27 | 28 | def __repr__(self): 29 | return type(self).__name__ 30 | 31 | def __init__(self, *args, **kwargs): 32 | # Logging setup. 33 | self.logger = logging.getLogger(__name__) 34 | 35 | self._laddr = None 36 | self._raddr = None 37 | self.proc = None 38 | 39 | self.output = '' 40 | 41 | self._commands = [] 42 | 43 | # This makes it harder to kill pppd so we're defaulting to it off for now 44 | # It's redudant anyway for the CLI 45 | if kwargs.pop('sudo', False): 46 | sudo_path = kwargs.pop('sudo_path', '/usr/bin/sudo') 47 | if not os.path.isfile(sudo_path) or not os.access(sudo_path, os.X_OK): 48 | raise IOError('%s not found' % sudo_path) 49 | self._commands.append(sudo_path) 50 | 51 | pppd_path = kwargs.pop('pppd_path', '/usr/sbin/pppd') 52 | if not os.path.isfile(pppd_path) or not os.access(pppd_path, os.X_OK): 53 | raise IOError('%s not found' % pppd_path) 54 | 55 | self._commands.append(pppd_path) 56 | 57 | for k, v in kwargs.items(): 58 | self._commands.append(k) 59 | self._commands.append(v) 60 | self._commands.extend(args) 61 | self._commands.append('nodetach') 62 | 63 | 64 | # EFFECTS: Spins out a new thread that connects to the network with a given 65 | # timeout value. Default to DEFAULT_CONNECT_TIMEOUT seconds. 66 | # Returns true if successful, false otherwise. 67 | def connect(self, timeout=DEFAULT_CONNECT_TIMEOUT): 68 | 69 | self.logger.info('Starting pppd') 70 | self.proc = Popen(self._commands, stdout=PIPE, stderr=STDOUT) 71 | 72 | # set stdout to non-blocking 73 | fd = self.proc.stdout.fileno() 74 | fl = fcntl.fcntl(fd, fcntl.F_GETFL) 75 | fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) 76 | 77 | result = False 78 | try: 79 | result = self.waitForPPPSuccess(timeout) 80 | except Exception as e: 81 | self.logger.error(e) 82 | 83 | if not result and self.proc and (self.proc.poll() is None): 84 | self.logger.debug('Killing pppd') 85 | self.proc.send_signal(signal.SIGTERM) 86 | time.sleep(1) 87 | 88 | return result 89 | 90 | 91 | def readFromPPP(self): 92 | try: 93 | pppd_out = self.proc.stdout.read() 94 | if pppd_out is not None: 95 | self.output += pppd_out.decode() 96 | except IOError as e: 97 | if e.errno != errno.EAGAIN: 98 | raise 99 | time.sleep(1) 100 | 101 | 102 | def waitForPPPSuccess(self, timeout): 103 | starttime = time.time() 104 | while (time.time() - starttime) < timeout: 105 | self.readFromPPP() 106 | 107 | if self.laddr is not None and self.raddr is not None: 108 | return True 109 | 110 | if 'Modem hangup' in self.output: 111 | raise PPPError('Modem hangup - possibly due to an unregistered SIM') 112 | elif self.proc.poll(): 113 | raise PPPConnectionError(self.proc.returncode, self.output) 114 | return False 115 | 116 | # EFFECTS: Disconnects from the network. 117 | def disconnect(self): 118 | if self.proc and self.proc.poll() is None: 119 | self.proc.send_signal(signal.SIGTERM) 120 | time.sleep(1) 121 | # Reset the values when we disconnect 122 | self._laddr = None 123 | self._raddr = None 124 | self.proc = None 125 | 126 | 127 | # EFFECTS: Returns true if a cellular connection is established. 128 | def connected(self): 129 | if self.proc and self.proc.poll(): 130 | self.readFromPPP() 131 | if self.proc.returncode not in [0, 5]: 132 | raise PPPConnectionError(self.proc.returncode, self.output) 133 | return False 134 | elif self.laddr is not None and self.raddr is not None: 135 | return True 136 | 137 | return False 138 | 139 | # EFFECTS: Returns the local IP address. 140 | @property 141 | def laddr(self): 142 | if self.proc and not self._laddr: 143 | self.readFromPPP() 144 | result = re.search(r'local IP address ([\d\.]+)', self.output) 145 | if result: 146 | self._laddr = result.group(1) 147 | 148 | return self._laddr 149 | 150 | # EFFECTS: Returns the remote IP address. 151 | @property 152 | def raddr(self): 153 | if self.proc and not self._raddr: 154 | self.readFromPPP() 155 | result = re.search(r'remote IP address ([\d\.]+)', self.output) 156 | if result: 157 | self._raddr = result.group(1) 158 | 159 | return self._raddr 160 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/Nova.py: -------------------------------------------------------------------------------- 1 | # Nova.py - Hologram Python SDK Nova modem interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | 11 | from Hologram.Network.Modem import Modem 12 | from Hologram.Event import Event 13 | 14 | DEFAULT_NOVA_TIMEOUT = 200 15 | 16 | class Nova(Modem): 17 | 18 | def __init__(self, device_name=None, baud_rate='9600', 19 | chatscript_file=None, event=Event()): 20 | 21 | super().__init__(device_name=device_name, baud_rate=baud_rate, 22 | chatscript_file=chatscript_file, event=event) 23 | 24 | def disable_at_sockets_mode(self): 25 | self._at_sockets_available = False 26 | 27 | def enable_at_sockets_mode(self): 28 | self._at_sockets_available = True 29 | 30 | @property 31 | def version(self): 32 | return self._basic_command('I9') 33 | 34 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/NovaM.py: -------------------------------------------------------------------------------- 1 | # NovaM.py - Hologram Python SDK Hologram Nova R404/R410 modem interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016-2018 - Hologram, Inc 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | 11 | from Hologram.Network.Modem.Nova import Nova 12 | from Hologram.Event import Event 13 | from Exceptions.HologramError import NetworkError 14 | from UtilClasses import ModemResult 15 | 16 | DEFAULT_NOVAM_TIMEOUT = 200 17 | 18 | class NovaM(Nova): 19 | 20 | usb_ids = [('05c6', '90b2')] 21 | module = 'option' 22 | syspath = '/sys/bus/usb-serial/drivers/option1/new_id' 23 | 24 | def __init__(self, device_name=None, baud_rate='9600', 25 | chatscript_file=None, event=Event()): 26 | super().__init__(device_name=device_name, baud_rate=baud_rate, 27 | chatscript_file=chatscript_file, event=event) 28 | self._at_sockets_available = True 29 | modem_id = self.modem_id 30 | self.baud_rate = '115200' 31 | if("R404" in modem_id): 32 | self.is_r410 = False 33 | else: 34 | self.is_r410 = True 35 | 36 | def set_network_registration_status(self): 37 | self.command("+CEREG", "2") 38 | 39 | def is_registered(self): 40 | return self.check_registered('+CEREG') 41 | 42 | @property 43 | def description(self): 44 | modemtype = '(R410)' if self.is_r410 else '(R404)' 45 | return 'Hologram Nova US 4G LTE Cat-M1 Cellular USB Modem ' + modemtype 46 | 47 | @property 48 | def location(self): 49 | raise NotImplementedError('The R404 and R410 do not support Cell Locate at this time') 50 | 51 | # same as Modem::connect_socket except with longer timeout 52 | def connect_socket(self, host, port): 53 | at_command_val = "%d,\"%s\",%s" % (self.socket_identifier, host, port) 54 | ok, _ = self.set('+USOCO', at_command_val, timeout=122) 55 | if ok != ModemResult.OK: 56 | self.logger.error('Failed to connect socket') 57 | raise NetworkError('Failed to connect socket') 58 | else: 59 | self.logger.info('Connect socket is successful') 60 | 61 | def reset(self): 62 | self.set('+CFUN', '15') # restart the modem 63 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/Nova_U201.py: -------------------------------------------------------------------------------- 1 | # Nova_U201.py - Hologram Python SDK Nova U201 modem interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | 11 | from Hologram.Network.Modem.Nova import Nova 12 | from Exceptions.HologramError import SerialError 13 | from Hologram.Event import Event 14 | from UtilClasses import Location 15 | from UtilClasses import ModemResult 16 | 17 | DEFAULT_NOVA_U201_TIMEOUT = 200 18 | 19 | class Nova_U201(Nova): 20 | usb_ids = [('1546','1102'),('1546','1104')] 21 | 22 | def __init__(self, device_name=None, baud_rate='9600', 23 | chatscript_file=None, event=Event()): 24 | 25 | super().__init__(device_name=device_name, baud_rate=baud_rate, 26 | chatscript_file=chatscript_file, event=event) 27 | # We need to enforce multi serial port support. We then reinstantiate 28 | # the serial interface with the correct device name. 29 | self.enforce_nova_modem_mode() 30 | self._at_sockets_available = True 31 | self.last_sim_otp_command_response = None 32 | self.last_location = None 33 | 34 | def connect(self, timeout=DEFAULT_NOVA_U201_TIMEOUT): 35 | 36 | success = super().connect(timeout) 37 | 38 | # put serial mode on other port 39 | if success is True: 40 | # detect another open serial port to use for PPP 41 | devices = self.detect_usable_serial_port() 42 | if not devices: 43 | raise SerialError('Not enough serial ports detected for Nova') 44 | self.device_name = devices[0] 45 | super().initialize_serial_interface() 46 | 47 | return success 48 | 49 | def create_socket(self): 50 | self._set_up_pdp_context() 51 | super().create_socket() 52 | 53 | def close_socket(self, socket_identifier=None): 54 | super().close_socket(socket_identifier) 55 | self._tear_down_pdp_context() 56 | 57 | def is_registered(self): 58 | return self.check_registered('+CREG') or self.check_registered('+CGREG') 59 | 60 | # EFFECTS: Enforces that the Nova modem be in the correct mode to support multiple 61 | # serial ports 62 | def enforce_nova_modem_mode(self): 63 | 64 | modem_mode = self.modem_mode 65 | self.logger.debug('USB modem mode: ' + str(modem_mode)) 66 | 67 | # Set the modem mode to 0 if necessary. 68 | if modem_mode == 2: 69 | self.modem_mode = 0 70 | devices = self.detect_usable_serial_port() 71 | self.device_name = devices[0] 72 | super().initialize_serial_interface() 73 | 74 | def set_network_registration_status(self): 75 | self.command("+CREG", "2") 76 | self.command("+CGREG", "2") 77 | 78 | # EFFECTS: Parses and populates the last sim otp response. 79 | def parse_and_populate_last_sim_otp_response(self, response): 80 | self.last_sim_otp_command_response = response.split(',')[-1].strip('"') 81 | 82 | # EFFECTS: Returns the sim otp response from the sim 83 | def get_sim_otp_response(self, command): 84 | 85 | self.command("+CSIM=46,\"008800801110" + command + "00\"", hide=True) 86 | 87 | while self.last_sim_otp_command_response is None: 88 | self.checkURC(hide=True) 89 | 90 | return self.last_sim_otp_command_response 91 | 92 | # EFFECTS: Handles URC related AT command responses. 93 | def handleURC(self, urc): 94 | if urc.startswith('+CSIM: '): 95 | self.parse_and_populate_last_sim_otp_response(urc.lstrip('+CSIM: ')) 96 | return 97 | 98 | super().handleURC(urc) 99 | 100 | def populate_location_obj(self, response): 101 | response_list = response.split(',') 102 | self.last_location = Location(*response_list) 103 | return self.last_location 104 | 105 | def _handle_location_urc(self, urc): 106 | self.populate_location_obj(urc.lstrip('+UULOC: ')) 107 | self.event.broadcast('location.received') 108 | 109 | @property 110 | def location(self): 111 | temp_loc = self.last_location 112 | if self._set_up_pdp_context(): 113 | self.last_location = None 114 | ok, r = self.set('+ULOC', '2,2,0,10,10') 115 | if ok != ModemResult.OK: 116 | self.logger.error('Location request failed') 117 | return None 118 | while self.last_location is None and self._is_pdp_context_active(): 119 | self.checkURC() 120 | if self.last_location is None: 121 | self.last_location = temp_loc 122 | return self.last_location 123 | 124 | @property 125 | def description(self): 126 | return 'Hologram Nova Global 3G/2G Cellular USB Modem (U201)' 127 | 128 | @property 129 | def operator(self): 130 | op = self._basic_set('+UDOPN','12') 131 | if op is not None: 132 | return op.strip('"') 133 | return op -------------------------------------------------------------------------------- /Hologram/Network/Modem/Quectel.py: -------------------------------------------------------------------------------- 1 | # Quectel.py - Hologram Python SDK Quectel modem interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | import time 11 | import binascii 12 | 13 | from serial.serialutil import Timeout 14 | 15 | from Hologram.Network.Modem import Modem 16 | from Hologram.Event import Event 17 | from UtilClasses import ModemResult 18 | from Exceptions.HologramError import SerialError, NetworkError 19 | 20 | class Quectel(Modem): 21 | 22 | def __init__(self, device_name=None, baud_rate='9600', chatscript_file=None, 23 | event=Event(), apn='hologram', pdp_context=1): 24 | 25 | super().__init__(device_name=device_name, baud_rate=baud_rate, chatscript_file=chatscript_file, 26 | event=event, apn=apn, pdp_context=pdp_context) 27 | self._at_sockets_available = True 28 | self.urc_response = '' 29 | 30 | def send_message(self, data, timeout=Modem.DEFAULT_SEND_TIMEOUT): 31 | # Waiting for the open socket urc 32 | while self.urc_state != Modem.SOCKET_WRITE_STATE: 33 | self.checkURC() 34 | 35 | self.write_socket(data) 36 | 37 | loop_timeout = Timeout(timeout) 38 | while self.urc_state != Modem.SOCKET_SEND_READ: 39 | self.checkURC() 40 | if self.urc_state != Modem.SOCKET_SEND_READ: 41 | if loop_timeout.expired(): 42 | raise SerialError('Timeout occurred waiting for message status') 43 | time.sleep(self._RETRY_DELAY) 44 | elif self.urc_state == Modem.SOCKET_CLOSED: 45 | return '[1,0]' #this is connection closed for hologram cloud response 46 | 47 | return self.urc_response.rstrip('\r\n') 48 | 49 | def create_socket(self): 50 | self._set_up_pdp_context() 51 | 52 | def connect_socket(self, host, port): 53 | self.command('+QIOPEN', '1,0,\"TCP\",\"%s\",%d,0,1' % (host, port)) 54 | # According to the Quectel Docs 55 | # Have to wait for URC response “+QIOPEN: ,” 56 | 57 | def close_socket(self, socket_identifier=None): 58 | ok, _ = self.command('+QICLOSE', self.socket_identifier) 59 | if ok != ModemResult.OK: 60 | self.logger.error('Failed to close socket') 61 | self.urc_state = Modem.SOCKET_CLOSED 62 | self._tear_down_pdp_context() 63 | 64 | def write_socket(self, data): 65 | hexdata = binascii.hexlify(data) 66 | # We have to do it in chunks of 510 since 512 is actually too long (CMEE error) 67 | # and we need 2n chars for hexified data 68 | for chunk in self._chunks(hexdata, 510): 69 | value = '%d,\"%s\"' % (self.socket_identifier, chunk.decode()) 70 | ok, _ = self.set('+QISENDEX', value, timeout=10) 71 | if ok != ModemResult.OK: 72 | self.logger.error('Failed to write to socket') 73 | raise NetworkError('Failed to write to socket') 74 | 75 | def read_socket(self, socket_identifier=None, payload_length=None): 76 | 77 | if socket_identifier is None: 78 | socket_identifier = self.socket_identifier 79 | 80 | if payload_length is None: 81 | payload_length = self.last_read_payload_length 82 | 83 | ok, resp = self.set('+QIRD', '%d,%d' % (socket_identifier, payload_length)) 84 | if ok == ModemResult.OK: 85 | resp = resp.lstrip('+QIRD: ') 86 | if resp is not None: 87 | resp = resp.strip('"') 88 | try: 89 | resp = resp.decode() 90 | except: 91 | # This is some sort of binary data that can't be decoded so just 92 | # return the bytes. We might want to make this happen via parameter 93 | # in the future so it is more deterministic 94 | self.logger.debug('Could not decode recieved data') 95 | 96 | return resp 97 | 98 | def listen_socket(self, port): 99 | # No equivilent exists for quectel modems 100 | pass 101 | 102 | def is_registered(self): 103 | return self.check_registered('+CREG') or self.check_registered('+CEREG') 104 | 105 | # EFFECTS: Handles URC related AT command responses. 106 | def handleURC(self, urc): 107 | if urc.startswith('+QIOPEN: '): 108 | response_list = urc.lstrip('+QIOPEN: ').split(',') 109 | socket_identifier = int(response_list[0]) 110 | err = int(response_list[-1]) 111 | if err == 0: 112 | self.urc_state = Modem.SOCKET_WRITE_STATE 113 | self.socket_identifier = socket_identifier 114 | else: 115 | self.logger.error('Failed to open socket') 116 | raise NetworkError('Failed to open socket') 117 | return 118 | if urc.startswith('+QIURC: '): 119 | response_list = urc.lstrip('+QIURC: ').split(',') 120 | urctype = response_list[0] 121 | if urctype == '\"recv\"': 122 | self.urc_state = Modem.SOCKET_SEND_READ 123 | self.socket_identifier = int(response_list[1]) 124 | self.last_read_payload_length = int(response_list[2]) 125 | self.urc_response = self._readline_from_serial_port(5) 126 | if urctype == '\"closed\"': 127 | self.urc_state = Modem.SOCKET_CLOSED 128 | self.socket_identifier = int(response_list[-1]) 129 | return 130 | super().handleURC(urc) 131 | 132 | def _is_pdp_context_active(self): 133 | if not self.is_registered(): 134 | return False 135 | 136 | ok, r = self.command('+QIACT?') 137 | if ok == ModemResult.OK: 138 | try: 139 | pdpstatus = int(r.lstrip('+QIACT: ').split(',')[1]) 140 | # 1: PDP active 141 | return pdpstatus == 1 142 | except (IndexError, ValueError) as e: 143 | self.logger.error(repr(e)) 144 | except AttributeError as e: 145 | self.logger.error(repr(e)) 146 | return False 147 | 148 | def set_network_registration_status(self): 149 | self.command("+CREG", "2") 150 | self.command("+CEREG", "2") 151 | 152 | def _set_up_pdp_context(self): 153 | if self._is_pdp_context_active(): return True 154 | self.logger.info('Setting up PDP context') 155 | self.set('+QICSGP', f'{self._pdp_context},1,\"{self._apn}\",\"\",\"\",1') 156 | ok, _ = self.set('+QIACT', f'{self._pdp_context}', timeout=30) 157 | if ok != ModemResult.OK: 158 | self.logger.error('PDP Context setup failed') 159 | raise NetworkError('Failed PDP context setup') 160 | else: 161 | self.logger.info('PDP context active') -------------------------------------------------------------------------------- /Hologram/Network/Modem/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['Modem', 'MockModem', 'MS2131', 'Nova', 'E303', 'E372', 'Quectel'] 2 | 3 | from .IModem import IModem 4 | from .Modem import Modem 5 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/chatscripts/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /Hologram/Network/Modem/chatscripts/default-script: -------------------------------------------------------------------------------- 1 | # Chat script for modems using Hologram SIM card 2 | # See hologram.io for more information 3 | 4 | ABORT 'BUSY' 5 | ABORT 'NO CARRIER' 6 | ABORT 'VOICE' 7 | ABORT 'NO DIALTONE' 8 | ABORT 'NO DIAL TONE' 9 | ABORT 'NO ANSWER' 10 | ABORT 'DELAYED' 11 | TIMEOUT 12 12 | REPORT CONNECT 13 | 14 | "" AT 15 | OK ATH 16 | OK ATZ 17 | OK ATQ0 18 | OK AT+CGDCONT=1,"IP","hologram" 19 | OK ATDT*99***1# 20 | CONNECT '' 21 | 22 | -------------------------------------------------------------------------------- /Hologram/Network/Network.py: -------------------------------------------------------------------------------- 1 | # Network.py - Hologram Python SDK Network interface 2 | # 3 | # Author: Hologram 4 | # 5 | # Copyright 2016 - Hologram (Konekt, Inc.) 6 | # 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | 11 | from Hologram.Event import Event 12 | import logging 13 | from logging import NullHandler 14 | from enum import Enum 15 | 16 | 17 | class NetworkScope(Enum): 18 | SYSTEM = 1 19 | HOLOGRAM = 2 20 | 21 | 22 | class Network: 23 | 24 | def __repr__(self): 25 | return type(self).__name__ 26 | 27 | def __init__(self, event=Event()): 28 | self.event = event 29 | # Logging setup. 30 | self.logger = logging.getLogger(__name__) 31 | self.logger.addHandler(NullHandler()) 32 | self.scope = NetworkScope.SYSTEM 33 | 34 | def connect(self): 35 | self.event.broadcast('network.connected') 36 | return True 37 | 38 | def disconnect(self): 39 | self.event.broadcast('network.disconnected') 40 | 41 | def reconnect(self): 42 | raise NotImplementedError('Must instantiate a defined Network type') 43 | 44 | def getConnectionStatus(self): 45 | raise NotImplementedError('Must instantiate a defined Network type') 46 | 47 | def getSignalStrength(self): 48 | raise NotImplementedError('Must instantiate a defined Network type') 49 | 50 | def getAvgSignalStrength(self): 51 | raise NotImplementedError('Must instantiate a defined Network type') 52 | 53 | def is_connected(self): 54 | return False 55 | 56 | @property 57 | def interfaceName(self): 58 | return self._interfaceName 59 | 60 | @interfaceName.setter 61 | def interfaceName(self, name): 62 | self._interfaceName = name 63 | -------------------------------------------------------------------------------- /Hologram/Network/NetworkManager.py: -------------------------------------------------------------------------------- 1 | # NetworkManager.py - Hologram Python SDK Network manager interface 2 | # This handles the network/connectivity interface for Hologram SDK. 3 | # 4 | # Author: Hologram 5 | # 6 | # Copyright 2016 - Hologram (Konekt, Inc.) 7 | # 8 | # 9 | # LICENSE: Distributed under the terms of the MIT License 10 | # 11 | 12 | from Hologram.Network import Wifi, Ethernet, BLE, Cellular 13 | from typing import Union 14 | from Hologram.Network.Modem.Modem import Modem 15 | from Exceptions.HologramError import NetworkError 16 | import logging 17 | from logging import NullHandler 18 | import os 19 | 20 | DEFAULT_NETWORK_TIMEOUT = 200 21 | 22 | class NetworkManager: 23 | 24 | _networkHandlers = { 25 | 'wifi' : Wifi.Wifi, 26 | 'cellular': Cellular.Cellular, 27 | 'ble' : BLE.BLE, 28 | 'ethernet' : Ethernet.Ethernet, 29 | } 30 | 31 | def __init__(self, event, network_name, modem: Union[None, Modem] = None): 32 | 33 | # Logging setup. 34 | self.logger = logging.getLogger(__name__) 35 | self.logger.addHandler(NullHandler()) 36 | 37 | self.event = event 38 | self.networkActive = False 39 | self.init_network(network_name, modem) 40 | 41 | # EFFECTS: Event handler function that sets the network disconnect flag. 42 | def networkDisconnected(self): 43 | self.networkActive = False 44 | 45 | def networkConnected(self): 46 | self.networkActive = True 47 | 48 | def listAvailableInterfaces(self): 49 | return self._networkHandlers.keys() 50 | 51 | @property 52 | def network(self): 53 | return self._network 54 | 55 | def init_network(self, network, modem: Union[None, Modem] = None): 56 | if not network: # non-network mode 57 | self.networkConnected() 58 | self._network = None 59 | elif network not in self._networkHandlers: 60 | raise NetworkError('Invalid network type: %s' % network) 61 | else: 62 | self.__enforce_network_privileges() 63 | self._network = self._networkHandlers[network](self.event) 64 | if network == 'cellular': 65 | if modem is not None: 66 | self._network.modem = modem 67 | else: 68 | try: 69 | self._network.autodetect_modem() 70 | except NetworkError as e: 71 | self.logger.info("No modem found. Loading drivers and retrying") 72 | self._network.load_modem_drivers() 73 | self._network.autodetect_modem() 74 | 75 | 76 | def __repr__(self): 77 | if not self.network: 78 | return 'Network Agnostic Mode' 79 | 80 | return type(self.network).__name__ 81 | 82 | def __enforce_network_privileges(self): 83 | 84 | if os.geteuid() != 0: 85 | raise RuntimeError('You need to have root privileges to use this interface. Please try again, this time using sudo.') 86 | -------------------------------------------------------------------------------- /Hologram/Network/Route.py: -------------------------------------------------------------------------------- 1 | # Route.py - Hologram Python SDK Routing Manager 2 | # This module configures routing for Hologram SDK. 3 | # 4 | # Author: Hologram 5 | # 6 | # Copyright 2016 - Hologram (Konekt, Inc.) 7 | # 8 | # 9 | # LICENSE: Distributed under the terms of the MIT License 10 | # 11 | 12 | import logging 13 | import time 14 | from logging import NullHandler 15 | from pyroute2 import IPRoute 16 | from pyroute2.netlink.exceptions import NetlinkError 17 | 18 | DEFAULT_DESTINATION = '0.0.0.0/0' 19 | 20 | 21 | class Route: 22 | def __init__(self): 23 | self.logger = logging.getLogger(__name__) 24 | self.logger.addHandler(NullHandler()) 25 | 26 | def is_interface_available(self, interface): 27 | # An interface is considered available if it is simply on the ip route list. 28 | # The interface does not need to be UP in order to be considered available. 29 | return self.__interface_index(interface) is not None 30 | 31 | def wait_for_interface(self, interface, max_retries): 32 | count = 0 33 | while count <= max_retries: 34 | try: 35 | # Check if ready to break out of loop when interface is found. 36 | if self.is_interface_available(interface): 37 | # NOTE: Ideally this conditional would be based on 38 | # self.is_interface_up(interface), but there is an issue 39 | # where the state of a ppp0 interface may show UNKNOWN 40 | # on Raspbian linux even if ppp0 is UP. 41 | return True 42 | else: 43 | self.logger.info('Waiting for interface %s:', interface) 44 | time.sleep(1) 45 | count += 1 46 | except Exception as e: 47 | pass 48 | if count > max_retries: 49 | return False 50 | 51 | def add_default(self, gateway): 52 | try: 53 | self.add(DEFAULT_DESTINATION, gateway) 54 | except NetlinkError as e: 55 | self.logger.debug('Could not set default route due to NetlinkError: %s', str(e)) 56 | 57 | def add(self, destination, gateway): 58 | self.logger.debug('Adding Route %s : %s', destination, gateway) 59 | with IPRoute() as ipr: 60 | ipr.route('add', 61 | dst=destination, 62 | gateway=gateway) 63 | 64 | def delete_default(self, gateway): 65 | try: 66 | self.delete(DEFAULT_DESTINATION, gateway) 67 | except NetlinkError as e: 68 | self.logger.debug('Could not set default route due to NetlinkError: %s', str(e)) 69 | 70 | def delete(self, destination, gateway): 71 | self.logger.debug('Removing Route %s : %s', destination, gateway) 72 | try: 73 | with IPRoute() as ipr: 74 | ipr.route('del', 75 | dst=destination, 76 | gateway=gateway) 77 | except NetlinkError as e: 78 | self.logger.debug('Could not delete route due to NetlinkError: %s', str(e)) 79 | 80 | 81 | def __interface_index(self, interface): 82 | index = None 83 | with IPRoute() as ipr: 84 | indexes = ipr.link_lookup(ifname=interface) 85 | if len(indexes) == 1: 86 | index = indexes[0] 87 | return index 88 | 89 | def __get_interface_state(self, interface): 90 | if self.is_interface_available(interface): 91 | link_state = None 92 | ipr_index = self.__interface_index(interface) 93 | with IPRoute() as ipr: 94 | links = ipr.get_links() 95 | 96 | for link in links: 97 | if link['index'] == ipr_index: 98 | link_state = link.get_attr('IFLA_OPERSTATE') 99 | break 100 | return link_state 101 | else: 102 | return None 103 | -------------------------------------------------------------------------------- /Hologram/Network/Wifi.py: -------------------------------------------------------------------------------- 1 | from Hologram.Network import Network 2 | from Hologram.Event import Event 3 | import time 4 | import os 5 | 6 | class Wifi(Network): 7 | 8 | def __init__(self, interfaceName = 'wlan0'): 9 | self.interfaceName = interfaceName 10 | # self.wifi = Wireless(interfaceName) 11 | super().__init__() 12 | self.event.broadcast('wifi.connected') 13 | 14 | def getSSID(self): 15 | return '' #self.wifi.getEssid() 16 | 17 | def getMode(self): 18 | return '' # return self.wifi.getMode() 19 | 20 | def getWirelessName(self): 21 | return '' # return self.wifi.getWirelessName() 22 | 23 | def getBitRate(self): 24 | # bitrate = self.wifi.wireless_info.getBitrate() 25 | # return "Bit Rate :%s" % self.wifi.getBitrate() 26 | return '' 27 | 28 | def getAvgSignalStrength(self): 29 | # mq = self.wifi.getQualityAvg() 30 | # return "quality: " + str(mq.quality) + " signal: " + str(mq.siglevel) \ 31 | # + " noise: " + str(mq.nlevel) 32 | return '' 33 | 34 | def getMaxSignalStrength(self): 35 | # mq = self.wifi.getQualityMax() 36 | # return "quality: " + str(mq.quality) + " signal: " + str(mq.siglevel) \ 37 | # + " noise: " + str(mq.nlevel) 38 | return '' 39 | 40 | def disconnect(self): 41 | os.system("ifconfig " + self.interfaceName + " down") 42 | self.event.broadcast('wifi.disconnected') 43 | super().disconnect() 44 | 45 | def connect(self): 46 | os.system("ifconfig " + self.interfaceName + " up") 47 | self.event.broadcast('wifi.connected') 48 | super().connect() 49 | 50 | # EFFECTS: Returns the AP address. 51 | def getAPAddress(self): 52 | self.disconnect() 53 | time.sleep(5) 54 | self.connect() 55 | # return self.wifi.getAPaddr() 56 | 57 | def getConnectionStatus(self): 58 | raise Exception('WiFi mode doesn\'t support this call yet') 59 | 60 | def setAPAddress(self, ap): 61 | try: 62 | self.wifi.setAPaddr(ap) 63 | except: 64 | raise Exception('Unable to set AP address to ' + str(ap)) 65 | 66 | def isConnected(self): 67 | return True 68 | -------------------------------------------------------------------------------- /Hologram/Network/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['Cellular', 'BLE', 'Wifi', 'Ethernet', 'Network'] 2 | 3 | from .Network import Network, NetworkScope 4 | -------------------------------------------------------------------------------- /Hologram/__init__.py: -------------------------------------------------------------------------------- 1 | from Hologram import * 2 | from Hologram.Event import * 3 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | If you run into SDK issues, please go [here](https://community.hologram.io/c/hardware/sdk) for help and support: 2 | 3 | You should open a GitHub issue only if it is a bug or a feature request. 4 | 5 | ------------------------ 6 | 7 | 8 | ### Describe the problem 9 | 10 | 11 | ### Expected behavior 12 | Please describe what you think the program should be doing here. 13 | 14 | 15 | ### Actual behavior 16 | Please provide any useful debug logs/output here. This will help us diagnose 17 | problems that you might have. Full stack traces can be extremely helpful here. 18 | 19 | 20 | ### Steps to reproduce the behavior 21 | Please provide the exact command(s) to reproduce the error. If it's non-deterministic, 22 | try your best to describe your observations that might help us troubleshoot the error. 23 | 24 | ### System information 25 | - **OS Platform and Distribution (e.g., Linux Ubuntu 16.04)**: 26 | - **Python SDK installed via PyPI or GitHub**: 27 | - **SDK version (use command below)**: 28 | - **Python version**: 29 | - **Hardware (modem) model**: 30 | 31 | We will be adding an environment capture script soon for your convenience. 32 | 33 | You can obtain the Python SDK version with: 34 | `hologram version` 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Hologram (Konekt, Inc.) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include MANIFEST.in 3 | include Makefile 4 | include requirements.txt 5 | include version.txt 6 | include credentials.json 7 | include install.sh 8 | include *.md 9 | include *.py 10 | include Hologram/Network/Modem/chatscripts/default-script 11 | recursive-include Hologram *.py 12 | recursive-include Exceptions *.py 13 | recursive-include UtilClasses *.py 14 | recursive-include examples *.py 15 | recursive-include tests *.py 16 | recursive-include scripts *.py 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install test clean 2 | 3 | install: 4 | python setup.py install 5 | 6 | test: 7 | pytest tests/ 8 | 9 | clean: 10 | find . -name '*.pyc' -exec rm --force {} + 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hologram-python 2 | 3 | [![PyPI version](https://badge.fury.io/py/hologram-python.svg)](https://badge.fury.io/py/hologram-python) 4 | [![Python package](https://github.com/hologram-io/hologram-python/workflows/Python%20package/badge.svg)](https://github.com/hologram-io/hologram-python/actions) 5 | [![Coverage Status](https://coveralls.io/repos/github/hologram-io/hologram-python/badge.svg?branch=master)](https://coveralls.io/github/hologram-io/hologram-python?branch=master) 6 | 7 | ## Introduction 8 | The Hologram Python Device SDK provides a simple way for devices to connect 9 | and communicate with the Hologram or other IoT platforms. You can activate, provision, 10 | send messages, receive inbound access connections, send/receive SMS, and 11 | setup secure tunnels. 12 | 13 | The SDK also supports networking interfaces for WiFi in addition to Cellular 14 | in the spirit of bringing connectivity to your devices. 15 | 16 | ## Installation 17 | 18 | ### Requirements: 19 | 20 | You will need `ppp` and Python 3.9 installed on your system for the SDK to work. 21 | 22 | We wrote scripts to ease the installation process. 23 | 24 | Please run this command to download the script that installs the Python SDK: 25 | 26 | `curl -L hologram.io/python-install | bash` 27 | 28 | Please run this command to download the script that updates the Python SDK: 29 | 30 | `curl -L hologram.io/python-update | bash` 31 | 32 | If everything goes well, you’re done and ready to use the SDK. 33 | 34 | ## Directory Structure 35 | 36 | 1. `tests` - This contains many of Hologram SDK unit tests. 37 | 2. `scripts` - This directory contains example Python scripts that utilize the Python SDK. 38 | 3. `Hologram` - This directory contains all the Hologram class interfaces. 39 | 4. `Exceptions` - This directory stores our custom `Exception` used in the SDK. 40 | 41 | You can also find more documentation [here](https://hologram.io/docs/reference/cloud/python-sdk). 42 | 43 | ## Support 44 | Please feel free to [reach out to us](mailto:support@hologram.io) if you have any questions/concerns. 45 | -------------------------------------------------------------------------------- /UtilClasses/UtilClasses.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # UtilClasses.py - Hologram Python SDK Util classes 3 | # 4 | # Author: Hologram 5 | # 6 | # Copyright 2016 - Hologram (Konekt, Inc.) 7 | # 8 | # 9 | # LICENSE: Distributed under the terms of the MIT License 10 | # 11 | 12 | import threading 13 | 14 | class Location: 15 | 16 | def __init__(self, date=None, time=None, latitude=None, longitude=None, 17 | altitude=None, uncertainty=None): 18 | self.date = date 19 | self.time = time 20 | self.latitude = latitude 21 | self.longitude = longitude 22 | self.altitude = altitude 23 | self.uncertainty = uncertainty 24 | 25 | def __repr__(self): 26 | return type(self).__name__ 27 | 28 | class SMS: 29 | 30 | def __init__(self, sender, timestamp, message): 31 | self.sender = sender 32 | self.timestamp = timestamp 33 | self.message = message 34 | 35 | def __repr__(self): 36 | 37 | temp_str = type(self).__name__ + ': ' 38 | temp_str = temp_str + 'sender: ' + self.sender + ', ' 39 | temp_str = temp_str + 'timestamp: ' + self.timestamp.strftime('%c') + ', ' 40 | temp_str = temp_str + 'message: ' + self.message 41 | 42 | return temp_str 43 | 44 | class ModemResult: 45 | Invalid = 'Invalid' 46 | NoMatch = 'NoMatch' 47 | Error = 'Error' 48 | Timeout = 'Timeout' 49 | OK = 'OK' 50 | 51 | class RWLock: 52 | 53 | def __init__(self): 54 | self.mutex = threading.Condition() 55 | self.readers = 0 56 | self.writer = False 57 | 58 | def acquire(self): 59 | self.writer_acquire() 60 | 61 | def release(self): 62 | self.writer_release() 63 | 64 | def reader_acquire(self): 65 | self.mutex.acquire() 66 | while self.writer: 67 | self.mutex.wait() 68 | self.readers += 1 69 | self.mutex.release() 70 | 71 | def reader_release(self): 72 | self.mutex.acquire() 73 | self.readers -= 1 74 | if self.readers == 0: 75 | self.mutex.notifyAll() 76 | self.mutex.release() 77 | 78 | def writer_acquire(self): 79 | self.mutex.acquire() 80 | while self.writer or self.readers > 0: 81 | self.mutex.wait() 82 | self.writer = True 83 | self.mutex.release() 84 | 85 | def writer_release(self): 86 | self.mutex.acquire() 87 | self.writer = False 88 | self.mutex.notifyAll() 89 | self.mutex.release() 90 | -------------------------------------------------------------------------------- /UtilClasses/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['UtilClasses'] 2 | from .UtilClasses import Location, ModemResult, SMS, RWLock 3 | -------------------------------------------------------------------------------- /credentials-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | // Hologram device key (8 characters long) 3 | "devicekey": "xxxxxxxx" 4 | } 5 | -------------------------------------------------------------------------------- /examples/example-average-max-signal-strength.py: -------------------------------------------------------------------------------- 1 | # 2 | # example-average-max-signal-strength.py - Example of getting average and max signal strength. 3 | # 4 | # Author: Hologram 5 | # 6 | # Copyright 2016 - Hologram (Konekt, Inc.) 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | 11 | import sys 12 | import time 13 | 14 | sys.path.append(".") 15 | sys.path.append("..") 16 | sys.path.append("../..") 17 | 18 | from Hologram.HologramCloud import HologramCloud 19 | 20 | if __name__ == "__main__": 21 | print("") 22 | print("") 23 | print("Testing Hologram Cloud class...") 24 | print("") 25 | print("* Note: You can obtain device keys from the Devices page") 26 | print("* at https://dashboard.hologram.io") 27 | print("") 28 | 29 | hologram = HologramCloud(None, network='cellular') 30 | 31 | sum_RSSI = 0.0 32 | sum_quality = 0.0 33 | num_samples = 5 34 | 35 | # Query for signal strength every 2 seconds... 36 | for i in range(num_samples): 37 | signal_strength = hologram.network.signal_strength 38 | print('Signal strength: ' + signal_strength) 39 | rssi, qual = signal_strength.split(',') 40 | sum_RSSI = sum_RSSI + int(rssi) 41 | sum_quality = sum_quality + int(qual) 42 | time.sleep(2) 43 | 44 | print('Average RSSI over ' + str(num_samples) + ' samples: ' + str(sum_RSSI/num_samples)) 45 | print('Average quality over ' + str(num_samples) + ' samples: ' + str(sum_quality/num_samples)) 46 | -------------------------------------------------------------------------------- /examples/example-cellular-send.py: -------------------------------------------------------------------------------- 1 | # 2 | # example-cellular-send.py - Example of using a supported modem to send messages 3 | # to the Hologram Cloud. 4 | # 5 | # Author: Hologram 6 | # 7 | # Copyright 2016 - Hologram (Konekt, Inc.) 8 | # 9 | # LICENSE: Distributed under the terms of the MIT License 10 | # 11 | 12 | import sys 13 | 14 | sys.path.append(".") 15 | sys.path.append("..") 16 | sys.path.append("../..") 17 | 18 | from Hologram.HologramCloud import HologramCloud 19 | 20 | if __name__ == "__main__": 21 | print("") 22 | print("") 23 | print("Testing Hologram Cloud class...") 24 | print("") 25 | print("* Note: You can obtain device keys from the Devices page") 26 | print("* at https://dashboard.hologram.io") 27 | print("") 28 | 29 | device_key = input("What is your device key? ") 30 | 31 | credentials = {'devicekey': device_key} 32 | 33 | hologram = HologramCloud(credentials, network='cellular') 34 | 35 | result = hologram.network.connect() 36 | if result == False: 37 | print('Failed to connect to cell network') 38 | 39 | print('Cloud type: ' + str(hologram)) 40 | 41 | print('Network type: ' + str(hologram.network_type)) 42 | 43 | recv = hologram.sendMessage("one two three!", 44 | topics = ["TOPIC1","TOPIC2"], 45 | timeout = 3) 46 | 47 | print('RESPONSE MESSAGE: ' + hologram.getResultString(recv)) 48 | 49 | print('LOCAL IP ADDRESS: ' + str(hologram.network.localIPAddress)) 50 | print('REMOTE IP ADDRESS: ' + str(hologram.network.remoteIPAddress)) 51 | 52 | hologram.network.disconnect() 53 | -------------------------------------------------------------------------------- /examples/example-hardware-auth.py: -------------------------------------------------------------------------------- 1 | # 2 | # example-cellular-send.py - Example of using a supported modem to send messages 3 | # to the Hologram Cloud. 4 | # 5 | # Author: Hologram 6 | # 7 | # Copyright 2016 - Hologram (Konekt, Inc.) 8 | # 9 | # LICENSE: Distributed under the terms of the MIT License 10 | # 11 | import logging 12 | import sys 13 | 14 | sys.path.append(".") 15 | sys.path.append("..") 16 | sys.path.append("../..") 17 | 18 | from Hologram.HologramCloud import HologramCloud 19 | 20 | if __name__ == "__main__": 21 | print("") 22 | print("") 23 | print("Testing Hologram Cloud class...") 24 | print("") 25 | print("* Note: You can obtain device keys from the Devices page") 26 | print("* at https://dashboard.hologram.io") 27 | print("") 28 | 29 | logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s") 30 | 31 | hologram = HologramCloud(dict(), authentication_type='sim-otp', 32 | network='cellular') 33 | 34 | result = hologram.network.connect() 35 | if result == False: 36 | print('Failed to connect to cell network') 37 | 38 | recv = hologram.sendMessage("one two three!", 39 | topics = ["TOPIC1","TOPIC2"], 40 | timeout = 5) 41 | 42 | print('RESPONSE MESSAGE: ' + hologram.getResultString(recv)) 43 | 44 | hologram.network.disconnect() 45 | -------------------------------------------------------------------------------- /examples/example-periodic-send.py: -------------------------------------------------------------------------------- 1 | # 2 | # example-hologram-cloud-periodic-send.py - Example for sending periodic messages 3 | # to the Hologram cloud. 4 | # Author: Hologram 5 | # 6 | # Copyright 2016 - Hologram (Konekt, Inc.) 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | 11 | import sys 12 | import time 13 | 14 | sys.path.append(".") 15 | sys.path.append("..") 16 | sys.path.append("../..") 17 | 18 | from Hologram.HologramCloud import HologramCloud 19 | 20 | if __name__ == "__main__": 21 | print("") 22 | print("") 23 | print("Testing Hologram Cloud class...") 24 | print("") 25 | print("* Note: You can obtain device keys from the Devices page") 26 | print("* at https://dashboard.hologram.io") 27 | print("") 28 | 29 | device_key = input("What is your device key? ") 30 | 31 | credentials = {'devicekey': device_key} 32 | 33 | hologram = HologramCloud(credentials, authentication_type='csrpsk') 34 | 35 | print('Sending a periodic message every 30 seconds...') 36 | recv = hologram.sendPeriodicMessage(30, 'This is a periodic message', 37 | topics=['PERIODICMESSAGES'], 38 | timeout=6) 39 | 40 | print('sleeping for 40 seconds...') 41 | time.sleep(40) 42 | print('waking up!') 43 | 44 | hologram.stopPeriodicMessage() 45 | 46 | print('') 47 | print('Testing complete.') 48 | print('') 49 | -------------------------------------------------------------------------------- /examples/example-receive-sms-at-commands.py: -------------------------------------------------------------------------------- 1 | # 2 | # example-sms-at_commands.py - Example of using AT command socket interfaces to send 3 | # SMS. 4 | # 5 | # Author: Hologram 6 | # 7 | # Copyright 2016 - Hologram (Konekt, Inc.) 8 | # 9 | # LICENSE: Distributed under the terms of the MIT License 10 | # 11 | 12 | import sys 13 | import time 14 | 15 | sys.path.append(".") 16 | sys.path.append("..") 17 | sys.path.append("../..") 18 | 19 | from Hologram.HologramCloud import HologramCloud 20 | 21 | def popReceivedMessage(): 22 | recv = hologram.popReceivedMessage() 23 | if recv is not None: 24 | print('Received message: ' + str(recv)) 25 | 26 | def handle_polling(timeout, fx, delay_interval=0): 27 | try: 28 | if timeout != -1: 29 | print('waiting for ' + str(timeout) + ' seconds...') 30 | end = time.time() + timeout 31 | while time.time() < end: 32 | fx() 33 | time.sleep(delay_interval) 34 | else: 35 | while True: 36 | fx() 37 | time.sleep(delay_interval) 38 | except KeyboardInterrupt as e: 39 | sys.exit(e) 40 | 41 | if __name__ == "__main__": 42 | print("") 43 | print("") 44 | print("Testing Hologram Cloud class...") 45 | print("") 46 | print("* Note: You can obtain device keys from the Devices page") 47 | print("* at https://dashboard.hologram.io") 48 | print("") 49 | 50 | device_key = input("What is your device key? ") 51 | 52 | credentials = {'devicekey': device_key} 53 | 54 | hologram = HologramCloud(credentials, network='cellular', authentication_type='csrpsk') 55 | 56 | hologram.openReceiveSocket() 57 | print ('Ready to receive data...') 58 | 59 | handle_polling(40, popReceivedMessage, 1) 60 | 61 | hologram.closeReceiveSocket() -------------------------------------------------------------------------------- /examples/example-send-msg.py: -------------------------------------------------------------------------------- 1 | # 2 | # example-cellular-send.py - Example of using a supported modem to send messages 3 | # to the Hologram Cloud. 4 | # 5 | # Author: Hologram 6 | # 7 | # Copyright 2016 - Hologram (Konekt, Inc.) 8 | # 9 | # LICENSE: Distributed under the terms of the MIT License 10 | # 11 | import sys 12 | 13 | sys.path.append(".") 14 | sys.path.append("..") 15 | sys.path.append("../..") 16 | 17 | from Hologram.HologramCloud import HologramCloud 18 | 19 | if __name__ == "__main__": 20 | print("") 21 | print("") 22 | print("Testing Hologram Cloud class...") 23 | print("") 24 | print("* Note: You can obtain device keys from the Devices page") 25 | print("* at https://dashboard.hologram.io") 26 | print("") 27 | 28 | hologram = HologramCloud(dict(), network='cellular') 29 | 30 | print('Cloud type: ' + str(hologram)) 31 | 32 | recv = hologram.sendMessage('one two three!', 33 | topics = ['TOPIC1','TOPIC2'], 34 | timeout = 3) 35 | 36 | print('RESPONSE MESSAGE: ' + hologram.getResultString(recv)) 37 | -------------------------------------------------------------------------------- /examples/example-serial-interface.py: -------------------------------------------------------------------------------- 1 | # 2 | # example-serial-interface.py - Example of using serial interface with a supported modem. 3 | # 4 | # Author: Hologram 5 | # 6 | # Copyright 2016 - Hologram (Konekt, Inc.) 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | # 10 | 11 | import sys 12 | 13 | sys.path.append(".") 14 | sys.path.append("..") 15 | sys.path.append("../..") 16 | 17 | from Hologram.HologramCloud import HologramCloud 18 | 19 | if __name__ == '__main__': 20 | 21 | hologram = HologramCloud(None, network='cellular') 22 | 23 | print('Signal strength: ' + str(hologram.network.signal_strength)) 24 | print('Modem id: ' + str(hologram.network.modem_id)) 25 | print('IMSI: ' + str(hologram.network.imsi)) 26 | print('ICCID: ' + str(hologram.network.iccid)) 27 | print('Operator: ' + str(hologram.network.operator)) 28 | print('Modem name: ' + str(hologram.network.active_modem_interface)) 29 | 30 | location = hologram.network.location 31 | if location is None: 32 | print('Location: ' + str(location)) 33 | else: 34 | print('Latitude: ' + str(location.latitude)) 35 | print('Longitude: ' + str(location.longitude)) 36 | print('Date: ' + str(location.date)) 37 | -------------------------------------------------------------------------------- /examples/example-sms-at-commands.py: -------------------------------------------------------------------------------- 1 | # 2 | # example-sms-at_commands.py - Example of using AT command socket interfaces to send 3 | # SMS. 4 | # 5 | # Author: Hologram 6 | # 7 | # Copyright 2016 - Hologram (Konekt, Inc.) 8 | # 9 | # LICENSE: Distributed under the terms of the MIT License 10 | # 11 | 12 | import sys 13 | 14 | sys.path.append(".") 15 | sys.path.append("..") 16 | sys.path.append("../..") 17 | 18 | from Hologram.HologramCloud import HologramCloud 19 | 20 | if __name__ == "__main__": 21 | print("") 22 | print("") 23 | print("Testing Hologram Cloud class...") 24 | print("") 25 | print("* Note: You can obtain device keys from the Devices page") 26 | print("* at https://dashboard.hologram.io") 27 | print("") 28 | 29 | device_key = input("What is your device key? ") 30 | destination_number = input("What is your destination number? ") 31 | 32 | credentials = {'devicekey': device_key} 33 | 34 | hologram = HologramCloud(credentials, network='cellular', authentication_type='csrpsk') 35 | 36 | 37 | recv = hologram.sendSMS(destination_number, 'Hi, SMS!') 38 | 39 | print('RESPONSE MESSAGE: ' + hologram.getResultString(recv)) -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Author: Hologram 3 | # 4 | # Copyright 2016 - Hologram (Konekt, Inc.) 5 | # 6 | # LICENSE: Distributed under the terms of the MIT License 7 | # 8 | # install.sh - This file helps install this Python SDK and all required dependencies 9 | # on a machine. 10 | 11 | set -euo pipefail 12 | 13 | # This script will install the Hologram SDK and the necessary software dependencies 14 | # for it to work. 15 | 16 | required_programs=('python3' 'pip3' 'ps' 'kill' 'libpython3.9-dev') 17 | OS='' 18 | 19 | # Check OS. 20 | if [ "$(uname)" == "Darwin" ]; then 21 | 22 | echo 'Darwin system detected' 23 | OS='DARWIN' 24 | 25 | elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then 26 | 27 | echo 'Linux system detected' 28 | OS='LINUX' 29 | required_programs+=('ip' 'pppd') 30 | 31 | elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW32_NT" ]; then 32 | 33 | echo 'Windows 32-bit system detected' 34 | OS='WINDOWS' 35 | 36 | elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW64_NT" ]; then 37 | 38 | echo 'Windows 64-bit system detected' 39 | OS='WINDOWS' 40 | fi 41 | 42 | # Error out on unsupported OS. 43 | if [ "$OS" == 'DARWIN' ] || [ "$OS" == 'WINDOWS' ]; then 44 | echo "$OS is not supported right now" 45 | exit 1 46 | fi 47 | 48 | function pause() { 49 | read -p "$*" 50 | } 51 | 52 | function install_software() { 53 | if [ "$OS" == 'LINUX' ]; then 54 | sudo apt -y install "$*" 55 | elif [ "$OS" == 'DARWIN' ]; then 56 | brew install "$*" 57 | echo 'TODO: macOS should go here' 58 | elif [ "$OS" == 'WINDOWS' ]; then 59 | echo 'TODO: windows should go here' 60 | fi 61 | } 62 | 63 | function check_python_version() { 64 | if ! python3 -V | grep -E '3.(9|1[012]).[0-9]' > /dev/null 2>&1; then 65 | echo "An unsupported version of python 3 is installed. Must have python 3.9+ installed to use the Hologram SDK" 66 | exit 1 67 | fi 68 | } 69 | 70 | # EFFECTS: Returns true if the specified program is installed, false otherwise. 71 | function check_if_installed() { 72 | if command -v "$*" >/dev/null 2>&1; then 73 | return 0 74 | else 75 | return 1 76 | fi 77 | } 78 | 79 | function update_repository() { 80 | if [ "$OS" == 'LINUX' ]; then 81 | sudo apt update 82 | elif [ "$OS" == 'DARWIN' ]; then 83 | brew update 84 | echo 'TODO: macOS should go here' 85 | elif [ "$OS" == 'WINDOWS' ]; then 86 | echo 'TODO: windows should go here' 87 | fi 88 | } 89 | 90 | # EFFECTS: Verifies that all required software is installed. 91 | function verify_installation() { 92 | echo 'Verifying that all dependencies are installed correctly...' 93 | # Verify pip packages 94 | INSTALLED_PIP_PACKAGES="$(pip3 list)" 95 | 96 | if ! [[ "$INSTALLED_PIP_PACKAGES" == *"python-sdk-auth"* ]]; then 97 | echo 'Cannot find python-sdk-auth. Please rerun the install script.' 98 | exit 1 99 | fi 100 | 101 | if ! [[ "$INSTALLED_PIP_PACKAGES" == *"hologram-python"* ]]; then 102 | echo 'Cannot find hologram-python. Please rerun the install script.' 103 | exit 1 104 | fi 105 | 106 | echo 'You are now ready to use the Hologram Python SDK!' 107 | } 108 | 109 | check_python_version 110 | 111 | update_repository 112 | 113 | # Iterate over all programs to see if they are installed 114 | # Installs them if necessary 115 | for program in ${required_programs[*]} 116 | do 117 | if [ "$program" == 'pppd' ]; then 118 | if ! check_if_installed "$program"; then 119 | pause "Installing $program. Press [Enter] key to continue..."; 120 | install_software 'ppp' 121 | fi 122 | elif [ "$program" == 'pip3' ]; then 123 | if ! check_if_installed "$program"; then 124 | pause "Installing $program. Press [Enter] key to continue..."; 125 | install_software 'python3-pip' 126 | fi 127 | if ! pip3 -V | grep -E '3.(9|1[012])' >/dev/null 2>&1; then 128 | echo "pip3 is installed for an unsupported version of python." 129 | exit 1 130 | fi 131 | elif check_if_installed "$program"; then 132 | echo "$program is already installed." 133 | else 134 | pause "Installing $program. Press [Enter] key to continue..."; 135 | install_software "$program" 136 | fi 137 | done 138 | 139 | # Install SDK itself. 140 | sudo pip3 install hologram-python 141 | 142 | verify_installation 143 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | mock~=4.0.3 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyroute2==0.5.* 2 | pyserial~=3.5 3 | python-pppd==1.0.4 4 | python-sdk-auth==0.4.0 5 | pyudev~=0.22.0 6 | pyusb~=1.2.1 7 | psutil~=5.8.0 8 | requests>=2.25.1 9 | -------------------------------------------------------------------------------- /scripts/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /scripts/hologram: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # hologram - Hologram Python SDK command line interface (CLI) 4 | # 5 | # Author: Hologram 6 | # 7 | # Copyright 2016 - Hologram (Konekt, Inc.) 8 | # 9 | # 10 | # LICENSE: Distributed under the terms of the MIT License 11 | 12 | import argparse 13 | from argparse import RawTextHelpFormatter 14 | import logging 15 | import sys 16 | from Hologram.CustomCloud import CustomCloud 17 | from Exceptions.HologramError import HologramError 18 | 19 | from scripts.hologram_send import parse_hologram_send_args 20 | from scripts.hologram_send import run_hologram_send 21 | from scripts.hologram_receive import parse_hologram_receive_args 22 | from scripts.hologram_receive import run_hologram_receive 23 | from scripts.hologram_spacebridge import parse_hologram_spacebridge_args 24 | from scripts.hologram_spacebridge import run_hologram_spacebridge 25 | from scripts.hologram_heartbeat import parse_hologram_heartbeat_args 26 | from scripts.hologram_heartbeat import run_hologram_heartbeat 27 | from scripts.hologram_modem import parse_hologram_modem_args 28 | from scripts.hologram_modem import run_hologram_modem 29 | from scripts.hologram_network import parse_hologram_network_args 30 | from scripts.hologram_network import run_hologram_network 31 | from scripts.hologram_activate import parse_hologram_activate_args 32 | from scripts.hologram_activate import run_hologram_activate 33 | from scripts.hologram_util import VAction 34 | 35 | cloud = CustomCloud(None) 36 | 37 | script_description = 'Hologram Python SDK version ' + str(cloud.version) + '.\n' 38 | script_description += '''This hologram command line program allows you to interact 39 | with the Hologram SDK.\n 40 | ''' 41 | 42 | help_send = '''This subcommand allows you to send cloud messages to the Hologram 43 | Cloud or SMS to a specified destination number. Type hologram send --help for more 44 | information.\n 45 | ''' 46 | 47 | help_receive = '''This subcommand allows you to listen on a given host and port for 48 | incoming cloud messages or SMS. Type hologram receive --help for more information.\n 49 | ''' 50 | 51 | help_spacebridge = '''This subcommand allows you to use spacebridge by establishing 52 | a connection via the Python SDK. Type hologram spacebridge --help for more information.\n 53 | ''' 54 | 55 | help_heartbeat = '''This subcommand allows you to send periodic messages to your 56 | device via a specified period parameter. Type hologram heartbeat --help for more 57 | information.\n 58 | ''' 59 | 60 | help_modem = '''This subcommand allows you to use the SDK to interact with a 61 | supported modem. Type hologram modem --help for more information.\n 62 | ''' 63 | 64 | help_network = '''This subcommand allows you to use the SDK to interact with a 65 | network connection. Type hologram network --help for more information.\n 66 | ''' 67 | 68 | help_version = '''This subcommand prints the Hologram SDK version\n 69 | ''' 70 | 71 | help_activate = '''This subcommand allows you to activate a SIM via the Hologram SDK.\n 72 | ''' 73 | 74 | _command_handlers = { 75 | 76 | 'send': run_hologram_send, 77 | 'receive': run_hologram_receive, 78 | 'spacebridge': run_hologram_spacebridge, 79 | 'heartbeat': run_hologram_heartbeat, 80 | 'modem': run_hologram_modem, 81 | 'network': run_hologram_network 82 | } 83 | 84 | def parse_operations(): 85 | 86 | parser = argparse.ArgumentParser(description=script_description, 87 | formatter_class=RawTextHelpFormatter) 88 | 89 | subparsers = parser.add_subparsers(title='subcommands', 90 | description='valid subcommands', 91 | dest='subcommand', 92 | required=True) 93 | 94 | # parse hologram send subcommands 95 | parser_send = subparsers.add_parser('send', help=help_send, 96 | formatter_class=RawTextHelpFormatter) 97 | parse_hologram_send_args(parser_send) 98 | 99 | # parse hologram receive subcommands 100 | parser_receive = subparsers.add_parser('receive', help=help_receive, 101 | formatter_class=RawTextHelpFormatter) 102 | parse_hologram_receive_args(parser_receive) 103 | 104 | # parse hologram spacebridge subcommands 105 | parser_spacebridge = subparsers.add_parser('spacebridge', help=help_spacebridge, 106 | formatter_class=RawTextHelpFormatter) 107 | parse_hologram_spacebridge_args(parser_spacebridge) 108 | 109 | # parse hologram heartbeat subcommands 110 | parser_heartbeat = subparsers.add_parser('heartbeat', help=help_heartbeat, 111 | formatter_class=RawTextHelpFormatter) 112 | parse_hologram_heartbeat_args(parser_heartbeat) 113 | 114 | # parse hologram modem subcommands 115 | parser_modem = subparsers.add_parser('modem', help=help_modem, 116 | formatter_class=RawTextHelpFormatter) 117 | parse_hologram_modem_args(parser_modem) 118 | 119 | # parse hologram network subcommands 120 | parser_network = subparsers.add_parser('network', help=help_network, 121 | formatter_class=RawTextHelpFormatter) 122 | parse_hologram_network_args(parser_network) 123 | 124 | # parse hologram version 125 | parser_version = subparsers.add_parser('version', help=help_version, 126 | formatter_class=RawTextHelpFormatter) 127 | parse_version(parser_version) 128 | 129 | # parse hologram activate 130 | parser_activate = subparsers.add_parser('activate', help=help_activate, 131 | formatter_class=RawTextHelpFormatter) 132 | parse_hologram_activate_args(parser_activate) 133 | 134 | return vars(parser.parse_args()) 135 | 136 | 137 | def parse_version(parser): 138 | parser.set_defaults(command_selected='version') 139 | parser.add_argument('-v', nargs='?', action=VAction, dest='verbose', required=False) 140 | 141 | def run_version(args): 142 | cloud = CustomCloud(None) 143 | print(cloud.version) 144 | sys.exit(0) 145 | 146 | # EFFECTS: Sets the log level for all SDK interfaces 147 | def set_log_level(is_verbose): 148 | 149 | args = {'format': "%(levelname)s: %(message)s", 'level': logging.ERROR} 150 | 151 | if is_verbose == 1: 152 | args['level'] = logging.INFO 153 | elif is_verbose == 2: 154 | args['level'] = logging.DEBUG 155 | 156 | logging.basicConfig(**args) 157 | 158 | def main(): 159 | 160 | args = parse_operations() 161 | 162 | set_log_level(args['verbose']) 163 | 164 | logger = logging.getLogger('') 165 | 166 | if args['command_selected'] == 'version': 167 | run_version(args) 168 | elif args['command_selected'] == 'activate': 169 | run_hologram_activate(args) 170 | else: 171 | command_selected_prefix = args['command_selected'].split('_', 1)[0] 172 | if command_selected_prefix not in _command_handlers: 173 | logger.error('Internal script error: Invalid network type: %s', 174 | args['command_selected']) 175 | else: 176 | try: 177 | _command_handlers[command_selected_prefix](args) 178 | except HologramError as e: 179 | logger.error(str(e)) 180 | 181 | if __name__ == '__main__': main() 182 | -------------------------------------------------------------------------------- /scripts/hologram_activate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # hologram_activate.py - Hologram Python SDK command line interface (CLI) for activating 4 | # devices. 5 | # 6 | # Author: Hologram 7 | # 8 | # Copyright 2016 - Hologram (Konekt, Inc.) 9 | # 10 | # 11 | # LICENSE: Distributed under the terms of the MIT License 12 | 13 | import copy 14 | import getpass 15 | import sys 16 | import time 17 | 18 | from Hologram.HologramCloud import HologramCloud 19 | from Hologram.Api import Api 20 | from Exceptions.HologramError import HologramError 21 | from .hologram_util import VAction 22 | 23 | CHECK_LIVE_SIM_STATE_MAX_TIMEOUT = 120 # 2 mins for max timeout 24 | CHECK_LIVE_SIM_STATE_INTERVAL = 5 25 | 26 | # EFFECTS: Parses hologram activate CLI options. 27 | def parse_hologram_activate_args(parser): 28 | parser.set_defaults(command_selected='activate') 29 | parser.add_argument('-v', nargs='?', action=VAction, dest='verbose', required=False) 30 | parser.add_argument('--apikey', nargs='?', help='Hologram organization API key') 31 | 32 | # EFFECTS: Handles all hologram_send operations. 33 | # This function will call the appropriate cloud/sms handler. 34 | def run_hologram_activate(args): 35 | 36 | hologram = HologramCloud(dict(), network='cellular') 37 | sim = hologram.network.iccid 38 | 39 | if sim is None: 40 | raise HologramError('Failed to fetch SIM number from the modem') 41 | 42 | func_args = dict() 43 | 44 | if args['apikey'] is None: 45 | (username, password) = prompt_for_username_and_password() 46 | func_args['username'] = username 47 | func_args['password'] = password 48 | else: 49 | func_args['apikey'] = args['apikey'] 50 | 51 | api = Api(**func_args) 52 | 53 | success, plans = api.getPlans() 54 | 55 | if success == False: 56 | raise HologramError('Failed to fetch plans') 57 | 58 | planIdToZonesDict = populate_valid_plans(plans) 59 | 60 | selected_plan, plan_name = prompt_for_plan(plans, planIdToZonesDict) 61 | selected_zone = prompt_for_zone(planIdToZonesDict[selected_plan]) 62 | 63 | # preview 64 | success, response = api.activateSIM(sim=sim, plan=selected_plan, 65 | zone=selected_zone, preview=True) 66 | 67 | if success == False: 68 | print('Activation verification failed: %s' % response) 69 | return 70 | elif not confirm_activation(sim, plan_name, selected_plan, selected_zone, response['total_cost']): 71 | return 72 | 73 | 74 | success, response = api.activateSIM(sim=sim, plan=selected_plan, 75 | zone=selected_zone) 76 | 77 | if success: 78 | print('Activating SIM... (this may take up to a few minutes)') 79 | else: 80 | print('Activation failed') 81 | return 82 | 83 | start_time = time.time() 84 | while time.time() < start_time + CHECK_LIVE_SIM_STATE_MAX_TIMEOUT: 85 | success, sim_state = api.getSIMState(sim) 86 | 87 | if sim_state == 'LIVE': 88 | break 89 | 90 | time.sleep(CHECK_LIVE_SIM_STATE_INTERVAL) 91 | 92 | if sim_state == 'LIVE': 93 | print('Activation successful!') 94 | else: 95 | print('SIM is still not activated. Check status in a few minutes on Hologram Dashboard (https://dashboard.hologram.io/) or contact support@hologram.io with any further delays') 96 | 97 | 98 | def confirm_activation(sim, plan_name, selected_plan, selected_zone, total_cost): 99 | response = input("Activate SIM #%s on %s Zone %s for $%.2f (y/N)? " % (sim, plan_name, str(selected_zone), total_cost)) 100 | return response == 'y' 101 | 102 | # EFFECTS: Populate valid and available plans and returns a plan -> zones dictionary. 103 | def populate_valid_plans(plans): 104 | planIdToZonesDict = dict() 105 | 106 | for plan in plans: 107 | if is_available_developer_plan(plan) or is_pay_as_you_go_plan(plan): 108 | planIdToZonesDict[plan['id']] = copy.deepcopy(plan['tiers']['BASE']['zones']) 109 | return planIdToZonesDict 110 | 111 | def prompt_for_plan(plans, planIdToZonesDict): 112 | planIdToNames = dict() 113 | 114 | print('Available plans: ') 115 | 116 | if not plans: 117 | print(' [NONE]') 118 | return None 119 | 120 | # Print plan options 121 | for plan in plans: 122 | if plan['id'] in planIdToZonesDict: 123 | planIdToNames[plan['id']] = plan['name'] 124 | print_plan_description(plan) 125 | 126 | while True: 127 | try: 128 | planid = input('Choose the plan id for this device: ') 129 | if not planid.isdigit(): 130 | print('Error: Invalid plan id') 131 | continue 132 | planid = int(planid) 133 | if planid not in planIdToZonesDict: 134 | print('Error: Invalid plan id') 135 | continue 136 | return planid, planIdToNames[planid] 137 | except KeyboardInterrupt as e: 138 | sys.exit(1) 139 | return None, None 140 | 141 | # REQUIRES: A zones dictionary. 142 | # EFFECTS: Prompts the user for a zoneid based on all available zones. 143 | # Returns that zoneid. 144 | def prompt_for_zone(zones): 145 | 146 | zoneid = None 147 | zoneids = [] 148 | 149 | for zoneid, zone_details in zones.items(): 150 | zoneids.append(zoneid) 151 | print_zone_description(zoneid, zone_details) 152 | 153 | # There's only one available zone, so we can just select that without asking 154 | # the user. 155 | if len(zoneids) == 1: 156 | return zoneids[0] 157 | 158 | while True: 159 | try: 160 | zone = input('Choose the zone for this device: ') 161 | if zone not in zoneids: 162 | print('Error: Invalid zone') 163 | continue 164 | return zone 165 | except KeyboardInterrupt as e: 166 | sys.exit(1) 167 | return None 168 | 169 | # EFFECTS: Returns true if the given plan is a developer plan with the 170 | # available flag set to True. 171 | def is_available_developer_plan(plan): 172 | return 'available' in plan and plan['available'] 173 | 174 | # EFFECTS: Returns true if it's a pay as you go plan, false otherwise. 175 | def is_pay_as_you_go_plan(plan): 176 | return plan['data'] == 0 177 | 178 | # REQUIRES: The plan object to be formatted properly. 179 | # EFFECTS: Prints the plan description. 180 | def print_plan_description(plan): 181 | print("-----------------------------------------") 182 | print(" Plan ID # : %d" % plan['id']) 183 | print(" Name : %s" % plan['name']) 184 | print("-----------------------------------------") 185 | 186 | # REQUIRES: The zone object to be formatted properly. 187 | # EFFECTS: Prints the zone description. 188 | def print_zone_description(zoneid, zone_details): 189 | print("-----------------------------------------") 190 | print(" ZONE : %s" % str(zoneid)) 191 | print(" Data ($/MB) : $%s" % str(zone_details['overage'])) 192 | print(" Monthly Platform Fee: $%s" % str(zone_details['amount'])) 193 | print("-----------------------------------------") 194 | 195 | # EFFECTS: Prompts user for username and password, returns them as a tuple. 196 | def prompt_for_username_and_password(): 197 | 198 | try: 199 | username = input("Please enter your Hologram username: ") 200 | password = getpass.getpass("Please enter your Hologram password: ") 201 | except KeyboardInterrupt as e: 202 | sys.exit(1) 203 | 204 | return (username, password) 205 | -------------------------------------------------------------------------------- /scripts/hologram_heartbeat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # hologram_heartbeat.py - Hologram Python SDK command line interface (CLI) for 4 | # heartbeat interfaces 5 | # 6 | # Author: Hologram 7 | # 8 | # Copyright 2016 - Hologram (Konekt, Inc.) 9 | # 10 | # 11 | # LICENSE: Distributed under the terms of the MIT License 12 | 13 | DEFAULT_TIMEOUT = 5 14 | DEFAULT_REPEAT_PERIOD = 30 15 | 16 | # Hologram heartbeat is basically an alias for send cloud with a specified timeout. 17 | from scripts.hologram_send import run_hologram_send 18 | from scripts.hologram_send import parse_hologram_send_args 19 | 20 | def parse_hologram_heartbeat_args(parser): 21 | parse_hologram_send_args(parser) 22 | parser.set_defaults(command_selected='heartbeat') 23 | 24 | def run_hologram_heartbeat(args): 25 | args['host'] = None 26 | args['port'] = None 27 | 28 | if args['repeat'] == 0: 29 | args['repeat'] = DEFAULT_REPEAT_PERIOD 30 | 31 | run_hologram_send(args) 32 | -------------------------------------------------------------------------------- /scripts/hologram_network.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # hologram_network.py - Hologram Python SDK command line interface (CLI) for connect/disconnect interfaces. 4 | # 5 | # Author: Hologram 6 | # 7 | # Copyright 2016 - Hologram (Konekt, Inc.) 8 | # 9 | # LICENSE: Distributed under the terms of the MIT License 10 | 11 | from Hologram.CustomCloud import CustomCloud 12 | from Exceptions.HologramError import HologramError 13 | from .hologram_util import VAction 14 | import psutil 15 | 16 | help_connect = '''This subcommand establishes a cellular connection.\n 17 | ''' 18 | 19 | help_disconnect = '''This subcommand brings down a cellular connection.\n 20 | ''' 21 | 22 | def run_network_connect(args): 23 | cloud = CustomCloud(None, network='cellular') 24 | cloud.network.disable_at_sockets_mode() 25 | res = cloud.network.connect() 26 | if res: 27 | print('PPP session started') 28 | else: 29 | print('Failed to start PPP') 30 | 31 | def run_network_disconnect(args): 32 | print('Checking for existing PPP sessions') 33 | for proc in psutil.process_iter(): 34 | 35 | try: 36 | pinfo = proc.as_dict(attrs=['pid', 'name']) 37 | except: 38 | raise HologramError('Failed to check for existing PPP sessions') 39 | 40 | if 'pppd' in pinfo['name']: 41 | print('Found existing PPP session on pid: %s' % pinfo['pid']) 42 | print('Killing pid %s now' % pinfo['pid']) 43 | process = psutil.Process(pinfo['pid']) 44 | process.terminate() 45 | process.wait() 46 | 47 | _run_handlers = { 48 | 'network_connect': run_network_connect, 49 | 'network_disconnect': run_network_disconnect 50 | } 51 | 52 | # EFFECTS: Parses the CLI arguments as options to the hologram modem subcommand. 53 | def parse_hologram_network_args(parser): 54 | # Create a subparser 55 | subparsers = parser.add_subparsers(title='subcommands', 56 | dest='network subcommand', 57 | required=True) 58 | 59 | # Connect 60 | parser_connect = subparsers.add_parser('connect', help=help_connect) 61 | parser_connect.set_defaults(command_selected='network_connect') 62 | parser_connect.add_argument('-v', nargs='?', action=VAction, dest='verbose', required=False) 63 | 64 | # Disconnect 65 | parser_disconnect = subparsers.add_parser('disconnect', help=help_disconnect) 66 | parser_disconnect.set_defaults(command_selected='network_disconnect') 67 | parser_disconnect.add_argument('-v', nargs='?', action=VAction, dest='verbose', required=False) 68 | 69 | # EFFECTS: Runs the hologram modem interfaces. 70 | def run_hologram_network(args): 71 | 72 | if args['command_selected'] not in _run_handlers: 73 | raise Exception('Internal CLI error: Invalid command_selected value') 74 | else: 75 | _run_handlers[args['command_selected']](args) 76 | -------------------------------------------------------------------------------- /scripts/hologram_receive.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # hologram_receive.py - Hologram Python SDK command line interface (CLI) for inbound messages. 4 | # 5 | # Author: Hologram 6 | # 7 | # Copyright 2016 - Hologram (Konekt, Inc.) 8 | # 9 | # LICENSE: Distributed under the terms of the MIT License 10 | 11 | from Hologram.HologramCloud import HologramCloud 12 | from .hologram_util import handle_polling 13 | from .hologram_util import VAction 14 | import sys 15 | 16 | help_data = '''This subcommand allows you to listen on a given host and port for incoming cloud messages.\n 17 | ''' 18 | 19 | help_sms = '''This subcommand allows you to listen on a given host and port for incoming SMS.\n 20 | ''' 21 | 22 | hologram = None 23 | 24 | def popReceivedMessage(): 25 | recv = hologram.popReceivedMessage() 26 | if recv is not None: 27 | print('Received message: ' + str(recv)) 28 | 29 | def popReceivedSMS(): 30 | recv = hologram.popReceivedSMS() 31 | if recv is not None: 32 | print('Received SMS:', recv) 33 | 34 | 35 | def parse_common_receive_args(parser): 36 | parser.add_argument('-m', '--modem', nargs='?', default='nova', 37 | help='The modem type. Choose between nova, ms2131 and e303.') 38 | parser.add_argument('-v', nargs='?', action=VAction, dest='verbose', required=False) 39 | parser.add_argument('-t', '--timeout', type=int, nargs='?', default=-1, 40 | help='The number of seconds before the socket is closed. Default is to block indefinitely.') 41 | 42 | 43 | def parse_hologram_receive_args(parser): 44 | parse_common_receive_args(parser) 45 | parse_data_args(parser) 46 | parse_sms_args(parser) 47 | 48 | def parse_data_args(parser): 49 | parser.set_defaults(command_selected='receive_data') 50 | parser.add_argument('--data', action='store_true', required=False) 51 | 52 | def parse_sms_args(parser): 53 | parser.set_defaults(command_selected='receive_sms') 54 | parser.add_argument('--sms', action='store_true', required=False) 55 | 56 | def run_hologram_receive(args): 57 | 58 | if args['data'] and args['sms']: 59 | raise Exception('must pick either one of data or sms') 60 | if args['sms']: 61 | run_hologram_receive_sms(args) 62 | else: 63 | run_hologram_receive_data(args) 64 | 65 | # EFFECTS: Receives data from the Hologram Cloud. 66 | def run_hologram_receive_data(args): 67 | 68 | global hologram 69 | hologram = HologramCloud(dict(), network='cellular') 70 | 71 | hologram.event.subscribe('message.received', popReceivedMessage) 72 | 73 | if not hologram.network.at_sockets_available: 74 | hologram.network.connect() 75 | 76 | try: 77 | hologram.openReceiveSocket() 78 | except Exception as e: 79 | print(f"Failed to open socket to listen for data: {e}") 80 | return 81 | 82 | print (f'Ready to receive data on port {hologram.receive_port}') 83 | 84 | try: 85 | handle_polling(args['timeout'], popReceivedMessage, 1) 86 | except KeyboardInterrupt as e: 87 | pass 88 | 89 | print('Closing socket...') 90 | hologram.closeReceiveSocket() 91 | 92 | if not hologram.network.at_sockets_available: 93 | hologram.network.disconnect() 94 | 95 | # EFFECTS: Receives SMS from the Hologram Cloud. 96 | def run_hologram_receive_sms(args): 97 | global hologram 98 | hologram = HologramCloud(dict(), network='cellular') 99 | print ('Ready to receive sms') 100 | try: 101 | handle_polling(args['timeout'], popReceivedSMS, 1) 102 | except KeyboardInterrupt as e: 103 | sys.exit(e) 104 | -------------------------------------------------------------------------------- /scripts/hologram_send.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # hologram_send.py - Hologram Python SDK command line interface (CLI) for sending messages to the cloud 4 | # 5 | # Author: Hologram 6 | # 7 | # Copyright 2016 - Hologram (Konekt, Inc.) 8 | # 9 | # 10 | # LICENSE: Distributed under the terms of the MIT License 11 | 12 | 13 | from Hologram.CustomCloud import CustomCloud 14 | from Hologram.HologramCloud import HologramCloud 15 | from Exceptions.HologramError import HologramError 16 | from .hologram_util import VAction 17 | 18 | import argparse 19 | import time 20 | 21 | DEFAULT_TIMEOUT = 5 22 | 23 | help_cloud = '''This subcommand allows you to send cloud messages to the Hologram Cloud.\n 24 | ''' 25 | 26 | help_sms = '''This subcommand allows you to send SMS to a specified destination number.\n 27 | ''' 28 | 29 | # EFFECTS: Parses hologram send CLI options. 30 | def parse_hologram_send_args(parser): 31 | 32 | # Create a subparser 33 | parser.add_argument('--devicekey', nargs='?', help='Hologram device key (8 characters long)') 34 | parser.add_argument('message', nargs='?', help='Message that will be sent to the cloud') 35 | parser.add_argument('-v', nargs='?', action=VAction, dest='verbose', required=False) 36 | parser.add_argument('--host', required=False, help=argparse.SUPPRESS) 37 | parser.add_argument('-p', '--port', required=False, help=argparse.SUPPRESS) 38 | parser.add_argument('--authtype', default='totp', nargs='?', 39 | help='The authentication type used if HologramCloud is in use. Choose between \'totp\' and \'csrpsk\'') 40 | 41 | # $ hologram send cloud ... 42 | parse_cloud_args(parser) 43 | 44 | # $ hologram send sms ... 45 | parse_sms_args(parser) 46 | 47 | # EFFECTS: Parses the send cloud options. Sets the default command_selected option 48 | # to send_cloud. 49 | def parse_cloud_args(parser): 50 | parser.set_defaults(command_selected='send_cloud') 51 | parser.add_argument('--cloud', action='store_true', help='Message that will be sent to the cloud') 52 | 53 | parser.add_argument('--duration', type=int, nargs='?', default=-1, 54 | help='The number of seconds before periodic message ends. Default is to block indefinitely.') 55 | parser.add_argument('--repeat', type=int, default=0, nargs='?', 56 | help='Time period in seconds for each message send') 57 | parser.add_argument('--timeout', type=int, default=DEFAULT_TIMEOUT, nargs='?', 58 | help='The period in seconds before the socket closes if it doesn\'t receive a response') 59 | parser.add_argument('-t', '--topic', nargs = '?', action='append', 60 | help='Topics for the message (optional)') 61 | 62 | # EFFECTS: Parses the send sms options. Sets the default command_selected option 63 | # to send_sms. 64 | def parse_sms_args(parser): 65 | 66 | parser.set_defaults(command_selected='send_sms') 67 | parser.add_argument('--destination', nargs='?', required=False, 68 | help='The destination number in which the SMS will be sent. Destination number needs to be well formatted and start with a \'+\' sign') 69 | parser.add_argument('--sms', action='store_true', 70 | help='Message that will be sent to the cloud') 71 | 72 | # EFFECTS: Parses and sends the Hologram message using TOTP Authentication 73 | def sendTOTP(args, data, is_sms=False): 74 | 75 | hologram = HologramCloud(dict(), authentication_type='totp', network='cellular') 76 | send_message_helper(hologram, args, is_sms=is_sms) 77 | 78 | 79 | def sendSIMOTP(args, data, is_sms=False): 80 | 81 | hologram = HologramCloud(dict(), authentication_type='sim-otp', network='cellular') 82 | send_message_helper(hologram, args, is_sms=is_sms) 83 | 84 | # EFFECTS: Parses and sends the specified message using CSRPSK Authentication 85 | def sendPSK(args, data, is_sms=False): 86 | 87 | if not (args['devicekey']) and ('devicekey' in data): 88 | args['devicekey'] = data['devicekey'] 89 | 90 | if not args['devicekey']: 91 | raise HologramError('Device key not specified') 92 | 93 | credentials = {'devicekey': args['devicekey']} 94 | 95 | recv = '' 96 | if not is_sms and (args['host'] is not None or args['port'] is not None): 97 | # we're using some custom cloud 98 | customCloud = CustomCloud(None, 99 | send_host=args['host'], 100 | send_port=args['port']) 101 | recv = customCloud.sendMessage(args['message'], timeout=args['timeout']) 102 | print(f'RESPONSE FROM CLOUD: {recv}') 103 | else: 104 | # host and port are default so use Hologram 105 | hologram = HologramCloud(credentials, authentication_type='csrpsk', network='cellular') 106 | send_message_helper(hologram, args, is_sms=is_sms) 107 | 108 | # EFFECTS: Wraps the send message interface based on the repeat parameter. 109 | def send_message_helper(cloud, args, is_sms=False): 110 | 111 | if cloud.network is not None and not cloud.network.at_sockets_available: 112 | cloud.network.connect() 113 | 114 | if is_sms: 115 | args['repeat'] = 0 116 | 117 | recv = None 118 | if args['repeat'] == 0: 119 | if is_sms: 120 | # Send SMS to destination number 121 | recv = cloud.sendSMS(args['destination'], args['message']) 122 | else: 123 | recv = cloud.sendMessage(args['message'], topics=args['topic'], 124 | timeout=args['timeout']) 125 | print(f'RESPONSE MESSAGE: {cloud.getResultString(recv)}') 126 | else: 127 | cloud.sendPeriodicMessage(args['repeat'], 128 | args['message'], 129 | topics=args['topic'], 130 | timeout=args['timeout']) 131 | hold_for_duration(cloud, args['duration']) 132 | 133 | if cloud.network is not None and not cloud.network.at_sockets_available: 134 | cloud.network.disconnect() 135 | 136 | 137 | def hold_for_duration(cloud, duration = -1): 138 | start = time.time() 139 | try: 140 | while(cloud.periodicMessageRunning() and 141 | (duration == -1 or 142 | (time.time() - start < duration))): 143 | time.sleep(1) 144 | except KeyboardInterrupt as e: 145 | print("Interrupted") 146 | finally: 147 | cloud.stopPeriodicMessage() 148 | 149 | 150 | # EFFECTS: Handles all hologram_send operations. 151 | # This function will call the appropriate cloud/sms handler. 152 | def run_hologram_send(args): 153 | 154 | if args['message'] is None: 155 | raise HologramError('Message body cannot be empty') 156 | elif args['cloud'] and args['sms']: 157 | raise HologramError('must pick either one of cloud or sms') 158 | elif args['sms']: 159 | run_hologram_send_sms(args) 160 | else: 161 | run_hologram_send_cloud(args) 162 | 163 | # EFFECTS: Sends a given Hologram message to the cloud. 164 | def run_hologram_send_cloud(args): 165 | data = dict() 166 | if args['authtype'] == 'totp' and not args['devicekey']: 167 | sendTOTP(args, data) 168 | elif args['authtype'] == 'sim-otp': 169 | sendSIMOTP(args, data) 170 | else: 171 | sendPSK(args, data) 172 | 173 | # EFFECTS: Handles and sends a SMS to a specified destination number. 174 | def run_hologram_send_sms(args): 175 | 176 | if args['devicekey'] is None: 177 | raise HologramError('--devicekey is required') 178 | elif args['destination'] is None: 179 | raise HologramError('--destination missing. A destination number must be provided in order to send SMS to it') 180 | 181 | data = dict() 182 | # SMS can only be sent with CSRPSK auth. 183 | sendPSK(args, data, is_sms=True) 184 | -------------------------------------------------------------------------------- /scripts/hologram_spacebridge.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # hologram_spacebridge.py - Hologram Python SDK command line interface (CLI) for 4 | # spacebridge interfaces 5 | # 6 | # Author: Hologram 7 | # 8 | # Copyright 2016 - Hologram (Konekt, Inc.) 9 | # 10 | # 11 | # LICENSE: Distributed under the terms of the MIT License 12 | 13 | from Hologram.HologramCloud import HologramCloud 14 | from Hologram.Network import NetworkScope 15 | from .hologram_util import handle_polling 16 | from scripts.hologram_receive import parse_common_receive_args 17 | import sys 18 | 19 | # pylint: disable=W0603 20 | hologram = None 21 | 22 | 23 | def popReceivedMessage(): 24 | recv = hologram.popReceivedMessage() 25 | if recv is not None: 26 | print(f'Received message: {recv}') 27 | 28 | 29 | def parse_hologram_spacebridge_args(parser): 30 | parse_common_receive_args(parser) 31 | parser.set_defaults(command_selected='spacebridge') 32 | 33 | 34 | def run_hologram_spacebridge(args): 35 | global hologram 36 | hologram = HologramCloud(dict(), network='cellular') 37 | 38 | hologram.event.subscribe('message.received', popReceivedMessage) 39 | 40 | hologram.network.disable_at_sockets_mode() # Persistent cellular connection 41 | hologram.network.scope = NetworkScope.HOLOGRAM # Default route NOT set to cellular 42 | hologram.network.connect() 43 | 44 | hologram.openReceiveSocket() 45 | print(f'Ready to receive data on port {hologram.receive_port}') 46 | 47 | try: 48 | handle_polling(args['timeout'], popReceivedMessage, 1) 49 | except KeyboardInterrupt as e: 50 | print('Closing socket...') 51 | hologram.closeReceiveSocket() 52 | sys.exit(e) 53 | finally: 54 | hologram.network.disconnect() 55 | -------------------------------------------------------------------------------- /scripts/hologram_util.py: -------------------------------------------------------------------------------- 1 | # hologram_util.py - Hologram Python SDK command line interface (CLI) util 2 | # helper methods 3 | # 4 | # Author: Hologram 5 | # 6 | # Copyright 2016 - Hologram (Konekt, Inc.) 7 | # 8 | # LICENSE: Distributed under the terms of the MIT License 9 | 10 | import argparse 11 | import time 12 | import sys 13 | 14 | DEFAULT_DELAY_INTERVAL = 0 15 | 16 | def handle_timeout(timeout): 17 | 18 | try: 19 | if timeout != -1: 20 | print(f'waiting for {str(timeout)} seconds...') 21 | time.sleep(timeout) 22 | else: 23 | while True: 24 | time.sleep(1) 25 | except KeyboardInterrupt as e: 26 | sys.exit(e) 27 | 28 | def handle_polling(timeout, fx, delay_interval=DEFAULT_DELAY_INTERVAL): 29 | if timeout != -1: 30 | print(f'waiting for {str(timeout)} seconds...') 31 | end = time.time() + timeout 32 | while time.time() < end: 33 | fx() 34 | time.sleep(delay_interval) 35 | else: 36 | while True: 37 | fx() 38 | time.sleep(delay_interval) 39 | 40 | class VAction(argparse.Action): 41 | def __call__(self, parser, args, values, option_string=None): 42 | # print 'values: {v!r}'.format(v=values) 43 | if values==None: 44 | values='1' 45 | try: 46 | values=int(values) 47 | except ValueError: 48 | values=values.count('v')+1 49 | setattr(args, self.dest, values) 50 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Author: Hologram 3 | # 4 | # Copyright 2016 - Hologram (Konekt, Inc.) 5 | # 6 | # LICENSE: Distributed under the terms of the MIT License 7 | # 8 | 9 | longdesc = ''' 10 | This is a library for connecting to the Hologram Cloud 11 | ''' 12 | 13 | import sys 14 | try: 15 | from setuptools import setup, find_packages 16 | kw = { 17 | } 18 | except ImportError: 19 | from distutils.core import setup 20 | kw = {} 21 | 22 | if sys.platform == 'darwin': 23 | import setup_helper 24 | setup_helper.install_custom_make_tarball() 25 | 26 | setup( 27 | name = 'hologram-python', 28 | version = open('version.txt').read().split()[0], 29 | description = 'Library for accessing Hologram Cloud at https://hologram.io', 30 | long_description = longdesc, 31 | author = 'Hologram', 32 | author_email = 'support@hologram.io', 33 | url = 'https://github.com/hologram-io/hologram-python/', 34 | packages = find_packages(), 35 | include_package_data = True, 36 | tests_require = open('requirements-test.txt').read().split(), 37 | install_requires = open('requirements.txt').read().split(), 38 | scripts = ['scripts/hologram'], 39 | license = 'MIT', 40 | platforms = 'Posix; MacOS X; Windows', 41 | classifiers = [ 42 | 'Development Status :: 5 - Production/Stable', 43 | 'Intended Audience :: Developers', 44 | 'License :: OSI Approved :: MIT License', 45 | 'Operating System :: OS Independent', 46 | 'Topic :: Internet', 47 | 'Topic :: Security :: Cryptography', 48 | 'Programming Language :: Python', 49 | 'Programming Language :: Python :: 3.9', 50 | ], 51 | **kw 52 | ) 53 | -------------------------------------------------------------------------------- /setup_helper.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Hologram (Konekt, Inc.) 2 | # 3 | # Author: Hologram 4 | # 5 | # License: MIT License 6 | # 7 | 8 | 9 | import os 10 | import tarfile 11 | 12 | from distutils import log 13 | import distutils.archive_util 14 | from distutils.dir_util import mkpath 15 | from distutils.spawn import spawn 16 | 17 | try: 18 | from pwd import getpwnam 19 | except ImportError: 20 | getpwnam = None 21 | 22 | try: 23 | from grp import getgrnam 24 | except ImportError: 25 | getgrnam = None 26 | 27 | def _get_gid(name): 28 | """Returns a gid, given a group name.""" 29 | if getgrnam is None or name is None: 30 | return None 31 | try: 32 | result = getgrnam(name) 33 | except KeyError: 34 | result = None 35 | if result is not None: 36 | return result[2] 37 | return None 38 | 39 | def _get_uid(name): 40 | """Returns an uid, given a user name.""" 41 | if getpwnam is None or name is None: 42 | return None 43 | try: 44 | result = getpwnam(name) 45 | except KeyError: 46 | result = None 47 | if result is not None: 48 | return result[2] 49 | return None 50 | 51 | def make_tarball(base_name, base_dir, compress='gzip', verbose=0, dry_run=0, 52 | owner=None, group=None): 53 | """Create a tar file from all the files under 'base_dir'. 54 | This file may be compressed. 55 | 56 | :param compress: Compression algorithms. Supported algorithms are: 57 | 'gzip': (the default) 58 | 'compress' 59 | 'bzip2' 60 | None 61 | For 'gzip' and 'bzip2' the internal tarfile module will be used. 62 | For 'compress' the .tar will be created using tarfile, and then 63 | we will spawn 'compress' afterwards. 64 | The output tar file will be named 'base_name' + ".tar", 65 | possibly plus the appropriate compression extension (".gz", 66 | ".bz2" or ".Z"). Return the output filename. 67 | """ 68 | # XXX GNU tar 1.13 has a nifty option to add a prefix directory. 69 | # It's pretty new, though, so we certainly can't require it -- 70 | # but it would be nice to take advantage of it to skip the 71 | # "create a tree of hardlinks" step! (Would also be nice to 72 | # detect GNU tar to use its 'z' option and save a step.) 73 | 74 | compress_ext = { 'gzip': ".gz", 75 | 'bzip2': '.bz2', 76 | 'compress': ".Z" } 77 | 78 | # flags for compression program, each element of list will be an argument 79 | tarfile_compress_flag = {'gzip':'gz', 'bzip2':'bz2'} 80 | compress_flags = {'compress': ["-f"]} 81 | 82 | if compress is not None and compress not in compress_ext.keys(): 83 | raise ValueError("bad value for 'compress': must be None, 'gzip'," 84 | "'bzip2' or 'compress'") 85 | 86 | archive_name = base_name + ".tar" 87 | if compress and compress in tarfile_compress_flag: 88 | archive_name += compress_ext[compress] 89 | 90 | mode = 'w:' + tarfile_compress_flag.get(compress, '') 91 | 92 | mkpath(os.path.dirname(archive_name), dry_run=dry_run) 93 | log.info('Creating tar file %s with mode %s' % (archive_name, mode)) 94 | 95 | uid = _get_uid(owner) 96 | gid = _get_gid(group) 97 | 98 | def _set_uid_gid(tarinfo): 99 | if gid is not None: 100 | tarinfo.gid = gid 101 | tarinfo.gname = group 102 | if uid is not None: 103 | tarinfo.uid = uid 104 | tarinfo.uname = owner 105 | return tarinfo 106 | 107 | if not dry_run: 108 | tar = tarfile.open(archive_name, mode=mode) 109 | # This recursively adds everything underneath base_dir 110 | try: 111 | try: 112 | # Support for the `filter' parameter was added in Python 2.7, 113 | # earlier versions will raise TypeError. 114 | tar.add(base_dir, filter=_set_uid_gid) 115 | except TypeError: 116 | tar.add(base_dir) 117 | finally: 118 | tar.close() 119 | 120 | if compress and compress not in tarfile_compress_flag: 121 | spawn([compress] + compress_flags[compress] + [archive_name], 122 | dry_run=dry_run) 123 | return archive_name + compress_ext[compress] 124 | else: 125 | return archive_name 126 | 127 | 128 | _custom_formats = { 129 | 'gztar': (make_tarball, [('compress', 'gzip')], "gzip'ed tar-file"), 130 | 'bztar': (make_tarball, [('compress', 'bzip2')], "bzip2'ed tar-file"), 131 | 'ztar': (make_tarball, [('compress', 'compress')], "compressed tar file"), 132 | 'tar': (make_tarball, [('compress', None)], "uncompressed tar file"), 133 | } 134 | 135 | # Hack in and insert ourselves into the distutils code base 136 | def install_custom_make_tarball(): 137 | distutils.archive_util.ARCHIVE_FORMATS.update(_custom_formats) 138 | 139 | -------------------------------------------------------------------------------- /tests/API/test_API.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pytest 3 | from unittest.mock import Mock, patch 4 | sys.path.append(".") 5 | sys.path.append("..") 6 | sys.path.append("../..") 7 | from Hologram.Api import Api 8 | from Exceptions.HologramError import ApiError 9 | 10 | class TestHologramAPI: 11 | 12 | def test_create_no_creds(self): 13 | with pytest.raises(ApiError, match = 'Must specify valid HTTP authentication credentials'): 14 | Api() 15 | 16 | def test_create_missing_password(self): 17 | with pytest.raises(ApiError, match = 'Must specify valid HTTP authentication credentials'): 18 | Api(username='user') 19 | 20 | def test_create_missing_username(self): 21 | with pytest.raises(ApiError, match = 'Must specify valid HTTP authentication credentials'): 22 | Api(password='password') 23 | 24 | @patch('requests.post') 25 | def test_activate(self, r_post): 26 | api = Api(apikey='123apikey') 27 | 28 | r_post.return_value = Mock(status_code=200) 29 | r_post.return_value.json = Mock(return_value={"success": True, 'order_data': {}}) 30 | 31 | success, response = api.activateSIM('iccid') 32 | 33 | assert success == True 34 | assert response == {} 35 | 36 | @patch('requests.post') 37 | def test_activate_failed(self, r_post): 38 | api = Api(apikey='123apikey') 39 | 40 | r_post.return_value = Mock(status_code=200) 41 | r_post.return_value.json = Mock(return_value={"success": False, 'data': {'iccid': 'Activation failed'}}) 42 | 43 | success, response = api.activateSIM('iccid') 44 | 45 | assert success == False 46 | assert response == 'Activation failed' 47 | 48 | @patch('requests.post') 49 | def test_activate_bad_status_code(self, r_post): 50 | api = Api(apikey='123apikey') 51 | 52 | r_post.return_value = Mock( 53 | status_code=429, 54 | text = 'Too many requests') 55 | 56 | success, response = api.activateSIM('iccid') 57 | 58 | assert success == False 59 | assert response == 'Too many requests' 60 | 61 | @patch('requests.get') 62 | def test_get_plans(self, r_post): 63 | api = Api(apikey='123apikey') 64 | 65 | r_post.return_value.json = Mock(return_value={"success": True, 'data': {'id': 1, 'orgid': 1}}) 66 | 67 | success, response = api.getPlans() 68 | 69 | assert success == True 70 | assert response == {'id': 1, 'orgid': 1} 71 | 72 | @patch('requests.get') 73 | def test_get_sim_state(self, r_post): 74 | api = Api(apikey='123apikey') 75 | 76 | r_post.return_value.json = Mock(return_value={"success": True, 'data': [{'state': 'LIVE'}]}) 77 | 78 | success, response = api.getSIMState('iccid') 79 | 80 | assert success == True 81 | assert response == 'LIVE' 82 | 83 | -------------------------------------------------------------------------------- /tests/Authentication/test_Authentication.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2016 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_Authentication.py - This file implements unit tests for the Authentication 8 | # class. 9 | 10 | import sys 11 | 12 | sys.path.append(".") 13 | sys.path.append("..") 14 | sys.path.append("../..") 15 | from Hologram.Authentication import Authentication 16 | 17 | credentials = {'devicekey':'12345678'} 18 | 19 | class TestAuthentication: 20 | 21 | def test_create(self): 22 | auth = Authentication.Authentication(credentials) 23 | -------------------------------------------------------------------------------- /tests/Authentication/test_CSRPSKAuthentication.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2016 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_CSRPSKAuthentication.py - This file implements unit tests for the 8 | # CSRPSKAuthentication class. 9 | 10 | import sys 11 | import pytest 12 | 13 | sys.path.append(".") 14 | sys.path.append("..") 15 | sys.path.append("../..") 16 | from Hologram.Authentication.CSRPSKAuthentication import CSRPSKAuthentication 17 | 18 | class TestCSRPSKAuthentication: 19 | 20 | def test_create(self): 21 | credentials = {'devicekey': '12345678'} 22 | auth = CSRPSKAuthentication(credentials) 23 | 24 | def test_invalid_device_key_length(self): 25 | credentials = {'devicekey': '12345678'} 26 | auth = CSRPSKAuthentication(credentials) 27 | auth.credentials['devicekey'] = '12345' 28 | with pytest.raises(Exception, match = 'Device key must be 8 characters long'): 29 | auth.buildPayloadString('test invalid device key') 30 | 31 | def test_build_payload_string_without_topics(self): 32 | credentials = {'devicekey': '12345678'} 33 | auth = CSRPSKAuthentication(credentials) 34 | message = 'test without topics' 35 | assert b"{\"k\": \"12345678\", \"m\": \"\\u0001e303-None\", \"d\": \"test without topics\"}\r\r" == auth.buildPayloadString(message, modem_type='E303') 36 | 37 | def test_build_payload_string_with_empty_modem_type_and_id(self): 38 | credentials = {'devicekey': '12345678'} 39 | auth = CSRPSKAuthentication(credentials) 40 | message = 'test with empty modem_type and modem_id' 41 | assert b"{\"k\": \"12345678\", \"m\": \"\\u0001agnostic-None\", \"d\": \"test with empty modem_type and modem_id\"}\r\r" \ 42 | == auth.buildPayloadString(message, modem_type=None) 43 | 44 | def test_invalid_device_key_length(self): 45 | credentials = {'devicekey': '12345678'} 46 | auth = CSRPSKAuthentication(credentials) 47 | auth.credentials['devicekey'] = '12345' 48 | with pytest.raises(Exception, match = 'Device key must be 8 characters long'): 49 | auth.buildPayloadString('test invalid device key') 50 | -------------------------------------------------------------------------------- /tests/Authentication/test_HologramAuthentication.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2016 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_HologramAuthentication.py - This file implements unit tests for the 8 | # HologramAuthentication class. 9 | 10 | import sys 11 | import pytest 12 | 13 | sys.path.append(".") 14 | sys.path.append("..") 15 | sys.path.append("../..") 16 | from Hologram.Authentication.HologramAuthentication import HologramAuthentication 17 | 18 | credentials = {'devicekey': '12345678'} 19 | 20 | class TestHologramAuthentication: 21 | 22 | def test_create(self): 23 | auth = HologramAuthentication(credentials) 24 | 25 | def test_invalid_auth_string(self): 26 | auth = HologramAuthentication(credentials) 27 | with pytest.raises(Exception, match = 'Must instantiate a subclass of HologramAuthentication'): 28 | auth.buildPayloadString('test msg', 'test topic') 29 | 30 | def test_invalid_topic_string(self): 31 | auth = HologramAuthentication(credentials) 32 | with pytest.raises(Exception, match = 'Must instantiate a subclass of HologramAuthentication'): 33 | auth.buildTopicString('test topic') 34 | 35 | def test_invalid_msg_string(self): 36 | auth = HologramAuthentication(credentials) 37 | with pytest.raises(Exception, match = 'Must instantiate a subclass of HologramAuthentication'): 38 | auth.buildMessageString('test msg') 39 | 40 | def test_build_modem_type_id_str(self): 41 | auth = HologramAuthentication(credentials) 42 | 43 | payload = auth.build_modem_type_id_str('Nova', 'TEST_SARA-1111') 44 | assert payload == 'nova-TEST_SARA-1111' 45 | 46 | payload = auth.build_modem_type_id_str('MS2131', 'TEST_SARA-1111') 47 | assert payload == 'ms2131' 48 | 49 | payload = auth.build_modem_type_id_str('E303', 'TEST_SARA-1111') 50 | assert payload == 'e303' 51 | -------------------------------------------------------------------------------- /tests/Event/test_Event.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2016 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_Event.py - This file implements unit tests for the 8 | # Event class. 9 | 10 | import sys 11 | 12 | sys.path.append(".") 13 | sys.path.append("..") 14 | sys.path.append("../..") 15 | from Hologram.Event import Event 16 | 17 | class TestEvent: 18 | 19 | def test_create(self): 20 | event = Event() 21 | -------------------------------------------------------------------------------- /tests/MessageMode/test_Cloud.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2016 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_Cloud.py - This file implements unit tests for the Cloud class. 8 | 9 | import pytest 10 | import sys 11 | sys.path.append(".") 12 | sys.path.append("..") 13 | sys.path.append("../..") 14 | from Hologram.Authentication import * 15 | from Hologram.Cloud import Cloud 16 | 17 | class TestCloud: 18 | 19 | def test_create_send(self): 20 | cloud = Cloud(None, send_host = '127.0.0.1', send_port = 9999) 21 | 22 | assert cloud.send_host == '127.0.0.1' 23 | assert cloud.send_port == 9999 24 | assert cloud.receive_host == '' 25 | assert cloud.receive_port == 0 26 | 27 | def test_create_receive(self): 28 | cloud = Cloud(None, receive_host = '127.0.0.1', receive_port = 9999) 29 | 30 | assert cloud.send_host == '' 31 | assert cloud.send_port == 0 32 | assert cloud.receive_host == '127.0.0.1' 33 | assert cloud.receive_port == 9999 34 | 35 | def test_invalid_send_message(self): 36 | cloud = Cloud(None, receive_host = '127.0.0.1', receive_port = 9999) 37 | 38 | with pytest.raises(Exception, match = 'Must instantiate a Cloud type'): 39 | cloud.sendMessage("hello SMS") 40 | 41 | def test_invalid_send_sms(self): 42 | cloud = Cloud(None, send_host = '127.0.0.1', send_port = 9999) 43 | 44 | with pytest.raises(Exception, match = 'Must instantiate a Cloud type'): 45 | cloud.sendSMS('+12345678900', 'hello SMS') 46 | 47 | # This is good for testing if we updated the internal SDK version numbers before release. 48 | def test_sdk_version(self): 49 | cloud = Cloud(None, send_host = '127.0.0.1', send_port = 9999) 50 | 51 | assert cloud.version == '0.9.1' 52 | -------------------------------------------------------------------------------- /tests/MessageMode/test_CustomCloud.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2016 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_CustomCloud.py - This file implements unit tests for the CustomCloud class. 8 | 9 | import pytest 10 | import sys 11 | sys.path.append(".") 12 | sys.path.append("..") 13 | sys.path.append("../..") 14 | from Hologram.Authentication import * 15 | from Hologram.CustomCloud import CustomCloud 16 | 17 | class TestCustomCloud: 18 | 19 | def test_create_send(self): 20 | customCloud = CustomCloud(None, send_host='127.0.0.1', 21 | send_port=9999, enable_inbound=False) 22 | 23 | assert customCloud.send_host == '127.0.0.1' 24 | assert customCloud.send_port == 9999 25 | assert customCloud.receive_host == '' 26 | assert customCloud.receive_port == 0 27 | 28 | def test_create_receive(self): 29 | customCloud = CustomCloud(None, receive_host='127.0.0.1', 30 | receive_port=9999, enable_inbound=False) 31 | 32 | assert customCloud.send_host == '' 33 | assert customCloud.send_port == 0 34 | assert customCloud.receive_host == '127.0.0.1' 35 | assert customCloud.receive_port == 9999 36 | 37 | def test_enable_inbound(self): 38 | 39 | with pytest.raises(Exception, match='Must set receive host and port for inbound connection'): 40 | customCloud = CustomCloud(None, send_host='receive.com', 41 | send_port=9999, enable_inbound=True) 42 | 43 | def test_invalid_send_host_and_port(self): 44 | customCloud = CustomCloud(None, receive_host='receive.com', receive_port=9999) 45 | 46 | with pytest.raises(Exception, match = 'Send host and port must be set before making this operation'): 47 | customCloud.sendMessage("hello") 48 | 49 | def test_invalid_send_sms(self): 50 | customCloud = CustomCloud(None, 'test.com', 9999) 51 | 52 | temp = "hello" 53 | with pytest.raises(NotImplementedError, match='Cannot send SMS via custom cloud'): 54 | customCloud.sendSMS('+1234567890', temp) 55 | -------------------------------------------------------------------------------- /tests/MessageMode/test_HologramCloud.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2016 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_HologramCloud.py - This file implements unit tests for the HologramCloud class. 8 | 9 | import sys 10 | import pytest 11 | sys.path.append(".") 12 | sys.path.append("..") 13 | sys.path.append("../..") 14 | from Hologram.Authentication import * 15 | from Hologram.HologramCloud import HologramCloud 16 | from Exceptions.HologramError import AuthenticationError 17 | 18 | credentials = {'devicekey':'12345678'} 19 | 20 | class TestHologramCloud: 21 | 22 | def test_create(self): 23 | hologram = HologramCloud(credentials, authentication_type='csrpsk', enable_inbound = False) 24 | 25 | assert hologram.send_host == 'cloudsocket.hologram.io' 26 | assert hologram.send_port == 9999 27 | assert hologram.receive_host == '0.0.0.0' 28 | assert hologram.receive_port == 4010 29 | 30 | def test_create_bad_totp_keys(self): 31 | with pytest.raises(AuthenticationError, match = 'Unable to fetch device id or private key for TOTP authenication'): 32 | HologramCloud(credentials, enable_inbound = False) 33 | 34 | def test_invalid_sms_length(self): 35 | 36 | hologram = HologramCloud(credentials, authentication_type='csrpsk', enable_inbound = False) 37 | 38 | temp = '111111111234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890' 39 | with pytest.raises(Exception, match = 'SMS cannot be more than 160 characters long'): 40 | hologram.sendSMS('+1234567890', temp) 41 | 42 | def test_get_result_string(self): 43 | 44 | hologram = HologramCloud(credentials, authentication_type='csrpsk', enable_inbound = False) 45 | 46 | assert hologram.getResultString(-1) == 'Unknown error' 47 | assert hologram.getResultString(0) == 'Message sent successfully' 48 | assert hologram.getResultString(1) == 'Connection was closed so we couldn\'t read the whole message' 49 | assert hologram.getResultString(2) == 'Failed to parse the message' 50 | assert hologram.getResultString(3) == 'Auth section of the message was invalid' 51 | assert hologram.getResultString(4) == 'Payload type was invalid' 52 | assert hologram.getResultString(5) == 'Protocol type was invalid' 53 | assert hologram.getResultString(6) == 'Internal error in Hologram Cloud' 54 | assert hologram.getResultString(7) == 'Metadata was formatted incorrectly' 55 | assert hologram.getResultString(8) == 'Topic was formatted incorrectly' 56 | -------------------------------------------------------------------------------- /tests/Modem/test_BG96.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2017 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_BG96.py - This file implements unit tests for the BG96 modem interface. 8 | 9 | from unittest.mock import patch, call 10 | import pytest 11 | import sys 12 | 13 | from Hologram.Network.Modem.BG96 import BG96 14 | from UtilClasses import ModemResult 15 | 16 | sys.path.append(".") 17 | sys.path.append("..") 18 | sys.path.append("../..") 19 | 20 | 21 | def mock_write(modem, message): 22 | return True 23 | 24 | 25 | def mock_read(modem): 26 | return True 27 | 28 | 29 | def mock_readline(modem, timeout=None, hide=False): 30 | return "" 31 | 32 | 33 | def mock_open_serial_port(modem, device_name=None): 34 | return True 35 | 36 | 37 | def mock_close_serial_port(modem): 38 | return True 39 | 40 | 41 | def mock_detect_usable_serial_port(modem, stop_on_first=True): 42 | return "/dev/ttyUSB0" 43 | 44 | 45 | @pytest.fixture 46 | def no_serial_port(monkeypatch): 47 | monkeypatch.setattr(BG96, "_read_from_serial_port", mock_read) 48 | monkeypatch.setattr(BG96, "_readline_from_serial_port", mock_readline) 49 | monkeypatch.setattr(BG96, "_write_to_serial_port_and_flush", mock_write) 50 | monkeypatch.setattr(BG96, "openSerialPort", mock_open_serial_port) 51 | monkeypatch.setattr(BG96, "closeSerialPort", mock_close_serial_port) 52 | monkeypatch.setattr(BG96, "detect_usable_serial_port", mock_detect_usable_serial_port) 53 | 54 | 55 | def test_init_BG96_no_args(no_serial_port): 56 | modem = BG96() 57 | assert modem.timeout == 1 58 | assert modem.socket_identifier == 0 59 | assert modem.chatscript_file.endswith("/chatscripts/default-script") 60 | assert modem._at_sockets_available 61 | 62 | 63 | @patch.object(BG96, "set") 64 | @patch.object(BG96, "command") 65 | @patch.object(BG96, "_is_pdp_context_active") 66 | def test_close_socket(mock_pdp, mock_command, mock_set, no_serial_port): 67 | modem = BG96() 68 | modem.socket_identifier = 1 69 | mock_set.return_value = (ModemResult.OK, None) 70 | mock_command.return_value = (ModemResult.OK, None) 71 | mock_pdp.return_value = True 72 | modem.close_socket() 73 | mock_set.assert_called_with("+QIACT", "0", timeout=30) 74 | mock_command.assert_called_with("+QICLOSE", 1) 75 | 76 | @patch.object(BG96, "set") 77 | def test_set_up_pdp_context_default(mock_set, no_serial_port): 78 | modem = BG96() 79 | mock_set.return_value = (ModemResult.OK, None) 80 | 81 | modem._set_up_pdp_context() 82 | 83 | expected_calls = [call('+QICSGP', '1,1,\"hologram\",\"\",\"\",1'), 84 | call('+QIACT', '1', timeout=30)] 85 | mock_set.assert_has_calls(expected_calls, any_order=True) 86 | 87 | @patch.object(BG96, "set") 88 | def test_set_up_pdp_context_custom_apn_and_pdp_context(mock_set, no_serial_port): 89 | modem = BG96(apn='hologram2', pdp_context=3) 90 | mock_set.return_value = (ModemResult.OK, None) 91 | 92 | modem._set_up_pdp_context() 93 | 94 | expected_calls = [call('+QICSGP', '3,1,\"hologram2\",\"\",\"\",1'), 95 | call('+QIACT', '3', timeout=30)] 96 | mock_set.assert_has_calls(expected_calls, any_order=True) 97 | -------------------------------------------------------------------------------- /tests/Modem/test_EC21.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2017 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_EC21.py - This file implements unit tests for the EC21 modem interface. 8 | 9 | from unittest.mock import patch 10 | import pytest 11 | import sys 12 | 13 | from Hologram.Network.Modem.EC21 import EC21 14 | from UtilClasses import ModemResult 15 | 16 | sys.path.append(".") 17 | sys.path.append("..") 18 | sys.path.append("../..") 19 | 20 | 21 | def mock_write(modem, message): 22 | return True 23 | 24 | 25 | def mock_read(modem): 26 | return True 27 | 28 | 29 | def mock_readline(modem, timeout=None, hide=False): 30 | return "" 31 | 32 | 33 | def mock_open_serial_port(modem, device_name=None): 34 | return True 35 | 36 | 37 | def mock_close_serial_port(modem): 38 | return True 39 | 40 | 41 | def mock_detect_usable_serial_port(modem, stop_on_first=True): 42 | return "/dev/ttyUSB0" 43 | 44 | 45 | @pytest.fixture 46 | def no_serial_port(monkeypatch): 47 | monkeypatch.setattr(EC21, "_read_from_serial_port", mock_read) 48 | monkeypatch.setattr(EC21, "_readline_from_serial_port", mock_readline) 49 | monkeypatch.setattr(EC21, "_write_to_serial_port_and_flush", mock_write) 50 | monkeypatch.setattr(EC21, "openSerialPort", mock_open_serial_port) 51 | monkeypatch.setattr(EC21, "closeSerialPort", mock_close_serial_port) 52 | monkeypatch.setattr(EC21, "detect_usable_serial_port", mock_detect_usable_serial_port) 53 | 54 | 55 | def test_init_EC21_no_args(no_serial_port): 56 | modem = EC21() 57 | assert modem.timeout == 1 58 | assert modem.socket_identifier == 0 59 | assert modem.chatscript_file.endswith("/chatscripts/default-script") 60 | assert modem._at_sockets_available 61 | 62 | 63 | @patch.object(EC21, "set") 64 | @patch.object(EC21, "command") 65 | @patch.object(EC21, "_is_pdp_context_active") 66 | def test_close_socket(mock_pdp, mock_command, mock_set, no_serial_port): 67 | modem = EC21() 68 | modem.socket_identifier = 1 69 | mock_set.return_value = (ModemResult.OK, None) 70 | mock_command.return_value = (ModemResult.OK, None) 71 | mock_pdp.return_value = True 72 | modem.close_socket() 73 | mock_set.assert_called_with("+QIDEACT", "1", timeout=30) 74 | mock_command.assert_called_with("+QICLOSE", 1) 75 | -------------------------------------------------------------------------------- /tests/Modem/test_MS2131.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2016 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_MS2131.py - This file implements unit tests for the MS2131 modem interface. 8 | 9 | from mock import patch 10 | import pytest 11 | import sys 12 | 13 | from Hologram.Network.Modem.MS2131 import MS2131 14 | 15 | sys.path.append(".") 16 | sys.path.append("..") 17 | sys.path.append("../..") 18 | 19 | def mock_write(ms2131, message): 20 | return True 21 | 22 | def mock_read(ms2131): 23 | return True 24 | 25 | def mock_readline(ms2131, timeout=None, hide=False): 26 | return '' 27 | 28 | def mock_open_serial_port(ms2131, device_name=None): 29 | return True 30 | 31 | def mock_close_serial_port(ms2131): 32 | return True 33 | 34 | def mock_detect_usable_serial_port(ms2131, stop_on_first=True): 35 | return '/dev/ttyUSB0' 36 | 37 | @pytest.fixture 38 | def no_serial_port(monkeypatch): 39 | monkeypatch.setattr(MS2131, '_read_from_serial_port', mock_read) 40 | monkeypatch.setattr(MS2131, '_readline_from_serial_port', mock_readline) 41 | monkeypatch.setattr(MS2131, '_write_to_serial_port_and_flush', mock_write) 42 | monkeypatch.setattr(MS2131, 'openSerialPort', mock_open_serial_port) 43 | monkeypatch.setattr(MS2131, 'closeSerialPort', mock_close_serial_port) 44 | monkeypatch.setattr(MS2131, 'detect_usable_serial_port', mock_detect_usable_serial_port) 45 | 46 | def test_init_ms2131_no_args(no_serial_port): 47 | modem = MS2131() 48 | assert(modem.timeout == 1) 49 | assert(modem.socket_identifier == 0) 50 | assert(modem.chatscript_file.endswith('/chatscripts/default-script')) 51 | assert(modem._at_sockets_available == False) 52 | 53 | def test_init_ms2131_chatscriptfileoverride(no_serial_port): 54 | modem = MS2131(chatscript_file='test-chatscript') 55 | assert(modem.timeout == 1) 56 | assert(modem.socket_identifier == 0) 57 | assert(modem.chatscript_file == 'test-chatscript') 58 | 59 | @patch.object(MS2131, 'command') 60 | def test_disable_at_sockets_mode_ms2131(mock_command, no_serial_port): 61 | modem = MS2131() 62 | 63 | mock_command.return_value = [] 64 | mock_command.reset_mock() 65 | modem.disable_at_sockets_mode() 66 | # The disable_at_sockets_mode() call should not do anything. 67 | mock_command.assert_not_called() 68 | -------------------------------------------------------------------------------- /tests/Modem/test_Modem.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2016 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_Modem.py - This file implements unit tests for the Modem class. 8 | 9 | from unittest.mock import patch, call 10 | import pytest 11 | import sys 12 | from datetime import datetime 13 | 14 | sys.path.append(".") 15 | sys.path.append("..") 16 | sys.path.append("../..") 17 | from Exceptions.HologramError import SerialError 18 | from Hologram.Network.Modem import Modem 19 | from UtilClasses import ModemResult 20 | 21 | def mock_write(modem, message): 22 | return True 23 | 24 | def mock_read(modem): 25 | return True 26 | 27 | def mock_init_commands(modem): 28 | return True 29 | 30 | def mock_readline(modem, timeout=None, hide=False): 31 | return '' 32 | 33 | def mock_open_serial_port(modem, device_name=None): 34 | return True 35 | 36 | def mock_close_serial_port(modem): 37 | return True 38 | 39 | def mock_result(modem): 40 | return (ModemResult.OK, None) 41 | 42 | def mock_detect_usable_serial_port(modem, stop_on_first=True): 43 | return '/dev/ttyUSB0' 44 | 45 | def mock_command_sms(modem, at_command): 46 | return (ModemResult.OK, ['+CMGL: 2,1,,26', '0791447779071413040C9144977304250500007160421062944008D4F29C0E8AC966']) 47 | 48 | def mock_set_sms(modem, at_command, val): 49 | return None 50 | 51 | def mock_inactive_pdp_context(modem): 52 | return False 53 | 54 | @pytest.fixture 55 | def no_serial_port(monkeypatch): 56 | monkeypatch.setattr(Modem, '_read_from_serial_port', mock_read) 57 | monkeypatch.setattr(Modem, '_readline_from_serial_port', mock_readline) 58 | monkeypatch.setattr(Modem, '_write_to_serial_port_and_flush', mock_write) 59 | monkeypatch.setattr(Modem, 'init_serial_commands', mock_init_commands) 60 | monkeypatch.setattr(Modem, 'openSerialPort', mock_open_serial_port) 61 | monkeypatch.setattr(Modem, 'closeSerialPort', mock_close_serial_port) 62 | monkeypatch.setattr(Modem, 'detect_usable_serial_port', mock_detect_usable_serial_port) 63 | monkeypatch.setattr(Modem, '_is_pdp_context_active', mock_inactive_pdp_context) 64 | 65 | @pytest.fixture 66 | def get_sms(monkeypatch): 67 | monkeypatch.setattr(Modem, 'command', mock_command_sms) 68 | monkeypatch.setattr(Modem, 'set', mock_set_sms) 69 | 70 | @pytest.fixture 71 | def override_command_result(monkeypatch): 72 | monkeypatch.setattr(Modem, '_command_result', mock_result) 73 | 74 | # CONSTRUCTOR 75 | 76 | def test_init_modem_no_args(no_serial_port): 77 | modem = Modem() 78 | assert(modem.timeout == 1) 79 | assert(modem.socket_identifier == 0) 80 | assert(modem.chatscript_file.endswith('/chatscripts/default-script')) 81 | assert(modem._at_sockets_available == False) 82 | assert(modem.description == 'Modem') 83 | assert(modem.apn == 'hologram') 84 | assert(modem.pdp_context == 1) 85 | 86 | def test_init_modem_chatscriptfileoverride(no_serial_port): 87 | modem = Modem(chatscript_file='test-chatscript') 88 | assert(modem.timeout == 1) 89 | assert(modem.socket_identifier == 0) 90 | assert(modem.chatscript_file == 'test-chatscript') 91 | 92 | def test_init_modem_apn(no_serial_port): 93 | modem = Modem(apn='hologram2') 94 | assert(modem.apn == 'hologram2') 95 | 96 | def test_init_modem_pdp_context(no_serial_port): 97 | modem = Modem(pdp_context=3) 98 | assert(modem.pdp_context == 3) 99 | 100 | def test_get_result_string(no_serial_port): 101 | modem = Modem() 102 | assert(modem.getResultString(0) == 'Modem returned OK') 103 | assert(modem.getResultString(-1) == 'Modem timeout') 104 | assert(modem.getResultString(-2) == 'Modem error') 105 | assert(modem.getResultString(-3) == 'Modem response doesn\'t match expected return value') 106 | assert(modem.getResultString(-99) == 'Unknown response code') 107 | 108 | # PROPERTIES 109 | 110 | def test_get_location(no_serial_port): 111 | modem = Modem() 112 | with pytest.raises(NotImplementedError) as e: 113 | assert(modem.location == 'test location') 114 | assert('This modem does not support this property' in str(e)) 115 | 116 | @patch.object(Modem, "set") 117 | def test_set_up_pdp_context_default(mock_set, no_serial_port): 118 | modem = Modem() 119 | mock_set.return_value = (ModemResult.OK, None) 120 | 121 | modem._set_up_pdp_context() 122 | 123 | expected_calls = [call('+UPSD', '0,1,\"hologram\"'), 124 | call('+UPSD', '0,7,\"0.0.0.0\"'), 125 | call('+UPSDA', '0,3', timeout=30)] 126 | mock_set.assert_has_calls(expected_calls, any_order=True) 127 | 128 | @patch.object(Modem, "set") 129 | def test_set_up_pdp_context_custom_apn_and_pdp_context(mock_set, no_serial_port): 130 | modem = Modem(apn='hologram2', pdp_context=3) 131 | mock_set.return_value = (ModemResult.OK, None) 132 | 133 | modem._set_up_pdp_context() 134 | 135 | expected_calls = [call('+UPSD', '0,100,3'), 136 | call('+UPSD', '0,1,\"hologram2\"'), 137 | call('+UPSD', '0,7,\"0.0.0.0\"'), 138 | call('+UPSDA', '0,3', timeout=30)] 139 | mock_set.assert_has_calls(expected_calls, any_order=True) 140 | 141 | # SMS 142 | 143 | def test_get_sms(no_serial_port, get_sms): 144 | modem = Modem() 145 | res = modem.popReceivedSMS() 146 | assert(res.sender == '447937405250') 147 | assert(res.timestamp == datetime.utcfromtimestamp(1498264009)) 148 | assert(res.message == 'Test 123') 149 | 150 | # WRITE SOCKET 151 | 152 | def test_socket_write_under_512(no_serial_port, override_command_result): 153 | modem = Modem() 154 | data = '{message:{fill}{align}{width}}'.format(message='Test-', fill='@', align='<', width=64) 155 | modem.write_socket(data.encode()) 156 | 157 | def test_socket_write_over_512(no_serial_port, override_command_result): 158 | modem = Modem() 159 | data = '{message:{fill}{align}{width}}'.format(message='Test-', fill='@', align='<', width=600) 160 | modem.write_socket(data.encode()) 161 | 162 | # DEBUGWRITE 163 | 164 | def test_debugwrite(no_serial_port): 165 | modem = Modem() 166 | assert(modem.debug_out == '') 167 | modem.debugwrite('test') 168 | assert(modem.debug_out == 'test') 169 | 170 | modem.debugwrite('test222', hide=True) 171 | assert(modem.debug_out == 'test') # debug_out shouldn't change since hide is enabled. 172 | 173 | # MODEMWRITE 174 | 175 | def test_modemwrite(no_serial_port): 176 | modem = Modem() 177 | assert(modem.debug_out == '') 178 | 179 | # use all method arg default values. 180 | modem.modemwrite('test-cmd') 181 | assert(modem.debug_out == 'test-cmd') 182 | 183 | modem.modemwrite('test2', start=True) 184 | assert(modem.debug_out == '[test2') 185 | 186 | modem.modemwrite('test3', start=True, hide=True) 187 | # This should be the same as the previous debug_out because hide is enabled. 188 | assert(modem.debug_out == '[test2') 189 | 190 | modem.modemwrite('test4', start=True, end=True) 191 | assert(modem.debug_out == '[test4]') 192 | 193 | modem.modemwrite('test5', start=True, at=True, seteq=True, read=True, end=True) 194 | assert(modem.debug_out == '[ATtest5=?]') 195 | 196 | # COMMAND_RESULT 197 | 198 | def test_command_result(no_serial_port): 199 | 200 | modem = Modem() 201 | 202 | # OK with an empty response list. 203 | assert(modem.result == ModemResult.OK) 204 | result, resp = modem._command_result() 205 | 206 | assert(result == ModemResult.OK) 207 | assert(resp == []) 208 | 209 | # OK with a response list of one element. 210 | modem.result = ModemResult.OK 211 | modem.response = ['test1'] 212 | result, resp = modem._command_result() 213 | 214 | assert(result == ModemResult.OK) 215 | assert(resp == 'test1') # should return just a string 216 | 217 | # INVALID 218 | modem.result = ModemResult.Invalid 219 | modem.response = ['test1', 'test2', 'test3'] 220 | 221 | result, resp = modem._command_result() 222 | 223 | assert(result == ModemResult.Invalid) 224 | assert(resp == ['test1', 'test2', 'test3']) 225 | 226 | # NOMATCH 227 | modem.result = ModemResult.NoMatch 228 | # This should still be a list since it's not ModemResult.OK. 229 | modem.response = ['test1'] 230 | result, resp = modem._command_result() 231 | 232 | assert(result == ModemResult.NoMatch) 233 | assert(resp == ['test1']) 234 | 235 | # ERROR 236 | modem.result = ModemResult.Error 237 | modem.response = [] 238 | result, resp = modem._command_result() 239 | 240 | assert(result == ModemResult.Error) 241 | assert(resp == []) 242 | 243 | # TIMEOUT 244 | modem.result = ModemResult.Timeout 245 | result, resp = modem._command_result() 246 | 247 | assert(result == ModemResult.Timeout) 248 | assert(resp == []) 249 | 250 | # HANDLEURC 251 | 252 | # These are static methods that can be tested independently. 253 | # We decided to wrap it all here under this test object 254 | class TestModemProtectedStaticMethods: 255 | 256 | def test_check_registered_string(self): 257 | result = '+CREG: 2,5,"5585","404C790",6' 258 | registered = Modem._check_registered_helper('+CREG', result) 259 | assert(registered) 260 | 261 | def test_registered_basic_unregistered_string(self): 262 | # This should force strips left and right, but the return value will 263 | # still be false since 3 is elem 1 in [2, 3, 2] 264 | result = '2, 3, 2' 265 | registered = Modem._check_registered_helper('+CREG', result) 266 | assert(registered == False) 267 | 268 | def test_registered_empty_string(self): 269 | result = '' 270 | with pytest.raises(SerialError) as e: 271 | registered = Modem._check_registered_helper('+CREG', result) 272 | 273 | def test_check_registered_short_list(self): 274 | result = ['+CREG: 5,"5585","404C78A",6', 275 | '+CREG: 5,"5585","404C790",6', 276 | '+CREG: 2,5,"5585","404C790",6'] 277 | registered = Modem._check_registered_helper('+CREG', result) 278 | assert(registered) 279 | 280 | def test_registered_empty_list(self): 281 | result = [] 282 | with pytest.raises(SerialError) as e: 283 | registered = Modem._check_registered_helper('+CREG', result) 284 | 285 | def test_check_registered_long_list(self): 286 | result = ['+CREG: 5,"5585","404EF4D",6', 287 | '+CREG: 5,"5585","404C816",6', 288 | '+CREG: 5,"5585","404C790",6', 289 | '+CREG: 5,"5585","404C816",6', 290 | '+CREG: 5,"5585","404EF4D",6', 291 | '+CREG: 5,"5585","404C78A",6', 292 | '+CREG: 5,"5585","404C790",6', 293 | '+CREG: 5,"5585","404C816",6', 294 | '+CREG: 2', 295 | '+CREG: 5,"5585","404C790",6', 296 | '+CREG: 2,5,"5585","404C790",6'] 297 | registered = Modem._check_registered_helper('+CREG', result) 298 | assert(registered) 299 | -------------------------------------------------------------------------------- /tests/Modem/test_Nova.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2017 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_Nova.py - This file implements unit tests for the Nova modem interface. 8 | 9 | import pytest 10 | import sys 11 | 12 | from Hologram.Network.Modem.Nova import Nova 13 | 14 | sys.path.append(".") 15 | sys.path.append("..") 16 | sys.path.append("../..") 17 | 18 | def mock_write(nova, message): 19 | return True 20 | 21 | def mock_read(nova): 22 | return True 23 | 24 | def mock_readline(nova, timeout=None, hide=False): 25 | return '' 26 | 27 | def mock_open_serial_port(nova, device_name=None): 28 | return True 29 | 30 | def mock_close_serial_port(nova): 31 | return True 32 | 33 | def mock_detect_usable_serial_port(nova, stop_on_first=True): 34 | return '/dev/ttyUSB0' 35 | 36 | @pytest.fixture 37 | def no_serial_port(monkeypatch): 38 | monkeypatch.setattr(Nova, '_read_from_serial_port', mock_read) 39 | monkeypatch.setattr(Nova, '_readline_from_serial_port', mock_readline) 40 | monkeypatch.setattr(Nova, '_write_to_serial_port_and_flush', mock_write) 41 | monkeypatch.setattr(Nova, 'openSerialPort', mock_open_serial_port) 42 | monkeypatch.setattr(Nova, 'closeSerialPort', mock_close_serial_port) 43 | monkeypatch.setattr(Nova, 'detect_usable_serial_port', mock_detect_usable_serial_port) 44 | 45 | def test_init_nova_no_args(no_serial_port): 46 | modem = Nova() 47 | assert(modem.timeout == 1) 48 | assert(modem.socket_identifier == 0) 49 | assert(modem.chatscript_file.endswith('/chatscripts/default-script')) 50 | assert(modem._at_sockets_available == False) 51 | 52 | def test_init_nova_chatscriptfileoverride(no_serial_port): 53 | modem = Nova(chatscript_file='test-chatscript') 54 | assert(modem.timeout == 1) 55 | assert(modem.socket_identifier == 0) 56 | assert(modem.chatscript_file == 'test-chatscript') 57 | -------------------------------------------------------------------------------- /tests/Modem/test_NovaM.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2018 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_NovaM.py - This file implements unit tests for the NovaM modem interface. 8 | 9 | from mock import patch 10 | import pytest 11 | import sys 12 | 13 | from Hologram.Network.Modem.NovaM import NovaM 14 | 15 | sys.path.append(".") 16 | sys.path.append("..") 17 | sys.path.append("../..") 18 | 19 | def mock_write(nova, message): 20 | return True 21 | 22 | def mock_read(nova): 23 | return True 24 | 25 | def mock_readline(nova, timeout=None, hide=False): 26 | return '' 27 | 28 | def mock_open_serial_port(nova, device_name=None): 29 | return True 30 | 31 | def mock_close_serial_port(nova): 32 | return True 33 | 34 | def mock_detect_usable_serial_port(nova, stop_on_first=True): 35 | return '/dev/ttyUSB0' 36 | 37 | @pytest.fixture 38 | def no_serial_port(monkeypatch): 39 | monkeypatch.setattr(NovaM, '_read_from_serial_port', mock_read) 40 | monkeypatch.setattr(NovaM, '_readline_from_serial_port', mock_readline) 41 | monkeypatch.setattr(NovaM, '_write_to_serial_port_and_flush', mock_write) 42 | monkeypatch.setattr(NovaM, 'openSerialPort', mock_open_serial_port) 43 | monkeypatch.setattr(NovaM, 'closeSerialPort', mock_close_serial_port) 44 | monkeypatch.setattr(NovaM, 'detect_usable_serial_port', mock_detect_usable_serial_port) 45 | monkeypatch.setattr(NovaM, 'modem_id', 'Sara-R410M-02B') 46 | 47 | def test_init_novam_no_args(no_serial_port): 48 | modem = NovaM() 49 | assert(modem.timeout == 1) 50 | assert(modem.socket_identifier == 0) 51 | assert(modem.chatscript_file.endswith('/chatscripts/default-script')) 52 | assert(modem._at_sockets_available) 53 | assert(modem.description == 'Hologram Nova US 4G LTE Cat-M1 Cellular USB Modem (R410)') 54 | 55 | def test_disable_at_sockets_mode(no_serial_port): 56 | modem = NovaM() 57 | assert(modem._at_sockets_available) 58 | modem.disable_at_sockets_mode() 59 | assert(modem._at_sockets_available == False) 60 | 61 | @patch.object(NovaM, 'check_registered') 62 | def test_is_registered(mock_check_registered, no_serial_port): 63 | modem = NovaM() 64 | mock_check_registered.return_value = True 65 | mock_check_registered.reset_mock() 66 | assert(modem.is_registered()) 67 | mock_check_registered.assert_called_once_with('+CEREG') 68 | 69 | @patch.object(NovaM, 'set') 70 | def test_close_socket_no_args(mock_set, no_serial_port): 71 | modem = NovaM() 72 | assert(modem.socket_identifier == 0) 73 | mock_set.return_value = (0,0) 74 | mock_set.reset_mock() 75 | modem.close_socket() 76 | mock_set.assert_called_once_with('+USOCL', '0') 77 | 78 | @patch.object(NovaM, 'set') 79 | def test_close_socket_with_socket_identifier(mock_set, no_serial_port): 80 | modem = NovaM() 81 | mock_set.return_value = (0,0) 82 | mock_set.reset_mock() 83 | modem.close_socket(5) 84 | mock_set.assert_called_once_with('+USOCL', '5') 85 | 86 | @patch.object(NovaM, 'command') 87 | def test_set_network_registration_status(mock_command, no_serial_port): 88 | modem = NovaM() 89 | mock_command.return_value = [] 90 | mock_command.reset_mock() 91 | modem.set_network_registration_status() 92 | mock_command.assert_called_once_with('+CEREG', '2') 93 | -------------------------------------------------------------------------------- /tests/Modem/test_NovaU201.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2017 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_NovaU201.py - This file implements unit tests for the Nova_U201 modem interface. 8 | 9 | from mock import call, patch 10 | import pytest 11 | import sys 12 | 13 | from Hologram.Network.Modem.Nova_U201 import Nova_U201 14 | 15 | sys.path.append(".") 16 | sys.path.append("..") 17 | sys.path.append("../..") 18 | 19 | def mock_write(nova, message): 20 | return True 21 | 22 | def mock_read(nova): 23 | return True 24 | 25 | def mock_readline(nova, timeout=None, hide=False): 26 | return '' 27 | 28 | def mock_open_serial_port(nova, device_name=None): 29 | return True 30 | 31 | def mock_close_serial_port(nova): 32 | return True 33 | 34 | def mock_detect_usable_serial_port(nova, stop_on_first=True): 35 | return '/dev/ttyUSB0' 36 | 37 | @pytest.fixture 38 | def no_serial_port(monkeypatch): 39 | monkeypatch.setattr(Nova_U201, '_read_from_serial_port', mock_read) 40 | monkeypatch.setattr(Nova_U201, '_readline_from_serial_port', mock_readline) 41 | monkeypatch.setattr(Nova_U201, '_write_to_serial_port_and_flush', mock_write) 42 | monkeypatch.setattr(Nova_U201, 'openSerialPort', mock_open_serial_port) 43 | monkeypatch.setattr(Nova_U201, 'closeSerialPort', mock_close_serial_port) 44 | monkeypatch.setattr(Nova_U201, 'detect_usable_serial_port', mock_detect_usable_serial_port) 45 | 46 | def test_init_nova_u201_no_args(no_serial_port): 47 | modem = Nova_U201() 48 | assert(modem.timeout == 1) 49 | assert(modem.socket_identifier == 0) 50 | assert(modem.chatscript_file.endswith('/chatscripts/default-script')) 51 | assert(modem._at_sockets_available) 52 | assert(modem.description == 'Hologram Nova Global 3G/2G Cellular USB Modem (U201)') 53 | assert(modem.last_location is None) 54 | 55 | def test_disable_at_sockets_mode(no_serial_port): 56 | modem = Nova_U201() 57 | assert(modem._at_sockets_available) 58 | modem.disable_at_sockets_mode() 59 | assert(modem._at_sockets_available == False) 60 | 61 | @patch.object(Nova_U201, 'check_registered') 62 | def test_is_registered(mock_check_registered, no_serial_port): 63 | modem = Nova_U201() 64 | mock_check_registered.return_value = False 65 | mock_check_registered.reset_mock() 66 | assert(modem.is_registered() == False) 67 | calls = [call('+CREG'), call('+CGREG')] 68 | mock_check_registered.assert_has_calls(calls, any_order=True) 69 | 70 | @patch.object(Nova_U201, 'command') 71 | def test_set_network_registration_status(mock_command, no_serial_port): 72 | modem = Nova_U201() 73 | mock_command.return_value = [] 74 | mock_command.reset_mock() 75 | modem.set_network_registration_status() 76 | calls = [call('+CREG', '2'), call('+CGREG', '2')] 77 | mock_command.assert_has_calls(calls) 78 | 79 | def test_parse_and_populate_last_sim_otp_response(no_serial_port): 80 | modem = Nova_U201() 81 | 82 | test_response = "01,\"test1\"" 83 | modem.parse_and_populate_last_sim_otp_response(test_response) 84 | assert(modem.last_sim_otp_command_response == 'test1') 85 | 86 | # Should still strip away the last element - test3 87 | test_response = "01,\"test2,test3\"" 88 | modem.parse_and_populate_last_sim_otp_response(test_response) 89 | assert(modem.last_sim_otp_command_response == 'test3') 90 | -------------------------------------------------------------------------------- /tests/Modem/test_Quectel.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2017 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_Quectel.py - This file implements unit tests for the Quectel modem interface. 8 | 9 | from unittest.mock import patch, call 10 | import pytest 11 | import sys 12 | 13 | from Hologram.Network.Modem.Quectel import Quectel 14 | from Hologram.Network.Modem.Modem import Modem 15 | from UtilClasses import ModemResult 16 | 17 | sys.path.append(".") 18 | sys.path.append("..") 19 | sys.path.append("../..") 20 | 21 | 22 | def mock_write(modem, message): 23 | return True 24 | 25 | 26 | def mock_read(modem): 27 | return True 28 | 29 | 30 | def mock_readline(modem, timeout=None, hide=False): 31 | return "" 32 | 33 | 34 | def mock_open_serial_port(modem, device_name=None): 35 | return True 36 | 37 | 38 | def mock_close_serial_port(modem): 39 | return True 40 | 41 | 42 | def mock_detect_usable_serial_port(modem, stop_on_first=True): 43 | return "/dev/ttyUSB0" 44 | 45 | 46 | @pytest.fixture 47 | def no_serial_port(monkeypatch): 48 | monkeypatch.setattr(Quectel, "_read_from_serial_port", mock_read) 49 | monkeypatch.setattr(Quectel, "_readline_from_serial_port", mock_readline) 50 | monkeypatch.setattr(Quectel, "_write_to_serial_port_and_flush", mock_write) 51 | monkeypatch.setattr(Quectel, "openSerialPort", mock_open_serial_port) 52 | monkeypatch.setattr(Quectel, "closeSerialPort", mock_close_serial_port) 53 | monkeypatch.setattr(Quectel, "detect_usable_serial_port", mock_detect_usable_serial_port) 54 | 55 | 56 | def test_init_Quectel_no_args(no_serial_port): 57 | modem = Quectel() 58 | assert modem.timeout == 1 59 | assert modem.socket_identifier == 0 60 | assert modem.chatscript_file.endswith("/chatscripts/default-script") 61 | assert modem._at_sockets_available 62 | 63 | @patch.object(Quectel, "check_registered") 64 | @patch.object(Quectel, "set") 65 | @patch.object(Quectel, "command") 66 | def test_create_socket(mock_command, mock_set, mock_check, no_serial_port): 67 | modem = Quectel() 68 | modem.apn = 'test' 69 | mock_check.return_value = True 70 | # The PDP context is not active 71 | mock_command.return_value = (ModemResult.OK, '+QIACT: 0,0') 72 | mock_set.return_value = (ModemResult.OK, None) 73 | modem.create_socket() 74 | mock_command.assert_called_with("+QIACT?") 75 | mock_set.assert_has_calls( 76 | [ 77 | call("+QICSGP", '1,1,\"test\",\"\",\"\",1'), 78 | call("+QIACT", '1', timeout=30) 79 | ], 80 | any_order=True 81 | ) 82 | 83 | @patch.object(Quectel, "command") 84 | def test_connect_socket(mock_command, no_serial_port): 85 | modem = Quectel() 86 | modem.socket_identifier = 1 87 | host = "hologram.io" 88 | port = 9999 89 | modem.connect_socket(host, port) 90 | mock_command.assert_called_with("+QIOPEN", '1,0,"TCP","%s",%d,0,1' % (host, port)) 91 | 92 | 93 | @patch.object(Quectel, "set") 94 | def test_write_socket_small(mock_command, no_serial_port): 95 | modem = Quectel() 96 | modem.socket_identifier = 1 97 | data = b"Message smaller than 510 bytes" 98 | mock_command.return_value = (ModemResult.OK, None) 99 | modem.write_socket(data) 100 | mock_command.assert_called_with( 101 | "+QISENDEX", 102 | '1,"4d65737361676520736d616c6c6572207468616e20353130206279746573"', 103 | timeout=10, 104 | ) 105 | 106 | 107 | @patch.object(Quectel, "set") 108 | def test_write_socket_large(mock_command, no_serial_port): 109 | modem = Quectel() 110 | modem.socket_identifier = 1 111 | data = b"a" * 300 112 | mock_command.return_value = (ModemResult.OK, None) 113 | modem.write_socket(data) 114 | mock_command.assert_has_calls( 115 | [ 116 | call("+QISENDEX", '1,"%s"' % ("61" * 255), timeout=10), 117 | call("+QISENDEX", '1,"%s"' % ("61" * 45), timeout=10), 118 | ], 119 | any_order=True, 120 | ) 121 | 122 | @patch.object(Quectel, "set") 123 | def test_read_socket(mock_command, no_serial_port): 124 | modem = Quectel() 125 | modem.socket_identifier = 1 126 | mock_command.return_value = (ModemResult.OK, '+QIRD: "Some val"') 127 | # Double quotes should be stripped from the reutrn value 128 | assert (modem.read_socket(payload_length=10) == 'Some val') 129 | mock_command.assert_called_with("+QIRD", '1,10') 130 | 131 | def test_handle_open_urc(no_serial_port): 132 | modem = Quectel() 133 | modem.handleURC('+QIOPEN: 1,0') 134 | assert modem.urc_state == Modem.SOCKET_WRITE_STATE 135 | assert modem.socket_identifier == 1 136 | 137 | def test_handle_received_data_urc(no_serial_port): 138 | modem = Quectel() 139 | modem.handleURC('+QIURC: \"recv\",1,25') 140 | assert modem.urc_state == Modem.SOCKET_SEND_READ 141 | assert modem.socket_identifier == 1 142 | assert modem.last_read_payload_length == 25 143 | assert modem.urc_response == "" 144 | 145 | def test_handle_socket_closed_urc(no_serial_port): 146 | modem = Quectel() 147 | modem.handleURC('+QIURC: \"closed\",1') 148 | assert modem.urc_state == Modem.SOCKET_CLOSED 149 | assert modem.socket_identifier == 1 150 | 151 | -------------------------------------------------------------------------------- /tests/ModemMode/test_ModemMode.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2016 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_ModemMode.py - This file implements unit tests for the ModemMode class. 8 | 9 | import sys 10 | sys.path.append(".") 11 | sys.path.append("..") 12 | sys.path.append("../..") 13 | from Hologram.Network.Modem.ModemMode.ModemMode import ModemMode 14 | 15 | class TestModemMode: 16 | 17 | def test_modem_mode_create(self): 18 | modem_mode = ModemMode(device_name='/dev/ttyUSB0', baud_rate='9600') 19 | 20 | assert modem_mode.device_name == '/dev/ttyUSB0' 21 | assert modem_mode.baud_rate == '9600' 22 | -------------------------------------------------------------------------------- /tests/ModemMode/test_PPP.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2016 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_PPP.py - This file implements unit tests for the PPP class. 8 | 9 | import sys 10 | import pytest 11 | sys.path.append(".") 12 | sys.path.append("..") 13 | sys.path.append("../..") 14 | from Hologram.Network.Modem.ModemMode.MockPPP import MockPPP 15 | 16 | class TestPPP: 17 | 18 | def test_ppp_create(self): 19 | ppp = MockPPP(chatscript_file='test') 20 | 21 | assert ppp.device_name == '/dev/ttyUSB0' 22 | assert ppp.baud_rate == '9600' 23 | assert ppp.localIPAddress is None 24 | assert ppp.remoteIPAddress is None 25 | assert ppp.connect_script == '/usr/sbin/chat -v -f test' 26 | 27 | def test_ppp_invalid_chatscript_create(self): 28 | with pytest.raises(Exception, match='Must specify chatscript file'): 29 | ppp = MockPPP() 30 | -------------------------------------------------------------------------------- /tests/Network/test_Cellular.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2016 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_Cellular.py - This file implements unit tests for the Cellular class. 8 | 9 | import sys 10 | import pytest 11 | 12 | sys.path.append(".") 13 | sys.path.append("..") 14 | sys.path.append("../..") 15 | from Hologram.Network import Cellular 16 | 17 | class TestCellular: 18 | 19 | def test_invalid_cellular_type(self): 20 | pass 21 | -------------------------------------------------------------------------------- /tests/Network/test_Ethernet.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2016 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_Ethernet.py - This file implements unit tests for the Ethernet class. 8 | 9 | import sys 10 | import pytest 11 | 12 | sys.path.append(".") 13 | sys.path.append("..") 14 | sys.path.append("../..") 15 | from Hologram.Network import Ethernet 16 | 17 | class TestEthernet: 18 | 19 | def test_Ethernet(self): 20 | ethernet = Ethernet.Ethernet() 21 | assert ethernet.interfaceName == 'eth0' 22 | 23 | ethernet.interfaceName = 'eth1' 24 | assert ethernet.interfaceName == 'eth1' 25 | 26 | def test_Ethernet_with_specified_interface(self): 27 | 28 | ethernet = Ethernet.Ethernet(interfaceName = 'eth2') 29 | assert ethernet.interfaceName == 'eth2' 30 | 31 | def test_get_invalid_signal_strength(self): 32 | ethernet = Ethernet.Ethernet() 33 | with pytest.raises(Exception, match = 'Ethernet mode doesn\'t support this call'): 34 | connectionStatus = ethernet.getAvgSignalStrength() 35 | -------------------------------------------------------------------------------- /tests/Network/test_Network.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2016 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_Network.py - This file implements unit tests for the Network class. 8 | 9 | import sys 10 | import pytest 11 | 12 | sys.path.append(".") 13 | sys.path.append("..") 14 | sys.path.append("../..") 15 | from Hologram.Network import Network 16 | 17 | class TestNetwork: 18 | 19 | def test_create_network(self): 20 | network = Network() 21 | 22 | def test_get_invalid_connection_status(self): 23 | network = Network() 24 | with pytest.raises(Exception, match = 'Must instantiate a defined Network type'): 25 | connectionStatus = network.getConnectionStatus() 26 | 27 | def test_get_invalid_signal_strength(self): 28 | network = Network() 29 | with pytest.raises(Exception, match = 'Must instantiate a defined Network type'): 30 | connectionStatus = network.getSignalStrength() 31 | -------------------------------------------------------------------------------- /tests/Network/test_NetworkManager.py: -------------------------------------------------------------------------------- 1 | # Author: Hologram 2 | # 3 | # Copyright 2016 - Hologram (Konekt, Inc.) 4 | # 5 | # LICENSE: Distributed under the terms of the MIT License 6 | # 7 | # test_NetworkManager.py - This file implements unit tests for the NetworkManager class. 8 | 9 | import sys 10 | import pytest 11 | 12 | sys.path.append(".") 13 | sys.path.append("..") 14 | sys.path.append("../..") 15 | from Hologram.Network import NetworkManager 16 | 17 | class TestNetworkManager: 18 | 19 | def test_create_non_network(self): 20 | networkManager = NetworkManager.NetworkManager(None, '') 21 | assert networkManager.networkActive 22 | assert repr(networkManager) == 'Network Agnostic Mode' 23 | 24 | def test_invalid_create(self): 25 | with pytest.raises(Exception, match = 'Invalid network type: invalid'): 26 | networkManager = NetworkManager.NetworkManager(None, 'invalid') 27 | 28 | def test_invalid_ppp_create(self): 29 | with pytest.raises(Exception, match = 'Invalid network type: invalid-ppp'): 30 | networkManager = NetworkManager.NetworkManager(None, 'invalid-ppp') 31 | 32 | def test_network_connected(self): 33 | networkManager = NetworkManager.NetworkManager(None, '') 34 | networkManager.networkConnected() 35 | assert networkManager.networkActive 36 | 37 | def test_network_disconnected(self): 38 | networkManager = NetworkManager.NetworkManager(None, '') 39 | networkManager.networkDisconnected() 40 | assert networkManager.networkActive == False 41 | -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Author: Hologram 3 | # 4 | # Copyright 2016 - Hologram (Konekt, Inc.) 5 | # 6 | # LICENSE: Distributed under the terms of the MIT License 7 | # 8 | # update.sh - This file helps update this Python SDK and all required dependencies 9 | # on a machine. 10 | 11 | set -euo pipefail 12 | 13 | required_programs=('python3' 'pip3' 'ps' 'kill' 'libpython3.9-dev') 14 | OS='' 15 | 16 | # Check OS. 17 | if [ "$(uname)" == "Darwin" ]; then 18 | 19 | echo 'Darwin system detected' 20 | OS='DARWIN' 21 | 22 | elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then 23 | 24 | echo 'Linux system detected' 25 | OS='LINUX' 26 | required_programs+=('ip' 'pppd') 27 | 28 | elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW32_NT" ]; then 29 | 30 | echo 'Windows 32-bit system detected' 31 | OS='WINDOWS' 32 | 33 | elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW64_NT" ]; then 34 | 35 | echo 'Windows 64-bit system detected' 36 | OS='WINDOWS' 37 | fi 38 | 39 | # Error out on unsupported OS. 40 | if [ "$OS" == 'DARWIN' ] || [ "$OS" == 'WINDOWS' ]; then 41 | echo "$OS is not supported right now" 42 | exit 1 43 | fi 44 | 45 | function pause() { 46 | read -p "$*" 47 | } 48 | 49 | function install_software() { 50 | if [ "$OS" == 'LINUX' ]; then 51 | sudo apt -y install "$*" 52 | elif [ "$OS" == 'DARWIN' ]; then 53 | brew install "$*" 54 | echo 'TODO: macOS should go here' 55 | elif [ "$OS" == 'WINDOWS' ]; then 56 | echo 'TODO: windows should go here' 57 | fi 58 | } 59 | 60 | function check_python_version() { 61 | if ! python3 -V | grep -E '3.(9|1[012]).[0-9]' > /dev/null 2>&1; then 62 | echo "An unsupported version of python 3 is installed. Must have python 3.9+ installed to use the Hologram SDK" 63 | exit 1 64 | fi 65 | } 66 | 67 | # EFFECTS: Returns true if the specified program is installed, false otherwise. 68 | function check_if_installed() { 69 | if command -v "$*" >/dev/null 2>&1; then 70 | return 0 71 | else 72 | return 1 73 | fi 74 | } 75 | 76 | function update_repository() { 77 | if [ "$OS" == 'LINUX' ]; then 78 | sudo apt update 79 | elif [ "$OS" == 'DARWIN' ]; then 80 | brew update 81 | echo 'TODO: macOS should go here' 82 | elif [ "$OS" == 'WINDOWS' ]; then 83 | echo 'TODO: windows should go here' 84 | fi 85 | } 86 | 87 | # EFFECTS: Verifies that all required software is installed. 88 | function verify_installation() { 89 | echo 'Verifying that all dependencies are installed correctly...' 90 | # Verify pip packages 91 | INSTALLED_PIP_PACKAGES="$(pip3 list)" 92 | 93 | if ! [[ "$INSTALLED_PIP_PACKAGES" == *"python-sdk-auth"* ]]; then 94 | echo 'Cannot find python-sdk-auth. Please rerun the install script.' 95 | exit 1 96 | fi 97 | 98 | if ! [[ "$INSTALLED_PIP_PACKAGES" == *"hologram-python"* ]]; then 99 | echo 'Cannot find hologram-python. Please rerun the install script.' 100 | exit 1 101 | fi 102 | 103 | echo 'You are now ready to use the Hologram Python SDK!' 104 | } 105 | 106 | check_python_version 107 | 108 | update_repository 109 | 110 | # Check if an older version exists and uninstall it 111 | if command hologram version | grep '0.[0-8]'; then 112 | echo "Found a previous version of the SDK on Python 2. The new update uses" 113 | echo "Python 3 and is not compatible with Python 2. This script will uninstall" 114 | echo "the previous SDK version, install Python 3 and then install the new" 115 | echo "Python 3 version of the SDK. It will not make Python 3 your default" 116 | echo "Python version." 117 | pause "Press [Enter] key to continue..."; 118 | sudo pip uninstall -y hologram-python 119 | fi 120 | 121 | # Iterate over all programs to see if they are installed 122 | # Installs them if necessary 123 | for program in ${required_programs[*]} 124 | do 125 | if [ "$program" == 'pppd' ]; then 126 | if ! check_if_installed "$program"; then 127 | pause "Installing $program. Press [Enter] key to continue..."; 128 | install_software 'ppp' 129 | fi 130 | elif [ "$program" == 'pip3' ]; then 131 | if ! check_if_installed "$program"; then 132 | pause "Installing $program. Press [Enter] key to continue..."; 133 | install_software 'python3-pip' 134 | fi 135 | if ! pip3 -V | grep -E '3.(9|1[012])' >/dev/null 2>&1; then 136 | echo "pip3 is installed for an unsupported version of python." 137 | exit 1 138 | fi 139 | elif check_if_installed "$program"; then 140 | echo "$program is already installed." 141 | else 142 | pause "Installing $program. Press [Enter] key to continue..."; 143 | install_software "$program" 144 | fi 145 | done 146 | 147 | # Install SDK itself. 148 | sudo pip3 install hologram-python --upgrade 149 | 150 | verify_installation 151 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 0.10.1 2 | --------------------------------------------------------------------------------