├── .gitignore
├── LICENSE
├── README.md
├── bin
└── tmo-monitor.py
├── example.env
├── requirements.txt
├── setup.py
├── tmo-monitor-logo.png
└── tmo_monitor
├── __init__.py
├── configuration.py
├── gateway
├── arcadyan.py
├── base.py
├── model.py
└── nokia.py
└── status.py
/.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 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 highvolt-dev
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # tmo-monitor
6 |
7 | A lightweight, cross-platform Python 3 script that can monitor the T-Mobile Home Internet Nokia, Arcadyan, and Sagecom 5G Gateways for 4G/5G bands, cellular site (tower), and internet connectivity and reboots as needed or on-demand.
8 |
9 | By default, checks for n41 5G signal and connectivity to google.com via ping.
10 |
11 | ## Getting Started
12 |
13 | ### Install dependencies
14 |
15 | `pip3 install .`
16 |
17 | The command will then be available anywhere as `tmo-monitor.py`.
18 |
19 | #### Windows
20 |
21 | 1. On Windows, open the folder where you downloaded the project.
22 | 2. Click `File` > `Open Windows PowerShell`
23 | 3. Run the above `pip3 install .` command.
24 | 4. To use, either:
25 | - Run `cmd.exe` within PowerShell first
26 | - Open `cmd.exe` (Command Prompt) instead of PowerShell
27 | - Run `python bin/tmo-monitor.py` in PowerShell from inside the project directory
28 |
29 | When in doubt, consult this document or run `tmo-monitor.py --help`.
30 |
31 | ## Usage
32 |
33 | ### Command line usage
34 | ```
35 | usage: tmo-monitor.py [-h] [--connectivity-check {ping,http}]
36 | [-I INTERFACE]
37 | [--http-target HTTP_TARGET] [--status-code {[100,600)}]
38 | [-H PING_HOST] [--ping-count PING_COUNT] [--ping-interval PING_INTERVAL] [-6]
39 | [-R] [-r]
40 | [--skip-bands] [--skip-5g-bands] [--skip-connectivity-check] [--skip-enbid]
41 | [--uptime UPTIME]
42 | [-4 {B2,B4,B5,B12,B13,B25,B26,B41,B46,B48,B66,B71}] [-5 {n41,n71}]
43 | [--enbid ENBID]
44 | [--print-config]
45 | [--logfile LOGFILE] [--log-all] [--log-delta] [--syslog]
46 | [--model {NOK5G21,ARCKVD21,FAST5688W}]
47 | [username] [password]
48 |
49 | Check T-Mobile Home Internet cellular band(s) and connectivity and reboot if necessary
50 |
51 | positional arguments:
52 | username the username (most likely "admin")
53 | password the administrative password (will be requested at runtime if not passed as argument)
54 |
55 | optional arguments:
56 | -h, --help show this help message and exit
57 | --connectivity-check {ping,http}
58 | type of connectivity check to perform (defaults to ping)
59 | -I INTERFACE, --interface INTERFACE
60 | the network interface to use for ping. pass the source IP on Windows
61 | --http-target HTTP_TARGET
62 | the URL to perform a http check against (defaults to https://google.com/generate_204)
63 | --status-code {[100,600)}
64 | expected HTTP status code for http connectivity check (defaults to 204)
65 | -H PING_HOST, --ping-host PING_HOST
66 | the host to ping (defaults to google.com)
67 | --ping-count PING_COUNT
68 | how many ping health checks to perform (defaults to 1)
69 | --ping-interval PING_INTERVAL
70 | how long in seconds to wait between ping health checks (defaults to 10)
71 | -6, --ping-6 use IPv6 ping
72 | -R, --reboot skip health checks and immediately reboot gateway
73 | -r, --skip-reboot skip rebooting gateway
74 | --skip-bands skip check for connected 4g band
75 | --skip-5g-bands skip check for connected 5g band
76 | --skip-connectivity-check, --skip-ping
77 | skip connectivity check
78 | --skip-enbid skip check for connected eNB ID
79 | --uptime UPTIME how long the gateway must be up before considering a reboot (defaults to 90 seconds)
80 | -4 {B2,B4,B5,B12,B13,B25,B26,B41,B46,B48,B66,B71}, --4g-band {B2,B4,B5,B12,B13,B25,B26,B41,B46,B48,B66,B71}
81 | the 4g band(s) to check
82 | -5 {n41,n71}, --5g-band {n41,n71}
83 | the 5g band(s) to check (defaults to n41)
84 | --enbid ENBID check for a connection to a given eNB ID
85 | --print-config output configuration settings
86 | --logfile LOGFILE output file for logging
87 | --log-all always write connection details to logfile
88 | --log-delta write connection details to logfile on change
89 | --syslog log to syslog
90 | --model {NOK5G21,ARCKVD21,FAST5688W}
91 | the gateway model (defaults to NOK5G21)
92 | ```
93 |
94 | ## Options
95 |
96 | ### Gateway Model
97 | **Gateway Model:** `--model`
98 |
99 | By default, the script will assume the silver-colored Nokia NOK5G21 gateway is being used.
100 |
101 | Valid values are `NOK5G21` for the Nokia gateway, `ARCKVD21` for the square, black-colored Arcadyan gateway without top vent holes, or `FAST5688W` for the square, black-colored Sagecom gateway with top vent holes.
102 |
103 | ### Connectivity check
104 | **Mode:** `--connectivity-check`
105 | Defaults to `ping`. Can instead use a HTTP(S) based health check with the `http` value. The `http` health check defaults to checking `https://google.com/generate_204` and checking its status code.
106 |
107 | **Interface:** `-I --interface`
108 | Can be used to specify the network interface used by the ping command. Useful if T-Mobile Home Internet is not your default network interface: e.g., this is running on a dual WAN router. On Windows, pass the source IP address to use. `http` connectivity checks will be dictated by system routing rules.
109 |
110 | ### HTTP check
111 | **Target:** `--http-target`
112 | Defaults to `https://google.com/generate_204` - both `http` and `https` targets are supported by the `http` value of the `--connectivity-check` flag.
113 |
114 | **Status Code:** `--status-code`
115 | Defaults to `204` for use with `https://google.com/generate_204` - in most common use cases, a `200` status code is expected instead. Expects a numeric value between 100-599 (inclusive).
116 |
117 | ### Ping options
118 |
119 | `ping` checks are the default connectivity check in `tmo-monitor`. It's possible to use HTTP(S)-based checks instead. Refer to the `--connectivity-check` flag.
120 |
121 | **Ping Host:** `-H --ping-host`
122 | Defaults to `google.com` - override if you'd like to ping an alternate host to determine internet connectivity. Must specify a host if flag is provided - you can simply omit the flag if you'd like to use the default google.com ping check.
123 |
124 | **Ping Count:** `--ping-count`
125 | Defaults to `1` - override if you'd like to perform multiple ping checks before rebooting. Short-circuits if a successful ping is encountered. Will reboot if all fail.
126 |
127 | **Ping Interval:** `--ping-interval`
128 | Defaults to `10` seconds - override if you'd like to use a different interval.
129 |
130 | **Ping v6:** `-6 --ping-6`
131 | Use IPv6 ping.
132 |
133 | ### Reboot options
134 | **Reboot:** `-R --reboot`
135 | Skip health checks and immediately reboot gateway.
136 |
137 | **Skip Reboot:** `-r --skip-reboot`
138 | Skip rebooting gateway.
139 |
140 | **Skip Bands:** `--skip-bands`
141 | Skip check for connected 4g band.
142 |
143 | **Skip 5g Bands:** `--skip-5g-bands`
144 | Skip check for connected 5g band.
145 |
146 | **Skip Ping:** `--skip-connectivity-check --skip-ping`
147 | Skip check for successful connectivity check.
148 |
149 | **Uptime Threshold:** `--uptime`
150 | Defaults to 90 seconds - Specify a required uptime for an implicit reboot to occur. Intended to allow sufficient time to establish a connection and stabilize band selection. Setting is used to avoid boot looping, but is not respected when the `--reboot` flag is used.
151 |
152 | ### Connection configuration
153 | **4G Band Checking:** `-4 --4g-band`
154 | Specify a 4G band you expect the gateway to be connected to. Repeat the flag to allow multiple acceptable bands. Case-sensitive.
155 |
156 | **5G Band Checking:** `-5 --5g-band`
157 | Defaults to n41 - Specify a 5G band you expect the gateway to be connected to. Repeat the flag to allow multiple acceptable bands. Case-sensitive.
158 |
159 | **eNB ID:** `--enbid`
160 | Specify the desired cell site you expect the gateway to be connected to. Expects a numeric eNB ID to be provided. [cellmapper.net](https://www.cellmapper.net) is a helpful resource for finding eNB ID values for nearby cell sites.
161 |
162 | ### General settings
163 |
164 | **Logfile:** `--logfile LOGFILE`
165 | Output file for logging. Defaults to `tmo-monitor.log`
166 |
167 | **Log all:** `--log-all`
168 | Always write connection details to logfile. Checks all configuration settings.
169 |
170 | **Log delta:** `--log-delta`
171 | Write connection details to logfile on change of any configuration setting or long ping time.
172 |
173 | ### Default settings
174 | - Username == admin
175 | - Password -> interactive prompt
176 | - 5G band == n41
177 | - Reboot on failure to ping google.com
178 |
179 | ### Environment (`.env`) options
180 |
181 | The script is normally run in batch mode, such as scheduled through a `cron` job. Interactive command-line options are meant to be used as overrides to defaults or environment settings.
182 |
183 | A common usage pattern would be to configure the script using a `.env` file to reboot on 5G band and wifi check. When messing with the settings, a user might want to specify `--skip-reboot`. When a user knows that the reboot is needed they might specify `--reboot` for an immediate reboot.
184 |
185 | - Default settings have the lowest precendence.
186 | - Environment settings--whether in the shell environment or a `.env` file--override the defaults
187 | - Command line options have the highest precedence and override both default settings and environment settings
188 |
189 |
190 | Environment settings are meant to be declarative. They fall into four categories:
191 |
192 | - Login settings (username, password)
193 | - Configuration settings
194 | - Ping settings (target host/interface, number of pings, interval)
195 | - Connection settings (preferred band, eNB ID, etc.)
196 | - Reboot settings: request reboot on any number of failed checks.
197 | - Skip reboot overrides all reboot requests
198 | - Reboot interval overrides all reboot requests
199 | - There is no "reboot immediately" option
200 | - General settings:
201 | - Default output/silent mode _(not yet implemented)_
202 | - Logging settings
203 |
204 |
205 | ## Exit Status
206 |
207 | tmo-monitor uses the following exit status codes:
208 |
209 | - Clean execution: 0
210 | - `GENERAL_ERROR`: 1
211 | - `CONFIGURATION_ERROR`: 2
212 | - `API_ERROR`: 3
213 | - `REBOOT_PERFORMED`: 4
214 |
215 |
216 | ## Roadmap
217 |
218 | (Not yet implemented):
219 | - Alternate connectivity checks
220 | - systemd service configuration
221 |
222 | ## Tip
223 | Run this script with either a cronjob or as a systemd service to implement periodic recurring T-Mobile Home Internet health checks with automatic rebooting.
224 |
--------------------------------------------------------------------------------
/bin/tmo-monitor.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # Start by getting signal metrics
3 | import logging
4 | import logging.handlers
5 | import os
6 | import platform
7 | import sys
8 | import tailer
9 | from parse import *
10 | from tmo_monitor.gateway.model import GatewayModel
11 | from tmo_monitor.configuration import Configuration
12 | from tmo_monitor.gateway.arcadyan import CubeController
13 | from tmo_monitor.gateway.nokia import TrashCanController
14 | from tmo_monitor.status import ExitStatus
15 |
16 | # __main__
17 | if __name__ == "__main__":
18 |
19 | config = Configuration()
20 | if config.general['print_config']:
21 | config.print_config()
22 | # Set up logging for console
23 | root_logger = logging.getLogger()
24 | root_logger.setLevel(logging.DEBUG)
25 | formatter = logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', '%Y/%m/%d %H:%M:%S')
26 | console_logger = logging.StreamHandler()
27 | console_logger.setFormatter(formatter)
28 | root_logger.addHandler(console_logger)
29 | if config.general['logfile']:
30 | file_logger = logging.FileHandler(config.general['logfile'])
31 | file_logger.setLevel(logging.INFO)
32 | file_logger.setFormatter(formatter)
33 | root_logger.addHandler(file_logger)
34 | logging.debug('Enabled file logging to {}'.format(config.general['logfile']))
35 | if config.general['syslog']:
36 | syslog_handler_opts = {}
37 | syslog_logging_details = ''
38 | if platform.system() != 'Windows':
39 | for syslog_socket in ['/dev/log', '/var/run/syslog']:
40 | if os.path.exists(syslog_socket):
41 | syslog_handler_opts['address'] = syslog_socket
42 | syslog_logging_details = ' via {}'.format(syslog_socket)
43 | break
44 | syslog_logger = logging.handlers.SysLogHandler(**syslog_handler_opts)
45 | syslog_formatter = logging.Formatter('[%(levelname)s] %(message)s')
46 | syslog_logger.setFormatter(syslog_formatter)
47 | syslog_logger.setLevel(logging.INFO)
48 | syslog_logger.ident = 'tmo-monitor[{}]: '.format(os.getpid())
49 | root_logger.addHandler(syslog_logger)
50 | logging.debug('Enabled syslog logging{}'.format(syslog_logging_details))
51 | if config.reboot_now:
52 | logging.info('Immediate reboot requested.')
53 | reboot_requested = True
54 | else:
55 | reboot_requested = False
56 |
57 | log_all = False
58 | connection = dict([('4G', ''), ('5G', ''), ('enbid', ''), ('ping', '')])
59 | if config.general['log_all'] or config.general['log_delta']:
60 | log_all = True
61 |
62 | if config.model == GatewayModel.NOKIA:
63 | gw_control = TrashCanController(config.login['username'], config.login['password'])
64 | # The Arcadyan and Sagecom gateways appear to conform to the same API
65 | elif config.model in [GatewayModel.ARCADYAN, GatewayModel.SAGECOM]:
66 | gw_control = CubeController(config.login['username'], config.login['password'])
67 | else:
68 | raise Exception('Unsupported Gateway Model')
69 |
70 | if not reboot_requested:
71 |
72 | # Check for eNB ID if an eNB ID was supplied & reboot on eNB ID wasn't False in the .env
73 | if config.connection['enbid'] and config.reboot['enbid'] or log_all:
74 | site_meta = gw_control.get_site_info()
75 | connection['enbid'] = site_meta['eNBID']
76 | if (site_meta['eNBID'] != config.connection['enbid']) and config.reboot['enbid']:
77 | logging.info('Not on eNB ID ' + str(config.connection['enbid']) + ', on ' + str(site_meta['eNBID']) + '.')
78 | reboot_requested = True
79 | else:
80 | print('eNB ID check passed, on ' + str(site_meta['eNBID']) + '.')
81 |
82 | # Check for preferred bands regardless of reboot on band mismatch
83 | if config.reboot['4G_band'] or config.reboot['5G_band'] or log_all:
84 | signal_info = gw_control.get_signal_info()
85 |
86 | if config.connection['primary_band'] or log_all:
87 | primary_band = config.connection['primary_band']
88 | band_4g = signal_info['4G']
89 | connection['4G'] = band_4g
90 | if (primary_band and band_4g not in primary_band) and config.reboot['4G_band']:
91 | logging.info('Not on ' + ('one of ' if len(primary_band) > 1 else '') + ', '.join(primary_band) + '.')
92 | if config.reboot['4G_band']:
93 | reboot_requested = True
94 | else:
95 | print('Camping on ' + band_4g + '.')
96 |
97 | # 5G has a default value set (n41)
98 | secondary_band = config.connection['secondary_band']
99 | band_5g = signal_info['5G']
100 | connection['5G'] = band_5g
101 | if band_5g not in secondary_band and config.reboot['5G_band']:
102 | logging.info('Not on ' + ('one of ' if len(secondary_band) > 1 else '') + ', '.join(secondary_band) + '.')
103 | if config.reboot['5G_band']:
104 | reboot_requested = True
105 | else:
106 | print('Camping on ' + band_5g + '.')
107 |
108 | # Check for successful ping
109 | ping_ms = gw_control.ping(config.ping['ping_host'], config.ping['ping_count'], config.ping['ping_interval'], config.connectivity['interface'], config.ping['ping_6'])
110 | if log_all:
111 | connection['ping'] = ping_ms
112 | if ping_ms < 0 and config.connectivity['connectivity_check'] == 'ping':
113 | logging.error('Could not ping ' + config.ping['ping_host'] + '.')
114 | if config.reboot['ping']:
115 | reboot_requested = True
116 |
117 | # Check for successful http check
118 | if config.connectivity['connectivity_check'] == 'http':
119 | status_code = gw_control.http_check(config.http['http_target'])
120 | if status_code != config.http['status_code']:
121 | logging.error('Status code failed check for ' + config.http['http_target'] + ' - received status code ' + str(status_code))
122 | if config.reboot['http']:
123 | reboot_requested = True
124 |
125 | # Reboot if needed
126 | reboot_performed = False
127 | if (reboot_requested or log_all):
128 | connection['uptime'] = gw_control.get_uptime()
129 | if reboot_requested:
130 | if config.skip_reboot:
131 | logging.info('Not rebooting.')
132 | else:
133 | logging.info('Reboot requested.')
134 |
135 | if config.reboot_now or (connection['uptime'] >= config.reboot['uptime']):
136 | logging.info('Rebooting.')
137 | gw_control.reboot()
138 | reboot_performed = True
139 | else:
140 | logging.info('Uptime threshold not met for reboot.')
141 | else:
142 | print('No reboot necessary.')
143 |
144 | if log_all and config.general['log_delta'] and config.general['logfile']:
145 | # Tail the last 10 lines of the file (to account for logged errors) and reverse to detect the newest logline
146 | logline = tailer.tail(open(config.general['logfile']), 10)
147 | logline.reverse()
148 | for line in logline:
149 | if line.__contains__('|'):
150 | print(line)
151 | data = parse("{0} [INFO] 4G: {1} | 5G: {2} | eNB ID: {3} | Avg Ping: {4} ms | Uptime: {5} sec", line)
152 | if data[1] != connection['4G']:
153 | logging.info("4G connection is {0}, was {1}".format(connection['4G'], data[1]))
154 | config.general['log_all'] = True
155 | if data[2] != connection['5G']:
156 | logging.info("5G connection is {0}, was {1}".format(connection['5G'], data[2]))
157 | config.general['log_all'] = True
158 | if int(data[3]) != connection['enbid']:
159 | logging.info("eNB ID is {0}, was {1}".format(connection['enbid'], data[3]))
160 | config.general['log_all'] = True
161 | if int(data[4]) * 3 < connection['ping']:
162 | logging.info("Ping ms {0}, over 3x {1} ms".format(connection['ping'], data[4]))
163 | config.general['log_all'] = True
164 | if int(data[5]) > connection['uptime']:
165 | logging.info("Uptime {0} sec, less than {1} sec".format(connection['uptime'], data[5]))
166 | config.general['log_all'] = True
167 | break
168 |
169 | if log_all and config.general['log_all']:
170 | if config.general['logfile'] == '' and not config.general['syslog']:
171 | logging.error("Logging requested but file or syslog not specified")
172 | else:
173 | msg = "4G: {0} | 5G: {1} | eNB ID: {2} | Avg Ping: {3} ms | Uptime: {4} sec".format(
174 | connection['4G'], connection['5G'], connection['enbid'], connection['ping'], connection['uptime'])
175 | logging.info(msg)
176 |
177 | if reboot_performed:
178 | sys.exit(ExitStatus.REBOOT_PERFORMED.value)
179 |
--------------------------------------------------------------------------------
/example.env:
--------------------------------------------------------------------------------
1 | # Rename file to `.env` to use.
2 | # The .env file will be loaded from the current or (recursive) parent directories.
3 | # All settings are prefixed with `tmo_` to allow for common `.env` files at the root.
4 |
5 | # Example:
6 | tmo_username=admin
7 | tmo_password=P4ssw0rd1!
8 |
9 | tmo_ping_count=3
10 | tmo_ping_interval=1
11 | tmo_primary_band=B2,B4
12 |
13 | tmo_5G_band_reboot=False
14 | tmo_ping_reboot=True
15 |
16 | tmo_print_config=True
17 |
18 | # Gateway model
19 | # tmo_model # {'NOK5G21' | 'ARCKVD21' | 'FAST5688W' } The gateway model (defaults to NOK5G21)
20 |
21 | # Trashcan login settings
22 | # tmo_username # defaults to 'admin'
23 | # tmo_password # if not supplied, will be prompted interactively
24 |
25 | # Connectivity check
26 | # tmo_connectivity_check # {'ping' | 'http' } Type of connectivity check to perform (defaults to ping)
27 | # tmo_interface # The network interface to use for ping. Pass the source IP on Windows. http checks use system routing rules.
28 |
29 | # HTTP checks
30 | # tmo_http_target # HTTP connectivity test target (defaults to https://google.com/generate_204)
31 | # tmo_status_code # The expected HTTP status code for HTTP connectivity tests (defaults to 204)
32 |
33 | # Ping configuration
34 | # tmo_ping_host # The host to ping (defaults to 'google.com')
35 | # tmo_ping_count # How many pings to perform before rebooting (defaults to 1)
36 | # tmo_ping_interval # The interval between pings (defaults to 10)
37 | # tmo_ping_6 # {True | False } Use IPv6 ping
38 |
39 | # Connection configuration
40 | # tmo_primary_band # 4G band: comma-separated list from {B2,B4,B5,B12,B13,B25,B26,B41,B46,B48,B66,B71}
41 | # tmo_secondary_band # 5G band: comma-separated list from {n41,n71}
42 | # tmo_enbid # eNB ID
43 |
44 | # Reboot settings: minimum uptime (seconds) & reboot on failed check {True, False}
45 | # Note that these semantics differ from command line arguments!
46 | # tmo_skip_reboot # overrides all other reboot options
47 | # tmo_min_uptime # Minimum uptime to reboot, defaults to 90 seconds
48 | # tmo_ping_reboot # {True | False } Reboot on failed ping
49 | # tmo_http_reboot # {True | False } Reboot on failed http connectivity test.
50 | # tmo_4G_band_reboot # {True | False } Reboot on failed 4G band check
51 | # tmo_5G_band_reboot # {True | False } Reboot on failed 5G band check
52 | # tmo_enbid_reboot # {True | False } Reboot on failed eNB ID check
53 |
54 | # General settings
55 | # tmo_print_config # {True | False } Output configuration to console
56 | # tmo_logfile # Filename for logging output (default: 'tmo-monitor.log')
57 | # tmo_log_all # {True | False } Log all connection statistics
58 | # tmo_log_delta # {True | False } Log any change in connection statistics
59 | # syslog # {True | False } Log to syslog
60 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | certifi==2021.5.30
2 | charset-normalizer==2.0.6
3 | idna==3.2
4 | requests==2.26.0
5 | urllib3==1.26.7
6 | python-dotenv==0.19.2
7 | parse==1.19.0
8 | tailer==0.4.1
9 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup(
4 | name='tmo_monitor',
5 | version='2.0.0-beta4',
6 | description='A script to monitor the T-Mobile Home Internet 5G Gateways',
7 | long_description='A lightweight, cross-platform Python 3 script that can monitor the T-Mobile Home Internet Arcadyan and Nokia 5G Gateways for 4G/5G bands, cellular site (tower), and internet connectivity and reboots as needed or on-demand.',
8 | url='https://github.com/highvolt-dev/tmo-monitor',
9 | author='highvolt-dev',
10 | license='MIT',
11 | packages=[
12 | 'tmo_monitor',
13 | 'tmo_monitor.gateway'
14 | ],
15 | scripts=['bin/tmo-monitor.py'],
16 | install_requires=[
17 | 'parse',
18 | 'python-dotenv',
19 | 'requests',
20 | 'tailer'
21 | ]
22 | )
23 |
--------------------------------------------------------------------------------
/tmo-monitor-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/highvolt-dev/tmo-monitor/dfc0d1aeb5431b9f7f8f84a0728816755cde52a4/tmo-monitor-logo.png
--------------------------------------------------------------------------------
/tmo_monitor/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/highvolt-dev/tmo-monitor/dfc0d1aeb5431b9f7f8f84a0728816755cde52a4/tmo_monitor/__init__.py
--------------------------------------------------------------------------------
/tmo_monitor/configuration.py:
--------------------------------------------------------------------------------
1 | import argparse
2 | import getpass
3 | import logging
4 | import os
5 | import sys
6 | from dotenv import load_dotenv, find_dotenv
7 | from .gateway.model import GatewayModel
8 | from .status import ExitStatus
9 |
10 | class Configuration:
11 | def __init__(self):
12 | # Set default values
13 | self.reboot_now = False
14 | self.skip_reboot = False
15 | self.login = dict([('username', 'admin'), ('password', '')])
16 | self.connectivity = dict([('connectivity_check', 'ping'), ('interface', '')])
17 | self.http = dict([('http_target', 'https://google.com/generate_204'), ('status_code', 204)])
18 | self.ping = dict([('ping_host', 'google.com'), ('ping_count', 1), ('ping_interval', 10), ('ping_6', False)])
19 | self.connection = dict([('primary_band', None), ('secondary_band', ['n41']), ('enbid', None), ('uptime', '')])
20 | self.reboot = dict([('uptime', 90), ('ping', True), ('http', True), ('4G_band', True), ('5G_band', True), ('enbid', True)])
21 | self.general = dict([('print_config', False), ('logfile', ''), ('log_all', False), ('log_delta', False), ('syslog', False)])
22 | self.model = GatewayModel.NOKIA
23 |
24 | # Command line arguments override defaults & .env file
25 | self.read_environment()
26 | args = self.parse_commandline()
27 | self.parse_arguments(args)
28 |
29 | if self.skip_reboot and self.reboot_now:
30 | logging.error('Incompatible options: --reboot and --skip-reboot')
31 | if sys.stdin and sys.stdin.isatty():
32 | self.parser.print_help(sys.stderr)
33 | sys.exit(ExitStatus.CONFIGURATION_ERROR.value)
34 | if self.skip_reboot:
35 | for var in {'ping', 'http', '4G_band', '5G_band', 'enbid'}:
36 | self.reboot[var] = False
37 | if not self.login['password']:
38 | self.login['password'] = getpass.getpass('Password: ')
39 |
40 |
41 | def read_environment(self):
42 | try:
43 | envfile=find_dotenv()
44 | load_dotenv(envfile)
45 | except:
46 | logging.debug("No .env file found")
47 | return
48 | for var in {'username', 'password'}:
49 | tmp = os.environ.get('tmo_' + var)
50 | if tmp != None:
51 | self.login[var] = tmp
52 | for var in {'connectivity_check', 'interface'}:
53 | tmp = os.environ.get('tmo_' + var)
54 | if tmp != None:
55 | self.connectivity[var] = tmp
56 | for var in {'http_target', 'status_code'}:
57 | tmp = os.environ.get('tmo_' + var)
58 | if tmp != None:
59 | self.http[var] = tmp
60 | for var in {'ping_host', 'ping_count', 'ping_interval'}:
61 | tmp = os.environ.get('tmo_' + var)
62 | if tmp != None:
63 | self.ping[var] = tmp
64 | tmp = os.environ.get('tmo_ping_6')
65 | if tmp != None:
66 | if tmp.lower() == 'false':
67 | self.ping['ping_6'] = False
68 | else:
69 | self.ping['ping_6'] = True
70 | for var in {'primary_band', 'secondary_band'}:
71 | tmp = os.environ.get('tmo_' + var)
72 | if tmp != None:
73 | splits = tmp.split(',')
74 | self.connection[var] = splits
75 | tmp = os.environ.get('tmo_enbid')
76 | if tmp != None:
77 | self.connection['enbid'] = tmp
78 | tmp = os.environ.get('tmo_min_uptime')
79 | if tmp != None:
80 | self.reboot['uptime'] = tmp
81 |
82 | # Default all reboot options to true, .env file can override to false
83 | for var in {'ping', 'http', '4G_band', '5G_band', 'enbid'}:
84 | tmp = os.environ.get('tmo_' + var + '_reboot')
85 | if tmp != None:
86 | if tmp.lower() == 'false':
87 | self.reboot[var] = False
88 | else:
89 | self.reboot[var] = True
90 |
91 | tmp = os.environ.get('tmo_skip_reboot')
92 | if tmp != None:
93 | if tmp.lower() == 'true':
94 | self.skip_reboot = True
95 | else:
96 | self.skip_reboot = False
97 | tmp = os.environ.get('tmo_logfile')
98 | if tmp != None:
99 | self.general['logfile'] = tmp
100 | for var in {'print_config', 'log_all', 'log_delta', 'syslog'}:
101 | tmp = os.environ.get('tmo_' + var)
102 | if tmp != None:
103 | if tmp.lower() == 'true':
104 | self.general[var] = True
105 | else:
106 | self.general[var] = False
107 | tmp = os.environ.get('tmo_model')
108 | if tmp != None:
109 | self.model = GatewayModel(tmp)
110 |
111 | def parse_commandline(self):
112 | self.parser = argparse.ArgumentParser(description='Check T-Mobile Home Internet cellular band(s) and connectivity and reboot if necessary')
113 | # login settings
114 | self.parser.add_argument('username', type=str, help='the username (most likely "admin")', nargs='?')
115 | self.parser.add_argument('password', type=str, help='the administrative password (will be requested at runtime if not passed as argument)', nargs='?')
116 | # connectivity check
117 | self.parser.add_argument('--connectivity-check', type=str, default=self.connectivity['connectivity_check'], choices=['ping', 'http'], help='type of connectivity check to perform (defaults to ping)')
118 | self.parser.add_argument('-I', '--interface', type=str, help='the network interface to use for connectivity checks. pass the source IP on Windows or http connectivity checks')
119 | # http check
120 | self.parser.add_argument('--http-target', type=str, default=self.http['http_target'], help='the URL to perform a http check against (defaults to https://google.com/generate_204)')
121 | self.parser.add_argument('--status-code', type=int, default=self.http['status_code'], choices=range(100,600), metavar='{[100,600)}', help='expected HTTP status code for http connectivity check (defaults to 204)')
122 | # ping configuration
123 | self.parser.add_argument('-H', '--ping-host', type=str, default=self.ping['ping_host'], help='the host to ping (defaults to google.com)')
124 | self.parser.add_argument('--ping-count', type=int, default=self.ping['ping_count'], help='how many ping health checks to perform (defaults to 1)')
125 | self.parser.add_argument('--ping-interval', type=int, default=self.ping['ping_interval'], help='how long in seconds to wait between ping health checks (defaults to 10)')
126 | self.parser.add_argument('-6', '--ping-6', action='store_true', default=self.ping['ping_6'], help='use IPv6 ping')
127 | # reboot settings
128 | self.parser.add_argument('-R', '--reboot', action='store_true', help='skip health checks and immediately reboot gateway')
129 | self.parser.add_argument('-r', '--skip-reboot', action='store_true', help='skip rebooting gateway')
130 | self.parser.add_argument('--skip-bands', action='store_true', help='skip check for connected 4g band')
131 | self.parser.add_argument('--skip-5g-bands', action='store_true', help='skip check for connected 5g band')
132 | self.parser.add_argument('--skip-connectivity-check', '--skip-ping', action='store_true', help='skip connectivity check')
133 | self.parser.add_argument('--skip-enbid', action='store_true', help='skip check for connected eNB ID')
134 | self.parser.add_argument('--uptime', type=int, default=self.reboot['uptime'], help='how long the gateway must be up before considering a reboot (defaults to 90 seconds)')
135 | # connection configuration
136 | self.parser.add_argument('-4', '--4g-band', type=str, action='append', dest='primary_band', default=None, choices=['B2', 'B4', 'B5', 'B12', 'B13', 'B25', 'B26', 'B41', 'B46', 'B48', 'B66', 'B71'], help='the 4g band(s) to check')
137 | self.parser.add_argument('-5', '--5g-band', type=str, action='append', dest='secondary_band', default=None, choices=['n41', 'n71'], help='the 5g band(s) to check (defaults to n41)')
138 | self.parser.add_argument('--enbid', type=int, default=self.connection['enbid'], help='check for a connection to a given eNB ID')
139 | # general configuration
140 | self.parser.add_argument('--print-config', action='store_true', default=self.general['print_config'], help='output configuration settings')
141 | self.parser.add_argument('--logfile', type=str, default=self.general['logfile'], help='output file for logging')
142 | self.parser.add_argument('--log-all', action='store_true', default=self.general['log_all'], help='always write connection details to logfile')
143 | self.parser.add_argument('--log-delta', action='store_true', default=self.general['log_delta'], help='write connection details to logfile on change')
144 | self.parser.add_argument('--syslog', action='store_true', default=self.general['syslog'], help='log to syslog')
145 | self.parser.add_argument('--model', type=str, default=self.model, choices=[model.value for model in GatewayModel], help='the gateway model (defaults to NOK5G21)')
146 | return self.parser.parse_args()
147 |
148 | def parse_arguments(self, args):
149 | for var in {'username', 'password'}:
150 | tmp = getattr(args, var)
151 | if tmp != None:
152 | self.login[var] = tmp
153 | for var in {'connectivity_check', 'interface'}:
154 | tmp = getattr(args, var)
155 | if tmp != None:
156 | self.connectivity[var] = tmp
157 | for var in {'http_target', 'status_code'}:
158 | tmp = getattr(args, var)
159 | if tmp != None:
160 | self.http[var] = tmp
161 | for var in {'ping_host', 'ping_count', 'ping_interval'}:
162 | tmp = getattr(args, var)
163 | if tmp != None:
164 | self.ping[var] = tmp
165 | if args.ping_6 == True:
166 | self.ping['ping_6'] = True
167 | for var in {'primary_band', 'secondary_band', 'enbid'}:
168 | tmp = getattr(args, var)
169 | if tmp != None:
170 | self.connection[var] = tmp
171 | self.general['logfile'] = args.logfile
172 | for var in {'print_config', 'log_all', 'log_delta', 'syslog'}:
173 | tmp = getattr(args, var)
174 | self.general[var] = tmp
175 |
176 | if args.uptime != None:
177 | self.reboot['uptime'] = args.uptime
178 |
179 | # At this point in the script self.reboot[*] defaults to True unless overridden in .env file
180 |
181 | # Reboot on ping and http check by default, override for args.skip_connectivity_check
182 | if args.skip_connectivity_check == True:
183 | self.reboot['ping'] = False
184 | self.reboot['http'] = False
185 |
186 | # Reboot on primary (4G) band only if one is specified & no overrides
187 | if self.connection['primary_band'] == None or args.skip_bands == True:
188 | self.reboot['4G_band'] = False
189 |
190 | # Secondary band has default (n41). Reboot only if skipped on command line
191 | if args.skip_5g_bands == True:
192 | self.reboot['5G_band'] = False
193 |
194 | # Reboot on enbid only if one is specified & no overrides
195 | if self.connection['enbid'] == None or args.skip_enbid == True:
196 | self.reboot['enbid'] = False
197 |
198 | if args.skip_reboot == True:
199 | self.skip_reboot = True
200 | if args.reboot == True:
201 | self.reboot_now = True
202 |
203 | if args.model is not None:
204 | self.model = GatewayModel(args.model)
205 |
206 | def print_config(self):
207 | print("Script configuration:")
208 | print(" Gateway model: " + self.model.value)
209 | if sys.stdin and sys.stdin.isatty():
210 | print(" Login info:")
211 | print(" Username: " + self.login.get('username') if self.login.get('username') else '')
212 | print(" Password: " + self.login.get('password') if self.login.get('password') else '')
213 | print(" Connectivity check:")
214 | print(" Mode: " + self.connectivity.get('connectivity_check'))
215 | (print(" Interface: " + self.connectivity.get('interface')) if self.connectivity.get('interface') else '')
216 | print(" Http check:")
217 | (print(" Target: " + self.http.get('http_target')) if self.http.get('http_target') else '')
218 | (print(" Status Code: " + str(self.http.get('status_code'))) if self.http.get('status_code') else '')
219 | print(" Ping configuration:")
220 | (print(" Host: " + self.ping.get('ping_host')) if self.ping.get('ping_host') else '')
221 | (print(" Count: " + str(self.ping.get('ping_count'))) if self.ping.get('ping_count') else '')
222 | (print(" Interval: " + str(self.ping.get('ping_interval'))) if self.ping.get('ping_interval') else '')
223 | print(" Protocol: " + ('IPv6' if self.ping.get('ping_6') else 'IPv4'))
224 | print(" Connection configuration:")
225 | (print(" Primary band: " + str(self.connection.get('primary_band'))) if self.connection.get('primary_band') else '')
226 | (print(" Secondary band: " + str(self.connection.get('secondary_band'))) if self.connection.get('secondary_band') else '')
227 | (print(" eNB ID: " + str(self.connection.get('enbid'))) if self.connection.get('enbid') else '')
228 | print(" Reboot settings:")
229 | print(" Reboot now: " + str(self.reboot_now))
230 | print(" Skip reboot: " + str(self.skip_reboot))
231 | (print(" Min uptime: " + str(self.reboot.get('uptime'))) if self.reboot.get('uptime') else '')
232 | print(" Reboot on: " + ("ping " if self.reboot['ping'] else '') + ("4G_band " if self.reboot['4G_band'] else '')
233 | + ("5G_band " if self.reboot['5G_band'] else '') + ("eNB_ID" if self.reboot['enbid'] else ''))
234 | print(" General settings:")
235 | print(" Log file: " + str(self.general['logfile']))
236 | print(" Log all: " + str(self.general['log_all']))
237 | print(" Log delta: " + str(self.general['log_delta']))
238 | print(" Log to syslog: " + str(self.general['syslog']))
239 | print('')
240 |
--------------------------------------------------------------------------------
/tmo_monitor/gateway/arcadyan.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import sys
3 | import logging
4 | import math
5 | from .base import ControllerBase
6 | from ..status import ExitStatus
7 |
8 | class CubeController(ControllerBase):
9 | def __init__(self, username, password):
10 | self.username = username
11 | self.password = password
12 | self.info_web = None
13 | self.app_token = None
14 | # functions using authenticated app API endpoints
15 | def login_app(self):
16 | try:
17 | login_request = requests.post('http://192.168.12.1/TMI/v1/auth/login', json={'username': self.username, 'password': self.password})
18 | except:
19 | logging.critical("Could not post login request, exiting.")
20 | sys.exit(ExitStatus.API_ERROR.value)
21 | login_request.raise_for_status()
22 | self.app_token = login_request.json()['auth']['token']
23 |
24 | def get_site_info(self):
25 | try:
26 | if not self.app_token:
27 | self.login_app()
28 | stat_request = requests.get('http://192.168.12.1/TMI/v1/network/telemetry?get=all', headers={'Authorization': 'Bearer ' + self.app_token})
29 | except:
30 | logging.critical("Could not query site info, exiting.")
31 | sys.exit(ExitStatus.API_ERROR.value)
32 |
33 | stat_request.raise_for_status()
34 | meta = stat_request.json()['cell']['4g']
35 |
36 | return {
37 | 'eNBID': math.floor(int(meta['ecgi'][6:])/256),
38 | 'PLMN': meta['mcc'] + '-' + meta['mnc']
39 | }
40 | def reboot(self):
41 | try:
42 | if not self.app_token:
43 | self.login_app()
44 | reboot_request = requests.post('http://192.168.12.1/TMI/v1/gateway/reset?set=reboot', headers={'Authorization': 'Bearer ' + self.app_token})
45 | except:
46 | logging.critical("Could not post reboot request, exiting.")
47 | sys.exit(ExitStatus.API_ERROR.value)
48 | reboot_request.raise_for_status()
49 | # functions using authenticated web API endpoints
50 | def login_web(self):
51 | raise Exception('Not implemented')
52 | # functions using unauthenticated API endpoints
53 | def get_all_info_web(self):
54 | if self.info_web is not None:
55 | return self.info_web
56 | try:
57 | signal_request = requests.get('http://192.168.12.1/TMI/v1/gateway?get=all')
58 | except:
59 | logging.critical("Could not query signal status, exiting.")
60 | sys.exit(ExitStatus.API_ERROR.value)
61 | signal_request.raise_for_status()
62 | self.info_web = signal_request.json()
63 | return self.info_web
64 | def get_uptime(self):
65 | return self.get_all_info_web()['time']['upTime']
66 | def get_signal_info(self):
67 | info = self.get_all_info_web()
68 | lte_info = info['signal']['4g']['bands']
69 | if '5g' in info['signal']:
70 | nr_info = info['signal']['5g']['bands']
71 | else:
72 | nr_info = []
73 |
74 | return {
75 | '4G': None if len(lte_info) == 0 else lte_info[0].upper(),
76 | '5G': None if len(nr_info) == 0 else nr_info[0]
77 | }
--------------------------------------------------------------------------------
/tmo_monitor/gateway/base.py:
--------------------------------------------------------------------------------
1 | import platform
2 | import requests
3 | import re
4 | import shutil
5 | import subprocess
6 | import time
7 | import sys
8 |
9 | class ControllerBase:
10 | # functions that don't touch the API
11 | def ping(self, ping_host, ping_count, ping_interval, interface = None, ping_6 = False):
12 | is_win = platform.system() == 'Windows'
13 | is_mac = platform.system() == 'Darwin'
14 |
15 | ping_cmd = []
16 | extra_flags = []
17 |
18 | # Handle IPv6 support - use ping6 binary or ping -6 flag
19 | ping_bin = 'ping'
20 | if ping_6:
21 | if shutil.which('ping6') is not None:
22 | ping_bin = 'ping6'
23 | else:
24 | extra_flags.append('-6')
25 | # Explicitly use -4 flag for IPv4 except for Mac OS X
26 | elif not is_mac:
27 | extra_flags.append('-4')
28 |
29 | # Add optional interface flag
30 | if interface:
31 | extra_flags.append('-S' if is_win else '-I')
32 | extra_flags.append(interface)
33 |
34 | # Combine base command with extra flags
35 | ping_cmd.append(ping_bin)
36 | ping_cmd = ping_cmd + extra_flags
37 |
38 | # Specify ping count
39 | ping_cmd.append('-n' if is_win else '-c')
40 | ping_cmd.append('1')
41 | ping_cmd.append(ping_host)
42 |
43 | def ping_time(ping_index):
44 | if ping_index > 0:
45 | time.sleep(ping_interval)
46 | ping_exec = subprocess.run(ping_cmd, capture_output=True)
47 | print(ping_exec.stdout.decode('utf-8'))
48 | if ping_exec.returncode != 0:
49 | return -1
50 | if is_win and 'Destination host unreachable' in str(ping_exec.stdout):
51 | return -1
52 | pattern = b'(?:rtt|round-trip) min/avg/max(?:/(?:mdev|stddev))? = \d+.\d+/(\d+.\d+)/\d+.\d+(?:/\d+.\d+)? ms'
53 | if is_win:
54 | pattern = b'Minimum = \d+ms, Maximum = \d+ms, Average = (\d+)ms'
55 | ping_ms = re.search(pattern, ping_exec.stdout)
56 | return round(float(ping_ms.group(1)))
57 |
58 | for i in range (ping_count):
59 | result = ping_time(i)
60 | if result > 0:
61 | return result
62 | return -1
63 |
64 | def http_check(self, target, interface = None):
65 | r = requests.get(target)
66 | return r.status_code
--------------------------------------------------------------------------------
/tmo_monitor/gateway/model.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 | class GatewayModel(Enum):
4 | NOKIA = 'NOK5G21'
5 | ARCADYAN = 'ARCKVD21'
6 | SAGECOM = 'FAST5688W'
--------------------------------------------------------------------------------
/tmo_monitor/gateway/nokia.py:
--------------------------------------------------------------------------------
1 | from .base import ControllerBase
2 | from base64 import b64encode
3 | import hashlib
4 | import logging
5 | import requests
6 | import secrets
7 | import sys
8 | from ..status import ExitStatus
9 |
10 | class TrashCanController(ControllerBase):
11 | def __init__(self, username, password):
12 | self.username = username
13 | self.password = password
14 | self.nonce = None
15 | self.csrf_token = None
16 | self.app_jar = None
17 | self.web_jar = None
18 | self.device_info = None
19 |
20 | # functions using authenticated app API endpoints
21 | def login_app(self):
22 | try:
23 | login_request = requests.post('http://192.168.12.1/login_app.cgi', data={'name': self.username, 'pswd': self.password})
24 | except:
25 | logging.critical("Could not post login request, exiting.")
26 | sys.exit(ExitStatus.API_ERROR.value)
27 | login_request.raise_for_status()
28 |
29 | self.app_jar = requests.cookies.RequestsCookieJar()
30 | self.app_jar.set('sid', login_request.cookies['sid'], domain='192.168.12.1', path='/')
31 | self.app_jar.set('lsid', login_request.cookies['lsid'], domain='192.168.12.1', path='/')
32 |
33 | def get_site_info(self):
34 | try:
35 | if not self.app_jar:
36 | self.login_app()
37 | stat_request = requests.get('http://192.168.12.1/cell_status_app.cgi', cookies=self.app_jar)
38 | except:
39 | logging.critical("Could not query site info, exiting.")
40 | sys.exit(ExitStatus.API_ERROR.value)
41 |
42 | stat_request.raise_for_status()
43 | meta = stat_request.json()['cell_stat_lte'][0]
44 |
45 | return {
46 | 'eNBID': int(meta['eNBID']),
47 | 'PLMN': meta['MCC'] + '-' + meta['MNC']
48 | }
49 |
50 | # functions using authenticated web API endpoints
51 | def login_web(self):
52 | try:
53 | nonce_request = requests.get('http://192.168.12.1/login_web_app.cgi?nonce')
54 | except:
55 | logging.critical("Could not query nonce, exiting.")
56 | sys.exit(ExitStatus.API_ERROR.value)
57 |
58 | nonce_request.raise_for_status()
59 | nonce_response = nonce_request.json()
60 | self.nonce = nonce_response['nonce']
61 |
62 | if self.get_firmware_version() < '1.2103.00.0338':
63 | pass_hash_input = self.password
64 | else:
65 | if nonce_response['iterations'] >= 1:
66 | raise Exception('Password strategy not implemented')
67 | else:
68 | r = self.password
69 |
70 | pass_hash_input = r.lower()
71 |
72 | user_pass_hash = self.sha256(self.username, pass_hash_input)
73 | user_pass_nonce_hash = self.sha256url(user_pass_hash, self.nonce)
74 | login_request_body = {
75 | 'userhash': self.sha256url(self.username, self.nonce),
76 | 'RandomKeyhash': self.sha256url(nonce_response['randomKey'], self.nonce),
77 | 'response': user_pass_nonce_hash,
78 | 'nonce': self.base64url_escape(self.nonce),
79 | 'enckey': self.base64url_escape(b64encode(secrets.token_bytes(16)).decode('utf-8')),
80 | 'enciv': self.base64url_escape(b64encode(secrets.token_bytes(16)).decode('utf-8'))
81 | }
82 |
83 | try:
84 | login_request = requests.post('http://192.168.12.1/login_web_app.cgi', data=login_request_body)
85 | except:
86 | logging.critical("Could not post login request, exiting.")
87 | sys.exit(ExitStatus.API_ERROR.value)
88 | login_request.raise_for_status()
89 | self.web_jar = requests.cookies.RequestsCookieJar()
90 | self.web_jar.set('sid', login_request.cookies['sid'], domain='192.168.12.1', path='/')
91 | if 'lsid' in login_request.cookies:
92 | self.web_jar.set('lsid', login_request.cookies['lsid'], domain='192.168.12.1', path='/')
93 | login_response = login_request.json()
94 | self.csrf_token = login_response['token']
95 |
96 | def reboot(self):
97 | try:
98 | if not (self.csrf_token or self.web_jar):
99 | self.login_web()
100 | reboot_request = requests.post('http://192.168.12.1/reboot_web_app.cgi', data={'csrf_token': self.csrf_token}, cookies=self.web_jar)
101 | except:
102 | logging.critical("Could not post reboot request, exiting.")
103 | sys.exit(ExitStatus.API_ERROR.value)
104 | reboot_request.raise_for_status()
105 |
106 | # functions using unauthenticated API endpoints
107 | def get_device_info(self):
108 | try:
109 | device_info_req = requests.get('http://192.168.12.1/dashboard_device_info_status_web_app.cgi')
110 | except:
111 | logging.critical("Could not query device info, exiting.")
112 | sys.exit(ExitStatus.API_ERROR.value)
113 | device_info_req.raise_for_status()
114 | return device_info_req.json()['device_app_status'][0]
115 |
116 | def get_firmware_version(self):
117 | try:
118 | if not self.device_info:
119 | self.device_info = self.get_device_info()
120 | except:
121 | logging.critical("Could not query firmware version, exiting.")
122 | sys.exit(ExitStatus.API_ERROR.value)
123 | return self.device_info['SoftwareVersion']
124 |
125 | def get_uptime(self):
126 | try:
127 | if not self.device_info:
128 | self.device_info = self.get_device_info()
129 | except:
130 | logging.critical("Could not query modem uptime, exiting.")
131 | sys.exit(ExitStatus.API_ERROR.value)
132 | return self.device_info['UpTime']
133 |
134 | def get_signal_info(self):
135 | try:
136 | signal_request = requests.get('http://192.168.12.1/fastmile_radio_status_web_app.cgi')
137 | except:
138 | logging.critical("Could not query signal status, exiting.")
139 | sys.exit(ExitStatus.API_ERROR.value)
140 | signal_request.raise_for_status()
141 | info = signal_request.json()
142 |
143 | return {
144 | '4G': info['cell_LTE_stats_cfg'][0]['stat']['Band'],
145 | '5G': info['cell_5G_stats_cfg'][0]['stat']['Band']
146 | }
147 |
148 | # helper functions - maybe move these into their own class and import it later?
149 | def base64url_escape(self, b64):
150 | out = ''
151 | for c in b64:
152 | if c == '+':
153 | out += '-'
154 | elif c == '/':
155 | out += '_'
156 | elif c == '=':
157 | out += '.'
158 | else:
159 | out += c
160 | return out
161 |
162 | def sha256(self, val1, val2):
163 | hash = hashlib.sha256()
164 | hash.update((val1 + ':' + val2).encode())
165 | return b64encode(hash.digest()).decode('utf-8')
166 |
167 | def sha256url(self, val1, val2):
168 | return self.base64url_escape(self.sha256(val1, val2))
--------------------------------------------------------------------------------
/tmo_monitor/status.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 | class ExitStatus(Enum):
4 | GENERAL_ERROR = 1
5 | CONFIGURATION_ERROR = 2
6 | API_ERROR = 3
7 | REBOOT_PERFORMED = 4
8 |
--------------------------------------------------------------------------------