commit
c516fcc1e7
11 changed files with 1084 additions and 761 deletions
54
README.md
54
README.md
|
@ -5,9 +5,11 @@ For more information on what Starlink is, see [starlink.com](https://www.starlin
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
|
Most of the scripts here are [Python](https://www.python.org/) scripts. To use them, you will either need Python installed on your system or you can use the Docker image. If you use the Docker image, you can skip the rest of the prerequisites other than making sure the dish IP is reachable and Docker itself. For Linux systems, the python package from your distribution should be fine, as long as it is Python 3. The JSON script should actually work with Python 2.7, but the grpc scripts all require Python 3 (and Python 2.7 is past end-of-life, so is not recommended anyway).
|
||||||
|
|
||||||
`parseJsonHistory.py` operates on a JSON format data representation of the protocol buffer messages, such as that output by [gRPCurl](https://github.com/fullstorydev/grpcurl). The command lines below assume `grpcurl` is installed in the runtime PATH. If that's not the case, just substitute in the full path to the command.
|
`parseJsonHistory.py` operates on a JSON format data representation of the protocol buffer messages, such as that output by [gRPCurl](https://github.com/fullstorydev/grpcurl). The command lines below assume `grpcurl` is installed in the runtime PATH. If that's not the case, just substitute in the full path to the command.
|
||||||
|
|
||||||
All the tools that pull data from the dish expect to be able to reach it at the dish's fixed IP address of 192.168.100.1, as do the Starlink [Android app](https://play.google.com/store/apps/details?id=com.starlink.mobile) and [iOS app](https://apps.apple.com/us/app/starlink/id1537177988). When using a router other than the one included with the Starlink installation kit, this usually requires some additional router configuration to make it work. That configuration is beyond the scope of this document, but if the Starlink app doesn't work on your home network, then neither will these scripts. That being said, you do not need the Starlink app installed to make use of these scripts.
|
All the tools that pull data from the dish expect to be able to reach it at the dish's fixed IP address of 192.168.100.1, as do the Starlink [Android app](https://play.google.com/store/apps/details?id=com.starlink.mobile), [iOS app](https://apps.apple.com/us/app/starlink/id1537177988), and the browser app you can run directly from http://192.168.100.1. When using a router other than the one included with the Starlink installation kit, this usually requires some additional router configuration to make it work. That configuration is beyond the scope of this document, but if the Starlink app doesn't work on your home network, then neither will these scripts. That being said, you do not need the Starlink app installed to make use of these scripts.
|
||||||
|
|
||||||
The scripts that don't use `grpcurl` to pull data require the `grpcio` Python package at runtime and generating the necessary gRPC protocol code requires the `grpcio-tools` package. Information about how to install both can be found at https://grpc.io/docs/languages/python/quickstart/
|
The scripts that don't use `grpcurl` to pull data require the `grpcio` Python package at runtime and generating the necessary gRPC protocol code requires the `grpcio-tools` package. Information about how to install both can be found at https://grpc.io/docs/languages/python/quickstart/
|
||||||
|
|
||||||
|
@ -15,8 +17,14 @@ The scripts that use [MQTT](https://mqtt.org/) for output require the `paho-mqtt
|
||||||
|
|
||||||
The scripts that use [InfluxDB](https://www.influxdata.com/products/influxdb/) for output require the `influxdb` Python package. Information about how to install that can be found at https://github.com/influxdata/influxdb-python. Note that this is the (slightly) older version of the InfluxDB client Python module, not the InfluxDB 2.0 client. It can still be made to work with an InfluxDB 2.0 server, but doing so requires using `influx v1` [CLI commands](https://docs.influxdata.com/influxdb/v2.0/reference/cli/influx/v1/) on the server to map the 1.x username, password, and database names to their 2.0 equivalents.
|
The scripts that use [InfluxDB](https://www.influxdata.com/products/influxdb/) for output require the `influxdb` Python package. Information about how to install that can be found at https://github.com/influxdata/influxdb-python. Note that this is the (slightly) older version of the InfluxDB client Python module, not the InfluxDB 2.0 client. It can still be made to work with an InfluxDB 2.0 server, but doing so requires using `influx v1` [CLI commands](https://docs.influxdata.com/influxdb/v2.0/reference/cli/influx/v1/) on the server to map the 1.x username, password, and database names to their 2.0 equivalents.
|
||||||
|
|
||||||
|
Running the scripts within a [Docker](https://www.docker.com/) container requires Docker to be installed. Information about how to install that can be found at https://docs.docker.com/engine/install/
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
Of the 3 groups below, the grpc scripts are really the only ones being actively developed. The others are mostly by way of example of what could be done with the underlying data.
|
||||||
|
|
||||||
|
### The JSON parser script
|
||||||
|
|
||||||
`parseJsonHistory.py` takes input from a file and writes its output to standard output. The easiest way to use it is to pipe the `grpcurl` command directly into it. For example:
|
`parseJsonHistory.py` takes input from a file and writes its output to standard output. The easiest way to use it is to pipe the `grpcurl` command directly into it. For example:
|
||||||
```
|
```
|
||||||
grpcurl -plaintext -d {\"get_history\":{}} 192.168.100.1:9200 SpaceX.API.Device.Device/Handle | python parseJsonHistory.py
|
grpcurl -plaintext -d {\"get_history\":{}} 192.168.100.1:9200 SpaceX.API.Device.Device/Handle | python parseJsonHistory.py
|
||||||
|
@ -28,7 +36,11 @@ python parseJsonHistory.py -h
|
||||||
|
|
||||||
When used as-is, `parseJsonHistory.py` will summarize packet loss information from the data the dish records. There's other bits of data in there, though, so that script (or more likely the parsing logic it uses, which now resides in `starlink_json.py`) could be used as a starting point or example of how to iterate through it. Most of the data displayed in the Statistics page of the Starlink app appears to come from this same `get_history` gRPC response. See the file `get_history_notes.txt` for some ramblings on how to interpret it.
|
When used as-is, `parseJsonHistory.py` will summarize packet loss information from the data the dish records. There's other bits of data in there, though, so that script (or more likely the parsing logic it uses, which now resides in `starlink_json.py`) could be used as a starting point or example of how to iterate through it. Most of the data displayed in the Statistics page of the Starlink app appears to come from this same `get_history` gRPC response. See the file `get_history_notes.txt` for some ramblings on how to interpret it.
|
||||||
|
|
||||||
The other scripts can do the gRPC communication directly, but they require some generated code to support the specific gRPC protocol messages used. These would normally be generated from .proto files that specify those messages, but to date (2020-Dec), SpaceX has not publicly released such files. The gRPC service running on the dish appears to have [server reflection](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md) enabled, though. `grpcurl` can use that to extract a protoset file, and the `protoc` compiler can use that to make the necessary generated code:
|
The one bit of functionality this script has over the grpc scripts is that it supports capturing the grpcurl output to a file and reading from that, which may be useful if you're collecting data in one place but analyzing it in another. Otherwise, it's probably better to use `dishHistoryStats.py`, described below.
|
||||||
|
|
||||||
|
### The grpc scripts
|
||||||
|
|
||||||
|
This set of scripts can do the gRPC communication directly, but they require some generated code to support the specific gRPC protocol messages used. These would normally be generated from .proto files that specify those messages, but to date (2020-Dec), SpaceX has not publicly released such files. The gRPC service running on the dish appears to have [server reflection](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md) enabled, though. `grpcurl` can use that to extract a protoset file, and the `protoc` compiler can use that to make the necessary generated code:
|
||||||
```
|
```
|
||||||
grpcurl -plaintext -protoset-out dish.protoset 192.168.100.1:9200 describe SpaceX.API.Device.Device
|
grpcurl -plaintext -protoset-out dish.protoset 192.168.100.1:9200 describe SpaceX.API.Device.Device
|
||||||
mkdir src
|
mkdir src
|
||||||
|
@ -41,29 +53,47 @@ python3 -m grpc_tools.protoc --descriptor_set_in=../dish.protoset --python_out=.
|
||||||
python3 -m grpc_tools.protoc --descriptor_set_in=../dish.protoset --python_out=. --grpc_python_out=. spacex/api/device/wifi.proto
|
python3 -m grpc_tools.protoc --descriptor_set_in=../dish.protoset --python_out=. --grpc_python_out=. spacex/api/device/wifi.proto
|
||||||
python3 -m grpc_tools.protoc --descriptor_set_in=../dish.protoset --python_out=. --grpc_python_out=. spacex/api/device/wifi_config.proto
|
python3 -m grpc_tools.protoc --descriptor_set_in=../dish.protoset --python_out=. --grpc_python_out=. spacex/api/device/wifi_config.proto
|
||||||
```
|
```
|
||||||
Then move the resulting files to where the Python scripts can find them.
|
Then move the resulting files to where the Python scripts can find them in the import path, such as in the same directory as the scripts themselves.
|
||||||
|
|
||||||
Once those are available, the `dishHistoryStats.py` script can be used in place of the `grpcurl | parseJsonHistory.py` pipeline, with most of the same command line options.
|
Once those are available, the `dishHistoryStats.py` script can be used in place of the `grpcurl | parseJsonHistory.py` pipeline, with most of the same command line options. For example:
|
||||||
|
```
|
||||||
|
python3 parseHistoryStats.py
|
||||||
|
```
|
||||||
|
|
||||||
To collect and record summary stats every hour, you can put something like the following in your user crontab:
|
By default, `parseHistoryStats.py` (and `parseJsonHistory.py`) will output the stats in CSV format. You can use the `-v` option to instead output in a (slightly) more human-readable format.
|
||||||
|
|
||||||
|
To collect and record summary stats at the top of every hour, you could put something like the following in your user crontab (assuming you have moved the scripts to ~/bin and made them executable):
|
||||||
```
|
```
|
||||||
00 * * * * [ -e ~/dishStats.csv ] || ~/bin/dishHistoryStats.py -H >~/dishStats.csv; ~/bin/dishHistoryStats.py >>~/dishStats.csv
|
00 * * * * [ -e ~/dishStats.csv ] || ~/bin/dishHistoryStats.py -H >~/dishStats.csv; ~/bin/dishHistoryStats.py >>~/dishStats.csv
|
||||||
```
|
```
|
||||||
|
|
||||||
`dishHistoryInflux.py` and `dishHistoryMqtt.py` are similar, but they send their output to an InfluxDB server and a MQTT broker, respectively. Run them with `-h` command line option for details on how to specify server and/or database options.
|
`dishHistoryInflux.py` and `dishHistoryMqtt.py` are similar, but they send their output to an InfluxDB server and a MQTT broker, respectively. Run them with `-h` command line option for details on how to specify server and/or database options.
|
||||||
|
|
||||||
`dishDumpStatus.py` is even simpler. Just run it as:
|
`dishStatusCsv.py`, `dishStatusInflux.py`, and `dishStatusMqtt.py` output the status data instead of history data, to various data backends. The information they pull is mostly what appears related to the dish in the Debug Data section of the Starlink app. As with the corresponding history scripts, run them with `-h` command line option for usage details.
|
||||||
|
|
||||||
|
By default, all of these scripts will pull data once, send it off to the specified data backend, and then exit. They can instead be made to run in a periodic loop by passing a `-t` option to specify loop interval, in seconds. For example, to capture status information to a InfluxDB server every 30 seconds, you could do something like this:
|
||||||
|
```
|
||||||
|
python3 dishStatusInflux.py -t 30 [... probably other args to specify server options ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
Some of the scripts (currently only the InfluxDB ones) also support specifying options through environment variables. See details in the scripts for the environment variables that map to options.
|
||||||
|
|
||||||
|
### Other scripts
|
||||||
|
|
||||||
|
`dishDumpStatus.py` is a simple example of how to use the grpc modules (the ones generated by protoc, not `starlink_grpc.py`) directly. Just run it as:
|
||||||
```
|
```
|
||||||
python3 dishDumpStatus.py
|
python3 dishDumpStatus.py
|
||||||
```
|
```
|
||||||
and revel in copious amounts of dish status information. OK, maybe it's not as impressive as all that. This one is really just meant to be a starting point for real functionality to be added to it. The information this script pulls is mostly what appears related to the dish in the Debug Data section of the Starlink app.
|
and revel in copious amounts of dish status information. OK, maybe it's not as impressive as all that. This one is really just meant to be a starting point for real functionality to be added to it.
|
||||||
|
|
||||||
`dishStatusCsv.py`, `dishStatusInflux.py`, and `dishStatusMqtt.py` output the same status data, but to various data backends. As with the corresponding history scripts, run them with `-h` command line option for usage details.
|
Possibly more simple examples to come, as the other scripts have started getting a bit complicated.
|
||||||
|
|
||||||
## To Be Done (Maybe)
|
## To Be Done (Maybe)
|
||||||
|
|
||||||
There are `reboot` and `dish_stow` requests in the Device protocol, too, so it should be trivial to write a command that initiates dish reboot and stow operations. These are easy enough to do with `grpcurl`, though, as there is no need to parse through the response data. For that matter, they're easy enough to do with the Starlink app.
|
There are `reboot` and `dish_stow` requests in the Device protocol, too, so it should be trivial to write a command that initiates dish reboot and stow operations. These are easy enough to do with `grpcurl`, though, as there is no need to parse through the response data. For that matter, they're easy enough to do with the Starlink app.
|
||||||
|
|
||||||
|
Proper Python packaging, since some of the scripts are no longer self-contained.
|
||||||
|
|
||||||
## Other Tidbits
|
## Other Tidbits
|
||||||
|
|
||||||
The Starlink Android app actually uses port 9201 instead of 9200. Both appear to expose the same gRPC service, but the one on port 9201 uses an HTTP/1.1 wrapper, whereas the one on port 9200 uses HTTP/2.0, which is what most gRPC tools expect.
|
The Starlink Android app actually uses port 9201 instead of 9200. Both appear to expose the same gRPC service, but the one on port 9201 uses an HTTP/1.1 wrapper, whereas the one on port 9200 uses HTTP/2.0, which is what most gRPC tools expect.
|
||||||
|
@ -75,7 +105,7 @@ The Starlink router also exposes a gRPC service, on ports 9000 (HTTP/2.0) and 90
|
||||||
Initialization of the container can be performed with the following command:
|
Initialization of the container can be performed with the following command:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run -d --name='starlink-grpc-tools' -e INFLUXDB_HOST={InfluxDB Hostname} \
|
docker run -d -t --name='starlink-grpc-tools' -e INFLUXDB_HOST={InfluxDB Hostname} \
|
||||||
-e INFLUXDB_PORT={Port, 8086 usually} \
|
-e INFLUXDB_PORT={Port, 8086 usually} \
|
||||||
-e INFLUXDB_USER={Optional, InfluxDB Username} \
|
-e INFLUXDB_USER={Optional, InfluxDB Username} \
|
||||||
-e INFLUXDB_PWD={Optional, InfluxDB Password} \
|
-e INFLUXDB_PWD={Optional, InfluxDB Password} \
|
||||||
|
@ -83,6 +113,10 @@ docker run -d --name='starlink-grpc-tools' -e INFLUXDB_HOST={InfluxDB Hostname}
|
||||||
neurocis/starlink-grpc-tools dishStatusInflux.py -v
|
neurocis/starlink-grpc-tools dishStatusInflux.py -v
|
||||||
```
|
```
|
||||||
|
|
||||||
`dishStatusInflux.py -v` is optional and will run same but not -verbose, or you can replace it with one of the other scripts if you wish to run that instead. There is also an `GrafanaDashboard - Starlink Statistics.json` which can be imported to get some charts like:
|
The `-t` option to `docker run` will prevent Python from buffering the script's standard output and can be omitted if you don't care about seeing the verbose output in the container logs as soon as it is printed.
|
||||||
|
|
||||||
|
The `dishStatusInflux.py -v` is optional and omitting it will run same but not verbose, or you can replace it with one of the other scripts if you wish to run that instead, or use other command line options. There is also a `GrafanaDashboard - Starlink Statistics.json` which can be imported to get some charts like:
|
||||||
|
|
||||||
![image](https://user-images.githubusercontent.com/945191/104257179-ae570000-5431-11eb-986e-3fedd04bfcfb.png)
|
![image](https://user-images.githubusercontent.com/945191/104257179-ae570000-5431-11eb-986e-3fedd04bfcfb.png)
|
||||||
|
|
||||||
|
You'll probably want to run with the `-t` option to `dishStatusInflux.py` to collect status information periodically for this to be meaningful.
|
||||||
|
|
|
@ -10,169 +10,247 @@
|
||||||
#
|
#
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
import datetime
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import getopt
|
import getopt
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from influxdb import InfluxDBClient
|
from influxdb import InfluxDBClient
|
||||||
|
|
||||||
import starlink_grpc
|
import starlink_grpc
|
||||||
|
|
||||||
arg_error = False
|
|
||||||
|
|
||||||
try:
|
class Terminated(Exception):
|
||||||
opts, args = getopt.getopt(sys.argv[1:], "ahn:p:rs:vC:D:IP:R:SU:")
|
pass
|
||||||
except getopt.GetoptError as err:
|
|
||||||
print(str(err))
|
|
||||||
arg_error = True
|
|
||||||
|
|
||||||
# Default to 1 hour worth of data samples.
|
|
||||||
samples_default = 3600
|
|
||||||
samples = samples_default
|
|
||||||
print_usage = False
|
|
||||||
verbose = False
|
|
||||||
run_lengths = False
|
|
||||||
host_default = "localhost"
|
|
||||||
database_default = "starlinkstats"
|
|
||||||
icargs = {"host": host_default, "timeout": 5, "database": database_default}
|
|
||||||
rp = None
|
|
||||||
|
|
||||||
# For each of these check they are both set and not empty string
|
def handle_sigterm(signum, frame):
|
||||||
influxdb_host = os.environ.get("INFLUXDB_HOST")
|
# Turn SIGTERM into an exception so main loop can clean up
|
||||||
if influxdb_host:
|
raise Terminated()
|
||||||
icargs["host"] = influxdb_host
|
|
||||||
influxdb_port = os.environ.get("INFLUXDB_PORT")
|
|
||||||
if influxdb_port:
|
|
||||||
icargs["port"] = int(influxdb_port)
|
|
||||||
influxdb_user = os.environ.get("INFLUXDB_USER")
|
|
||||||
if influxdb_user:
|
|
||||||
icargs["username"] = influxdb_user
|
|
||||||
influxdb_pwd = os.environ.get("INFLUXDB_PWD")
|
|
||||||
if influxdb_pwd:
|
|
||||||
icargs["password"] = influxdb_pwd
|
|
||||||
influxdb_db = os.environ.get("INFLUXDB_DB")
|
|
||||||
if influxdb_db:
|
|
||||||
icargs["database"] = influxdb_db
|
|
||||||
influxdb_rp = os.environ.get("INFLUXDB_RP")
|
|
||||||
if influxdb_rp:
|
|
||||||
rp = influxdb_rp
|
|
||||||
influxdb_ssl = os.environ.get("INFLUXDB_SSL")
|
|
||||||
if influxdb_ssl:
|
|
||||||
icargs["ssl"] = True
|
|
||||||
if influxdb_ssl.lower() == "secure":
|
|
||||||
icargs["verify_ssl"] = True
|
|
||||||
elif influxdb_ssl.lower() == "insecure":
|
|
||||||
icargs["verify_ssl"] = False
|
|
||||||
else:
|
|
||||||
icargs["verify_ssl"] = influxdb_ssl
|
|
||||||
|
|
||||||
if not arg_error:
|
|
||||||
if len(args) > 0:
|
def main():
|
||||||
|
arg_error = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(sys.argv[1:], "ahn:p:rs:t:vC:D:IP:R:SU:")
|
||||||
|
except getopt.GetoptError as err:
|
||||||
|
print(str(err))
|
||||||
arg_error = True
|
arg_error = True
|
||||||
else:
|
|
||||||
for opt, arg in opts:
|
|
||||||
if opt == "-a":
|
|
||||||
samples = -1
|
|
||||||
elif opt == "-h":
|
|
||||||
print_usage = True
|
|
||||||
elif opt == "-n":
|
|
||||||
icargs["host"] = arg
|
|
||||||
elif opt == "-p":
|
|
||||||
icargs["port"] = int(arg)
|
|
||||||
elif opt == "-r":
|
|
||||||
run_lengths = True
|
|
||||||
elif opt == "-s":
|
|
||||||
samples = int(arg)
|
|
||||||
elif opt == "-v":
|
|
||||||
verbose = True
|
|
||||||
elif opt == "-C":
|
|
||||||
icargs["ssl"] = True
|
|
||||||
icargs["verify_ssl"] = arg
|
|
||||||
elif opt == "-D":
|
|
||||||
icargs["database"] = arg
|
|
||||||
elif opt == "-I":
|
|
||||||
icargs["ssl"] = True
|
|
||||||
icargs["verify_ssl"] = False
|
|
||||||
elif opt == "-P":
|
|
||||||
icargs["password"] = arg
|
|
||||||
elif opt == "-R":
|
|
||||||
rp = arg
|
|
||||||
elif opt == "-S":
|
|
||||||
icargs["ssl"] = True
|
|
||||||
icargs["verify_ssl"] = True
|
|
||||||
elif opt == "-U":
|
|
||||||
icargs["username"] = arg
|
|
||||||
|
|
||||||
if "password" in icargs and "username" not in icargs:
|
# Default to 1 hour worth of data samples.
|
||||||
print("Password authentication requires username to be set")
|
samples_default = 3600
|
||||||
arg_error = True
|
samples = None
|
||||||
|
print_usage = False
|
||||||
|
verbose = False
|
||||||
|
default_loop_time = 0
|
||||||
|
loop_time = default_loop_time
|
||||||
|
run_lengths = False
|
||||||
|
host_default = "localhost"
|
||||||
|
database_default = "starlinkstats"
|
||||||
|
icargs = {"host": host_default, "timeout": 5, "database": database_default}
|
||||||
|
rp = None
|
||||||
|
flush_limit = 6
|
||||||
|
|
||||||
if print_usage or arg_error:
|
# For each of these check they are both set and not empty string
|
||||||
print("Usage: " + sys.argv[0] + " [options...]")
|
influxdb_host = os.environ.get("INFLUXDB_HOST")
|
||||||
print("Options:")
|
if influxdb_host:
|
||||||
print(" -a: Parse all valid samples")
|
icargs["host"] = influxdb_host
|
||||||
print(" -h: Be helpful")
|
influxdb_port = os.environ.get("INFLUXDB_PORT")
|
||||||
print(" -n <name>: Hostname of InfluxDB server, default: " + host_default)
|
if influxdb_port:
|
||||||
print(" -p <num>: Port number to use on InfluxDB server")
|
icargs["port"] = int(influxdb_port)
|
||||||
print(" -r: Include ping drop run length stats")
|
influxdb_user = os.environ.get("INFLUXDB_USER")
|
||||||
print(" -s <num>: Number of data samples to parse, default: " + str(samples_default))
|
if influxdb_user:
|
||||||
print(" -v: Be verbose")
|
icargs["username"] = influxdb_user
|
||||||
print(" -C <filename>: Enable SSL/TLS using specified CA cert to verify server")
|
influxdb_pwd = os.environ.get("INFLUXDB_PWD")
|
||||||
print(" -D <name>: Database name to use, default: " + database_default)
|
if influxdb_pwd:
|
||||||
print(" -I: Enable SSL/TLS but disable certificate verification (INSECURE!)")
|
icargs["password"] = influxdb_pwd
|
||||||
print(" -P <word>: Set password for authentication")
|
influxdb_db = os.environ.get("INFLUXDB_DB")
|
||||||
print(" -R <name>: Retention policy name to use")
|
if influxdb_db:
|
||||||
print(" -S: Enable SSL/TLS using default CA cert")
|
icargs["database"] = influxdb_db
|
||||||
print(" -U <name>: Set username for authentication")
|
influxdb_rp = os.environ.get("INFLUXDB_RP")
|
||||||
sys.exit(1 if arg_error else 0)
|
if influxdb_rp:
|
||||||
|
rp = influxdb_rp
|
||||||
logging.basicConfig(format="%(levelname)s: %(message)s")
|
influxdb_ssl = os.environ.get("INFLUXDB_SSL")
|
||||||
|
if influxdb_ssl:
|
||||||
try:
|
icargs["ssl"] = True
|
||||||
dish_id = starlink_grpc.get_id()
|
if influxdb_ssl.lower() == "secure":
|
||||||
except starlink_grpc.GrpcError as e:
|
icargs["verify_ssl"] = True
|
||||||
logging.error("Failure getting dish ID: " + str(e))
|
elif influxdb_ssl.lower() == "insecure":
|
||||||
sys.exit(1)
|
icargs["verify_ssl"] = False
|
||||||
|
|
||||||
timestamp = datetime.datetime.utcnow()
|
|
||||||
|
|
||||||
try:
|
|
||||||
g_stats, pd_stats, rl_stats = starlink_grpc.history_ping_stats(samples, verbose)
|
|
||||||
except starlink_grpc.GrpcError as e:
|
|
||||||
logging.error("Failure getting ping stats: " + str(e))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
all_stats = g_stats.copy()
|
|
||||||
all_stats.update(pd_stats)
|
|
||||||
if run_lengths:
|
|
||||||
for k, v in rl_stats.items():
|
|
||||||
if k.startswith("run_"):
|
|
||||||
for i, subv in enumerate(v, start=1):
|
|
||||||
all_stats[k + "_" + str(i)] = subv
|
|
||||||
else:
|
else:
|
||||||
all_stats[k] = v
|
icargs["verify_ssl"] = influxdb_ssl
|
||||||
|
|
||||||
points = [{
|
if not arg_error:
|
||||||
"measurement": "spacex.starlink.user_terminal.ping_stats",
|
if len(args) > 0:
|
||||||
"tags": {"id": dish_id},
|
arg_error = True
|
||||||
"time": timestamp,
|
else:
|
||||||
"fields": all_stats,
|
for opt, arg in opts:
|
||||||
}]
|
if opt == "-a":
|
||||||
|
samples = -1
|
||||||
|
elif opt == "-h":
|
||||||
|
print_usage = True
|
||||||
|
elif opt == "-n":
|
||||||
|
icargs["host"] = arg
|
||||||
|
elif opt == "-p":
|
||||||
|
icargs["port"] = int(arg)
|
||||||
|
elif opt == "-r":
|
||||||
|
run_lengths = True
|
||||||
|
elif opt == "-s":
|
||||||
|
samples = int(arg)
|
||||||
|
elif opt == "-t":
|
||||||
|
loop_time = float(arg)
|
||||||
|
elif opt == "-v":
|
||||||
|
verbose = True
|
||||||
|
elif opt == "-C":
|
||||||
|
icargs["ssl"] = True
|
||||||
|
icargs["verify_ssl"] = arg
|
||||||
|
elif opt == "-D":
|
||||||
|
icargs["database"] = arg
|
||||||
|
elif opt == "-I":
|
||||||
|
icargs["ssl"] = True
|
||||||
|
icargs["verify_ssl"] = False
|
||||||
|
elif opt == "-P":
|
||||||
|
icargs["password"] = arg
|
||||||
|
elif opt == "-R":
|
||||||
|
rp = arg
|
||||||
|
elif opt == "-S":
|
||||||
|
icargs["ssl"] = True
|
||||||
|
icargs["verify_ssl"] = True
|
||||||
|
elif opt == "-U":
|
||||||
|
icargs["username"] = arg
|
||||||
|
|
||||||
if "verify_ssl" in icargs and not icargs["verify_ssl"]:
|
if "password" in icargs and "username" not in icargs:
|
||||||
# user has explicitly said be insecure, so don't warn about it
|
print("Password authentication requires username to be set")
|
||||||
warnings.filterwarnings("ignore", message="Unverified HTTPS request")
|
arg_error = True
|
||||||
|
|
||||||
influx_client = InfluxDBClient(**icargs)
|
if print_usage or arg_error:
|
||||||
try:
|
print("Usage: " + sys.argv[0] + " [options...]")
|
||||||
influx_client.write_points(points, retention_policy=rp)
|
print("Options:")
|
||||||
rc = 0
|
print(" -a: Parse all valid samples")
|
||||||
except Exception as e:
|
print(" -h: Be helpful")
|
||||||
logging.error("Failed writing to InfluxDB database: " + str(e))
|
print(" -n <name>: Hostname of InfluxDB server, default: " + host_default)
|
||||||
rc = 1
|
print(" -p <num>: Port number to use on InfluxDB server")
|
||||||
finally:
|
print(" -r: Include ping drop run length stats")
|
||||||
influx_client.close()
|
print(" -s <num>: Number of data samples to parse, default: loop interval,")
|
||||||
sys.exit(rc)
|
print(" if set, else " + str(samples_default))
|
||||||
|
print(" -t <num>: Loop interval in seconds or 0 for no loop, default: " +
|
||||||
|
str(default_loop_time))
|
||||||
|
print(" -v: Be verbose")
|
||||||
|
print(" -C <filename>: Enable SSL/TLS using specified CA cert to verify server")
|
||||||
|
print(" -D <name>: Database name to use, default: " + database_default)
|
||||||
|
print(" -I: Enable SSL/TLS but disable certificate verification (INSECURE!)")
|
||||||
|
print(" -P <word>: Set password for authentication")
|
||||||
|
print(" -R <name>: Retention policy name to use")
|
||||||
|
print(" -S: Enable SSL/TLS using default CA cert")
|
||||||
|
print(" -U <name>: Set username for authentication")
|
||||||
|
sys.exit(1 if arg_error else 0)
|
||||||
|
|
||||||
|
if samples is None:
|
||||||
|
samples = int(loop_time) if loop_time > 0 else samples_default
|
||||||
|
|
||||||
|
logging.basicConfig(format="%(levelname)s: %(message)s")
|
||||||
|
|
||||||
|
class GlobalState:
|
||||||
|
pass
|
||||||
|
|
||||||
|
gstate = GlobalState()
|
||||||
|
gstate.dish_id = None
|
||||||
|
gstate.points = []
|
||||||
|
|
||||||
|
def conn_error(msg, *args):
|
||||||
|
# Connection errors that happen in an interval loop are not critical
|
||||||
|
# failures, but are interesting enough to print in non-verbose mode.
|
||||||
|
if loop_time > 0:
|
||||||
|
print(msg % args)
|
||||||
|
else:
|
||||||
|
logging.error(msg, *args)
|
||||||
|
|
||||||
|
def flush_points(client):
|
||||||
|
try:
|
||||||
|
client.write_points(gstate.points, retention_policy=rp)
|
||||||
|
if verbose:
|
||||||
|
print("Data points written: " + str(len(gstate.points)))
|
||||||
|
gstate.points.clear()
|
||||||
|
except Exception as e:
|
||||||
|
conn_error("Failed writing to InfluxDB database: %s", str(e))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def loop_body(client):
|
||||||
|
if gstate.dish_id is None:
|
||||||
|
try:
|
||||||
|
gstate.dish_id = starlink_grpc.get_id()
|
||||||
|
if verbose:
|
||||||
|
print("Using dish ID: " + gstate.dish_id)
|
||||||
|
except starlink_grpc.GrpcError as e:
|
||||||
|
conn_error("Failure getting dish ID: %s", str(e))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
timestamp = datetime.datetime.utcnow()
|
||||||
|
|
||||||
|
try:
|
||||||
|
g_stats, pd_stats, rl_stats = starlink_grpc.history_ping_stats(samples, verbose)
|
||||||
|
except starlink_grpc.GrpcError as e:
|
||||||
|
conn_error("Failure getting ping stats: %s", str(e))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
all_stats = g_stats.copy()
|
||||||
|
all_stats.update(pd_stats)
|
||||||
|
if run_lengths:
|
||||||
|
for k, v in rl_stats.items():
|
||||||
|
if k.startswith("run_"):
|
||||||
|
for i, subv in enumerate(v, start=1):
|
||||||
|
all_stats[k + "_" + str(i)] = subv
|
||||||
|
else:
|
||||||
|
all_stats[k] = v
|
||||||
|
|
||||||
|
gstate.points.append({
|
||||||
|
"measurement": "spacex.starlink.user_terminal.ping_stats",
|
||||||
|
"tags": {
|
||||||
|
"id": gstate.dish_id
|
||||||
|
},
|
||||||
|
"time": timestamp,
|
||||||
|
"fields": all_stats,
|
||||||
|
})
|
||||||
|
if verbose:
|
||||||
|
print("Data points queued: " + str(len(gstate.points)))
|
||||||
|
|
||||||
|
if len(gstate.points) >= flush_limit:
|
||||||
|
return flush_points(client)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if "verify_ssl" in icargs and not icargs["verify_ssl"]:
|
||||||
|
# user has explicitly said be insecure, so don't warn about it
|
||||||
|
warnings.filterwarnings("ignore", message="Unverified HTTPS request")
|
||||||
|
|
||||||
|
signal.signal(signal.SIGTERM, handle_sigterm)
|
||||||
|
influx_client = InfluxDBClient(**icargs)
|
||||||
|
try:
|
||||||
|
next_loop = time.monotonic()
|
||||||
|
while True:
|
||||||
|
rc = loop_body(influx_client)
|
||||||
|
if loop_time > 0:
|
||||||
|
now = time.monotonic()
|
||||||
|
next_loop = max(next_loop + loop_time, now)
|
||||||
|
time.sleep(next_loop - now)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
except Terminated:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
if gstate.points:
|
||||||
|
rc = flush_points(influx_client)
|
||||||
|
influx_client.close()
|
||||||
|
|
||||||
|
sys.exit(rc)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
|
@ -10,9 +10,10 @@
|
||||||
#
|
#
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
import sys
|
|
||||||
import getopt
|
import getopt
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import ssl
|
import ssl
|
||||||
|
@ -24,111 +25,161 @@ import paho.mqtt.publish
|
||||||
|
|
||||||
import starlink_grpc
|
import starlink_grpc
|
||||||
|
|
||||||
arg_error = False
|
|
||||||
|
|
||||||
try:
|
def main():
|
||||||
opts, args = getopt.getopt(sys.argv[1:], "ahn:p:rs:vC:ISP:U:")
|
arg_error = False
|
||||||
except getopt.GetoptError as err:
|
|
||||||
print(str(err))
|
|
||||||
arg_error = True
|
|
||||||
|
|
||||||
# Default to 1 hour worth of data samples.
|
try:
|
||||||
samples_default = 3600
|
opts, args = getopt.getopt(sys.argv[1:], "ahn:p:rs:t:vC:ISP:U:")
|
||||||
samples = samples_default
|
except getopt.GetoptError as err:
|
||||||
print_usage = False
|
print(str(err))
|
||||||
verbose = False
|
|
||||||
run_lengths = False
|
|
||||||
host_default = "localhost"
|
|
||||||
mqargs = {"hostname": host_default}
|
|
||||||
username = None
|
|
||||||
password = None
|
|
||||||
|
|
||||||
if not arg_error:
|
|
||||||
if len(args) > 0:
|
|
||||||
arg_error = True
|
arg_error = True
|
||||||
else:
|
|
||||||
for opt, arg in opts:
|
|
||||||
if opt == "-a":
|
|
||||||
samples = -1
|
|
||||||
elif opt == "-h":
|
|
||||||
print_usage = True
|
|
||||||
elif opt == "-n":
|
|
||||||
mqargs["hostname"] = arg
|
|
||||||
elif opt == "-p":
|
|
||||||
mqargs["port"] = int(arg)
|
|
||||||
elif opt == "-r":
|
|
||||||
run_lengths = True
|
|
||||||
elif opt == "-s":
|
|
||||||
samples = int(arg)
|
|
||||||
elif opt == "-v":
|
|
||||||
verbose = True
|
|
||||||
elif opt == "-C":
|
|
||||||
mqargs["tls"] = {"ca_certs": arg}
|
|
||||||
elif opt == "-I":
|
|
||||||
if ssl_ok:
|
|
||||||
mqargs["tls"] = {"cert_reqs": ssl.CERT_NONE}
|
|
||||||
else:
|
|
||||||
print("No SSL support found")
|
|
||||||
sys.exit(1)
|
|
||||||
elif opt == "-P":
|
|
||||||
password = arg
|
|
||||||
elif opt == "-S":
|
|
||||||
mqargs["tls"] = {}
|
|
||||||
elif opt == "-U":
|
|
||||||
username = arg
|
|
||||||
|
|
||||||
if username is None and password is not None:
|
# Default to 1 hour worth of data samples.
|
||||||
print("Password authentication requires username to be set")
|
samples_default = 3600
|
||||||
arg_error = True
|
samples = None
|
||||||
|
print_usage = False
|
||||||
|
verbose = False
|
||||||
|
default_loop_time = 0
|
||||||
|
loop_time = default_loop_time
|
||||||
|
run_lengths = False
|
||||||
|
host_default = "localhost"
|
||||||
|
mqargs = {"hostname": host_default}
|
||||||
|
username = None
|
||||||
|
password = None
|
||||||
|
|
||||||
if print_usage or arg_error:
|
if not arg_error:
|
||||||
print("Usage: " + sys.argv[0] + " [options...]")
|
if len(args) > 0:
|
||||||
print("Options:")
|
arg_error = True
|
||||||
print(" -a: Parse all valid samples")
|
|
||||||
print(" -h: Be helpful")
|
|
||||||
print(" -n <name>: Hostname of MQTT broker, default: " + host_default)
|
|
||||||
print(" -p <num>: Port number to use on MQTT broker")
|
|
||||||
print(" -r: Include ping drop run length stats")
|
|
||||||
print(" -s <num>: Number of data samples to parse, default: " + str(samples_default))
|
|
||||||
print(" -v: Be verbose")
|
|
||||||
print(" -C <filename>: Enable SSL/TLS using specified CA cert to verify broker")
|
|
||||||
print(" -I: Enable SSL/TLS but disable certificate verification (INSECURE!)")
|
|
||||||
print(" -P: Set password for username/password authentication")
|
|
||||||
print(" -S: Enable SSL/TLS using default CA cert")
|
|
||||||
print(" -U: Set username for authentication")
|
|
||||||
sys.exit(1 if arg_error else 0)
|
|
||||||
|
|
||||||
logging.basicConfig(format="%(levelname)s: %(message)s")
|
|
||||||
|
|
||||||
try:
|
|
||||||
dish_id = starlink_grpc.get_id()
|
|
||||||
except starlink_grpc.GrpcError as e:
|
|
||||||
logging.error("Failure getting dish ID: " + str(e))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
g_stats, pd_stats, rl_stats = starlink_grpc.history_ping_stats(samples, verbose)
|
|
||||||
except starlink_grpc.GrpcError as e:
|
|
||||||
logging.error("Failure getting ping stats: " + str(e))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
topic_prefix = "starlink/dish_ping_stats/" + dish_id + "/"
|
|
||||||
msgs = [(topic_prefix + k, v, 0, False) for k, v in g_stats.items()]
|
|
||||||
msgs.extend([(topic_prefix + k, v, 0, False) for k, v in pd_stats.items()])
|
|
||||||
if run_lengths:
|
|
||||||
for k, v in rl_stats.items():
|
|
||||||
if k.startswith("run_"):
|
|
||||||
msgs.append((topic_prefix + k, ",".join(str(x) for x in v), 0, False))
|
|
||||||
else:
|
else:
|
||||||
msgs.append((topic_prefix + k, v, 0, False))
|
for opt, arg in opts:
|
||||||
|
if opt == "-a":
|
||||||
|
samples = -1
|
||||||
|
elif opt == "-h":
|
||||||
|
print_usage = True
|
||||||
|
elif opt == "-n":
|
||||||
|
mqargs["hostname"] = arg
|
||||||
|
elif opt == "-p":
|
||||||
|
mqargs["port"] = int(arg)
|
||||||
|
elif opt == "-r":
|
||||||
|
run_lengths = True
|
||||||
|
elif opt == "-s":
|
||||||
|
samples = int(arg)
|
||||||
|
elif opt == "-t":
|
||||||
|
loop_time = float(arg)
|
||||||
|
elif opt == "-v":
|
||||||
|
verbose = True
|
||||||
|
elif opt == "-C":
|
||||||
|
mqargs["tls"] = {"ca_certs": arg}
|
||||||
|
elif opt == "-I":
|
||||||
|
if ssl_ok:
|
||||||
|
mqargs["tls"] = {"cert_reqs": ssl.CERT_NONE}
|
||||||
|
else:
|
||||||
|
print("No SSL support found")
|
||||||
|
sys.exit(1)
|
||||||
|
elif opt == "-P":
|
||||||
|
password = arg
|
||||||
|
elif opt == "-S":
|
||||||
|
mqargs["tls"] = {}
|
||||||
|
elif opt == "-U":
|
||||||
|
username = arg
|
||||||
|
|
||||||
if username is not None:
|
if username is None and password is not None:
|
||||||
mqargs["auth"] = {"username": username}
|
print("Password authentication requires username to be set")
|
||||||
if password is not None:
|
arg_error = True
|
||||||
mqargs["auth"]["password"] = password
|
|
||||||
|
|
||||||
try:
|
if print_usage or arg_error:
|
||||||
paho.mqtt.publish.multiple(msgs, client_id=dish_id, **mqargs)
|
print("Usage: " + sys.argv[0] + " [options...]")
|
||||||
except Exception as e:
|
print("Options:")
|
||||||
logging.error("Failed publishing to MQTT broker: " + str(e))
|
print(" -a: Parse all valid samples")
|
||||||
sys.exit(1)
|
print(" -h: Be helpful")
|
||||||
|
print(" -n <name>: Hostname of MQTT broker, default: " + host_default)
|
||||||
|
print(" -p <num>: Port number to use on MQTT broker")
|
||||||
|
print(" -r: Include ping drop run length stats")
|
||||||
|
print(" -s <num>: Number of data samples to parse, default: loop interval,")
|
||||||
|
print(" if set, else " + str(samples_default))
|
||||||
|
print(" -t <num>: Loop interval in seconds or 0 for no loop, default: " +
|
||||||
|
str(default_loop_time))
|
||||||
|
print(" -v: Be verbose")
|
||||||
|
print(" -C <filename>: Enable SSL/TLS using specified CA cert to verify broker")
|
||||||
|
print(" -I: Enable SSL/TLS but disable certificate verification (INSECURE!)")
|
||||||
|
print(" -P: Set password for username/password authentication")
|
||||||
|
print(" -S: Enable SSL/TLS using default CA cert")
|
||||||
|
print(" -U: Set username for authentication")
|
||||||
|
sys.exit(1 if arg_error else 0)
|
||||||
|
|
||||||
|
if samples is None:
|
||||||
|
samples = int(loop_time) if loop_time > 0 else samples_default
|
||||||
|
|
||||||
|
if username is not None:
|
||||||
|
mqargs["auth"] = {"username": username}
|
||||||
|
if password is not None:
|
||||||
|
mqargs["auth"]["password"] = password
|
||||||
|
|
||||||
|
logging.basicConfig(format="%(levelname)s: %(message)s")
|
||||||
|
|
||||||
|
class GlobalState:
|
||||||
|
pass
|
||||||
|
|
||||||
|
gstate = GlobalState()
|
||||||
|
gstate.dish_id = None
|
||||||
|
|
||||||
|
def conn_error(msg, *args):
|
||||||
|
# Connection errors that happen in an interval loop are not critical
|
||||||
|
# failures, but are interesting enough to print in non-verbose mode.
|
||||||
|
if loop_time > 0:
|
||||||
|
print(msg % args)
|
||||||
|
else:
|
||||||
|
logging.error(msg, *args)
|
||||||
|
|
||||||
|
def loop_body():
|
||||||
|
if gstate.dish_id is None:
|
||||||
|
try:
|
||||||
|
gstate.dish_id = starlink_grpc.get_id()
|
||||||
|
if verbose:
|
||||||
|
print("Using dish ID: " + gstate.dish_id)
|
||||||
|
except starlink_grpc.GrpcError as e:
|
||||||
|
conn_error("Failure getting dish ID: %s", str(e))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
g_stats, pd_stats, rl_stats = starlink_grpc.history_ping_stats(samples, verbose)
|
||||||
|
except starlink_grpc.GrpcError as e:
|
||||||
|
conn_error("Failure getting ping stats: %s", str(e))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
topic_prefix = "starlink/dish_ping_stats/" + gstate.dish_id + "/"
|
||||||
|
msgs = [(topic_prefix + k, v, 0, False) for k, v in g_stats.items()]
|
||||||
|
msgs.extend([(topic_prefix + k, v, 0, False) for k, v in pd_stats.items()])
|
||||||
|
if run_lengths:
|
||||||
|
for k, v in rl_stats.items():
|
||||||
|
if k.startswith("run_"):
|
||||||
|
msgs.append((topic_prefix + k, ",".join(str(x) for x in v), 0, False))
|
||||||
|
else:
|
||||||
|
msgs.append((topic_prefix + k, v, 0, False))
|
||||||
|
|
||||||
|
try:
|
||||||
|
paho.mqtt.publish.multiple(msgs, client_id=gstate.dish_id, **mqargs)
|
||||||
|
if verbose:
|
||||||
|
print("Successfully published to MQTT broker")
|
||||||
|
except Exception as e:
|
||||||
|
conn_error("Failed publishing to MQTT broker: %s", str(e))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
next_loop = time.monotonic()
|
||||||
|
while True:
|
||||||
|
rc = loop_body()
|
||||||
|
if loop_time > 0:
|
||||||
|
now = time.monotonic()
|
||||||
|
next_loop = max(next_loop + loop_time, now)
|
||||||
|
time.sleep(next_loop - now)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
sys.exit(rc)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
|
@ -11,105 +11,141 @@
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import sys
|
|
||||||
import getopt
|
import getopt
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
import starlink_grpc
|
import starlink_grpc
|
||||||
|
|
||||||
arg_error = False
|
|
||||||
|
|
||||||
try:
|
def main():
|
||||||
opts, args = getopt.getopt(sys.argv[1:], "ahrs:vH")
|
arg_error = False
|
||||||
except getopt.GetoptError as err:
|
|
||||||
print(str(err))
|
|
||||||
arg_error = True
|
|
||||||
|
|
||||||
# Default to 1 hour worth of data samples.
|
try:
|
||||||
samples_default = 3600
|
opts, args = getopt.getopt(sys.argv[1:], "ahrs:t:vH")
|
||||||
samples = samples_default
|
except getopt.GetoptError as err:
|
||||||
print_usage = False
|
print(str(err))
|
||||||
verbose = False
|
|
||||||
print_header = False
|
|
||||||
run_lengths = False
|
|
||||||
|
|
||||||
if not arg_error:
|
|
||||||
if len(args) > 0:
|
|
||||||
arg_error = True
|
arg_error = True
|
||||||
else:
|
|
||||||
for opt, arg in opts:
|
|
||||||
if opt == "-a":
|
|
||||||
samples = -1
|
|
||||||
elif opt == "-h":
|
|
||||||
print_usage = True
|
|
||||||
elif opt == "-r":
|
|
||||||
run_lengths = True
|
|
||||||
elif opt == "-s":
|
|
||||||
samples = int(arg)
|
|
||||||
elif opt == "-v":
|
|
||||||
verbose = True
|
|
||||||
elif opt == "-H":
|
|
||||||
print_header = True
|
|
||||||
|
|
||||||
if print_usage or arg_error:
|
# Default to 1 hour worth of data samples.
|
||||||
print("Usage: " + sys.argv[0] + " [options...]")
|
samples_default = 3600
|
||||||
print("Options:")
|
samples = None
|
||||||
print(" -a: Parse all valid samples")
|
print_usage = False
|
||||||
print(" -h: Be helpful")
|
verbose = False
|
||||||
print(" -r: Include ping drop run length stats")
|
default_loop_time = 0
|
||||||
print(" -s <num>: Number of data samples to parse, default: " + str(samples_default))
|
loop_time = default_loop_time
|
||||||
print(" -v: Be verbose")
|
run_lengths = False
|
||||||
print(" -H: print CSV header instead of parsing file")
|
print_header = False
|
||||||
sys.exit(1 if arg_error else 0)
|
|
||||||
|
|
||||||
logging.basicConfig(format="%(levelname)s: %(message)s")
|
if not arg_error:
|
||||||
|
if len(args) > 0:
|
||||||
|
arg_error = True
|
||||||
|
else:
|
||||||
|
for opt, arg in opts:
|
||||||
|
if opt == "-a":
|
||||||
|
samples = -1
|
||||||
|
elif opt == "-h":
|
||||||
|
print_usage = True
|
||||||
|
elif opt == "-r":
|
||||||
|
run_lengths = True
|
||||||
|
elif opt == "-s":
|
||||||
|
samples = int(arg)
|
||||||
|
elif opt == "-t":
|
||||||
|
loop_time = float(arg)
|
||||||
|
elif opt == "-v":
|
||||||
|
verbose = True
|
||||||
|
elif opt == "-H":
|
||||||
|
print_header = True
|
||||||
|
|
||||||
g_fields, pd_fields, rl_fields = starlink_grpc.history_ping_field_names()
|
if print_usage or arg_error:
|
||||||
|
print("Usage: " + sys.argv[0] + " [options...]")
|
||||||
|
print("Options:")
|
||||||
|
print(" -a: Parse all valid samples")
|
||||||
|
print(" -h: Be helpful")
|
||||||
|
print(" -r: Include ping drop run length stats")
|
||||||
|
print(" -s <num>: Number of data samples to parse, default: loop interval,")
|
||||||
|
print(" if set, else " + str(samples_default))
|
||||||
|
print(" -t <num>: Loop interval in seconds or 0 for no loop, default: " +
|
||||||
|
str(default_loop_time))
|
||||||
|
print(" -v: Be verbose")
|
||||||
|
print(" -H: print CSV header instead of parsing history data")
|
||||||
|
sys.exit(1 if arg_error else 0)
|
||||||
|
|
||||||
if print_header:
|
if samples is None:
|
||||||
header = ["datetimestamp_utc"]
|
samples = int(loop_time) if loop_time > 0 else samples_default
|
||||||
header.extend(g_fields)
|
|
||||||
header.extend(pd_fields)
|
|
||||||
if run_lengths:
|
|
||||||
for field in rl_fields:
|
|
||||||
if field.startswith("run_"):
|
|
||||||
header.extend(field + "_" + str(x) for x in range(1, 61))
|
|
||||||
else:
|
|
||||||
header.append(field)
|
|
||||||
print(",".join(header))
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
timestamp = datetime.datetime.utcnow()
|
logging.basicConfig(format="%(levelname)s: %(message)s")
|
||||||
|
|
||||||
try:
|
g_fields, pd_fields, rl_fields = starlink_grpc.history_ping_field_names()
|
||||||
g_stats, pd_stats, rl_stats = starlink_grpc.history_ping_stats(samples, verbose)
|
|
||||||
except starlink_grpc.GrpcError as e:
|
|
||||||
logging.error("Failure getting ping stats: " + str(e))
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if verbose:
|
if print_header:
|
||||||
print("Parsed samples: " + str(g_stats["samples"]))
|
header = ["datetimestamp_utc"]
|
||||||
print("Total ping drop: " + str(pd_stats["total_ping_drop"]))
|
header.extend(g_fields)
|
||||||
print("Count of drop == 1: " + str(pd_stats["count_full_ping_drop"]))
|
header.extend(pd_fields)
|
||||||
print("Obstructed: " + str(pd_stats["count_obstructed"]))
|
if run_lengths:
|
||||||
print("Obstructed ping drop: " + str(pd_stats["total_obstructed_ping_drop"]))
|
for field in rl_fields:
|
||||||
print("Obstructed drop == 1: " + str(pd_stats["count_full_obstructed_ping_drop"]))
|
if field.startswith("run_"):
|
||||||
print("Unscheduled: " + str(pd_stats["count_unscheduled"]))
|
header.extend(field + "_" + str(x) for x in range(1, 61))
|
||||||
print("Unscheduled ping drop: " + str(pd_stats["total_unscheduled_ping_drop"]))
|
else:
|
||||||
print("Unscheduled drop == 1: " + str(pd_stats["count_full_unscheduled_ping_drop"]))
|
header.append(field)
|
||||||
if run_lengths:
|
print(",".join(header))
|
||||||
print("Initial drop run fragment: " + str(rl_stats["init_run_fragment"]))
|
sys.exit(0)
|
||||||
print("Final drop run fragment: " + str(rl_stats["final_run_fragment"]))
|
|
||||||
print("Per-second drop runs: " + ", ".join(str(x) for x in rl_stats["run_seconds"]))
|
def loop_body():
|
||||||
print("Per-minute drop runs: " + ", ".join(str(x) for x in rl_stats["run_minutes"]))
|
timestamp = datetime.datetime.utcnow()
|
||||||
else:
|
|
||||||
csv_data = [timestamp.replace(microsecond=0).isoformat()]
|
try:
|
||||||
csv_data.extend(str(g_stats[field]) for field in g_fields)
|
g_stats, pd_stats, rl_stats = starlink_grpc.history_ping_stats(samples, verbose)
|
||||||
csv_data.extend(str(pd_stats[field]) for field in pd_fields)
|
except starlink_grpc.GrpcError as e:
|
||||||
if run_lengths:
|
logging.error("Failure getting ping stats: %s", str(e))
|
||||||
for field in rl_fields:
|
return 1
|
||||||
if field.startswith("run_"):
|
|
||||||
csv_data.extend(str(substat) for substat in rl_stats[field])
|
if verbose:
|
||||||
else:
|
print("Parsed samples: " + str(g_stats["samples"]))
|
||||||
csv_data.append(str(rl_stats[field]))
|
print("Total ping drop: " + str(pd_stats["total_ping_drop"]))
|
||||||
print(",".join(csv_data))
|
print("Count of drop == 1: " + str(pd_stats["count_full_ping_drop"]))
|
||||||
|
print("Obstructed: " + str(pd_stats["count_obstructed"]))
|
||||||
|
print("Obstructed ping drop: " + str(pd_stats["total_obstructed_ping_drop"]))
|
||||||
|
print("Obstructed drop == 1: " + str(pd_stats["count_full_obstructed_ping_drop"]))
|
||||||
|
print("Unscheduled: " + str(pd_stats["count_unscheduled"]))
|
||||||
|
print("Unscheduled ping drop: " + str(pd_stats["total_unscheduled_ping_drop"]))
|
||||||
|
print("Unscheduled drop == 1: " + str(pd_stats["count_full_unscheduled_ping_drop"]))
|
||||||
|
if run_lengths:
|
||||||
|
print("Initial drop run fragment: " + str(rl_stats["init_run_fragment"]))
|
||||||
|
print("Final drop run fragment: " + str(rl_stats["final_run_fragment"]))
|
||||||
|
print("Per-second drop runs: " +
|
||||||
|
", ".join(str(x) for x in rl_stats["run_seconds"]))
|
||||||
|
print("Per-minute drop runs: " +
|
||||||
|
", ".join(str(x) for x in rl_stats["run_minutes"]))
|
||||||
|
if loop_time > 0:
|
||||||
|
print()
|
||||||
|
else:
|
||||||
|
csv_data = [timestamp.replace(microsecond=0).isoformat()]
|
||||||
|
csv_data.extend(str(g_stats[field]) for field in g_fields)
|
||||||
|
csv_data.extend(str(pd_stats[field]) for field in pd_fields)
|
||||||
|
if run_lengths:
|
||||||
|
for field in rl_fields:
|
||||||
|
if field.startswith("run_"):
|
||||||
|
csv_data.extend(str(substat) for substat in rl_stats[field])
|
||||||
|
else:
|
||||||
|
csv_data.append(str(rl_stats[field]))
|
||||||
|
print(",".join(csv_data))
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
next_loop = time.monotonic()
|
||||||
|
while True:
|
||||||
|
rc = loop_body()
|
||||||
|
if loop_time > 0:
|
||||||
|
now = time.monotonic()
|
||||||
|
next_loop = max(next_loop + loop_time, now)
|
||||||
|
time.sleep(next_loop - now)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
sys.exit(rc)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
204
dishStatusCsv.py
204
dishStatusCsv.py
|
@ -1,111 +1,147 @@
|
||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
######################################################################
|
######################################################################
|
||||||
#
|
#
|
||||||
# Output get_status info in CSV format.
|
# Output Starlink user terminal status info in CSV format.
|
||||||
#
|
#
|
||||||
# This script pulls the current status once and prints to stdout.
|
# This script pulls the current status and prints to stdout either
|
||||||
|
# once or in a periodic loop.
|
||||||
#
|
#
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
import sys
|
|
||||||
import getopt
|
import getopt
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
|
||||||
import spacex.api.device.device_pb2
|
import spacex.api.device.device_pb2
|
||||||
import spacex.api.device.device_pb2_grpc
|
import spacex.api.device.device_pb2_grpc
|
||||||
|
|
||||||
arg_error = False
|
|
||||||
|
|
||||||
try:
|
def main():
|
||||||
opts, args = getopt.getopt(sys.argv[1:], "hH")
|
arg_error = False
|
||||||
except getopt.GetoptError as err:
|
|
||||||
print(str(err))
|
|
||||||
arg_error = True
|
|
||||||
|
|
||||||
print_usage = False
|
try:
|
||||||
print_header = False
|
opts, args = getopt.getopt(sys.argv[1:], "ht:H")
|
||||||
|
except getopt.GetoptError as err:
|
||||||
if not arg_error:
|
print(str(err))
|
||||||
if len(args) > 0:
|
|
||||||
arg_error = True
|
arg_error = True
|
||||||
else:
|
|
||||||
for opt, arg in opts:
|
|
||||||
if opt == "-h":
|
|
||||||
print_usage = True
|
|
||||||
elif opt == "-H":
|
|
||||||
print_header = True
|
|
||||||
|
|
||||||
if print_usage or arg_error:
|
print_usage = False
|
||||||
print("Usage: " + sys.argv[0] + " [options...]")
|
default_loop_time = 0
|
||||||
print("Options:")
|
loop_time = default_loop_time
|
||||||
print(" -h: Be helpful")
|
print_header = False
|
||||||
print(" -H: print CSV header instead of parsing file")
|
|
||||||
sys.exit(1 if arg_error else 0)
|
|
||||||
|
|
||||||
logging.basicConfig(format="%(levelname)s: %(message)s")
|
if not arg_error:
|
||||||
|
if len(args) > 0:
|
||||||
|
arg_error = True
|
||||||
|
else:
|
||||||
|
for opt, arg in opts:
|
||||||
|
if opt == "-h":
|
||||||
|
print_usage = True
|
||||||
|
elif opt == "-t":
|
||||||
|
loop_time = float(arg)
|
||||||
|
elif opt == "-H":
|
||||||
|
print_header = True
|
||||||
|
|
||||||
if print_header:
|
if print_usage or arg_error:
|
||||||
header = [
|
print("Usage: " + sys.argv[0] + " [options...]")
|
||||||
"datetimestamp_utc",
|
print("Options:")
|
||||||
"hardware_version",
|
print(" -h: Be helpful")
|
||||||
"software_version",
|
print(" -t <num>: Loop interval in seconds or 0 for no loop, default: " +
|
||||||
"state",
|
str(default_loop_time))
|
||||||
"uptime",
|
print(" -H: print CSV header instead of parsing file")
|
||||||
"snr",
|
sys.exit(1 if arg_error else 0)
|
||||||
"seconds_to_first_nonempty_slot",
|
|
||||||
"pop_ping_drop_rate",
|
|
||||||
"downlink_throughput_bps",
|
|
||||||
"uplink_throughput_bps",
|
|
||||||
"pop_ping_latency_ms",
|
|
||||||
"alerts",
|
|
||||||
"fraction_obstructed",
|
|
||||||
"currently_obstructed",
|
|
||||||
"seconds_obstructed"
|
|
||||||
]
|
|
||||||
header.extend("wedges_fraction_obstructed_" + str(x) for x in range(12))
|
|
||||||
print(",".join(header))
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
try:
|
logging.basicConfig(format="%(levelname)s: %(message)s")
|
||||||
with grpc.insecure_channel("192.168.100.1:9200") as channel:
|
|
||||||
stub = spacex.api.device.device_pb2_grpc.DeviceStub(channel)
|
|
||||||
response = stub.Handle(spacex.api.device.device_pb2.Request(get_status={}))
|
|
||||||
except grpc.RpcError:
|
|
||||||
logging.error("Failed getting status info")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
timestamp = datetime.datetime.utcnow()
|
if print_header:
|
||||||
|
header = [
|
||||||
|
"datetimestamp_utc",
|
||||||
|
"hardware_version",
|
||||||
|
"software_version",
|
||||||
|
"state",
|
||||||
|
"uptime",
|
||||||
|
"snr",
|
||||||
|
"seconds_to_first_nonempty_slot",
|
||||||
|
"pop_ping_drop_rate",
|
||||||
|
"downlink_throughput_bps",
|
||||||
|
"uplink_throughput_bps",
|
||||||
|
"pop_ping_latency_ms",
|
||||||
|
"alerts",
|
||||||
|
"fraction_obstructed",
|
||||||
|
"currently_obstructed",
|
||||||
|
"seconds_obstructed",
|
||||||
|
]
|
||||||
|
header.extend("wedges_fraction_obstructed_" + str(x) for x in range(12))
|
||||||
|
print(",".join(header))
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
status = response.dish_get_status
|
def loop_body():
|
||||||
|
timestamp = datetime.datetime.utcnow()
|
||||||
|
|
||||||
# More alerts may be added in future, so rather than list them individually,
|
try:
|
||||||
# build a bit field based on field numbers of the DishAlerts message.
|
with grpc.insecure_channel("192.168.100.1:9200") as channel:
|
||||||
alert_bits = 0
|
stub = spacex.api.device.device_pb2_grpc.DeviceStub(channel)
|
||||||
for alert in status.alerts.ListFields():
|
response = stub.Handle(spacex.api.device.device_pb2.Request(get_status={}))
|
||||||
alert_bits |= (1 if alert[1] else 0) << (alert[0].number - 1)
|
|
||||||
|
|
||||||
csv_data = [
|
status = response.dish_get_status
|
||||||
timestamp.replace(microsecond=0).isoformat(),
|
|
||||||
status.device_info.id,
|
# More alerts may be added in future, so rather than list them individually,
|
||||||
status.device_info.hardware_version,
|
# build a bit field based on field numbers of the DishAlerts message.
|
||||||
status.device_info.software_version,
|
alert_bits = 0
|
||||||
spacex.api.device.dish_pb2.DishState.Name(status.state)
|
for alert in status.alerts.ListFields():
|
||||||
]
|
alert_bits |= (1 if alert[1] else 0) << (alert[0].number - 1)
|
||||||
csv_data.extend(str(x) for x in [
|
|
||||||
status.device_state.uptime_s,
|
csv_data = [
|
||||||
status.snr,
|
timestamp.replace(microsecond=0).isoformat(),
|
||||||
status.seconds_to_first_nonempty_slot,
|
status.device_info.id,
|
||||||
status.pop_ping_drop_rate,
|
status.device_info.hardware_version,
|
||||||
status.downlink_throughput_bps,
|
status.device_info.software_version,
|
||||||
status.uplink_throughput_bps,
|
spacex.api.device.dish_pb2.DishState.Name(status.state),
|
||||||
status.pop_ping_latency_ms,
|
]
|
||||||
alert_bits,
|
csv_data.extend(
|
||||||
status.obstruction_stats.fraction_obstructed,
|
str(x) for x in [
|
||||||
status.obstruction_stats.currently_obstructed,
|
status.device_state.uptime_s,
|
||||||
status.obstruction_stats.last_24h_obstructed_s
|
status.snr,
|
||||||
])
|
status.seconds_to_first_nonempty_slot,
|
||||||
csv_data.extend(str(x) for x in status.obstruction_stats.wedge_abs_fraction_obstructed)
|
status.pop_ping_drop_rate,
|
||||||
print(",".join(csv_data))
|
status.downlink_throughput_bps,
|
||||||
|
status.uplink_throughput_bps,
|
||||||
|
status.pop_ping_latency_ms,
|
||||||
|
alert_bits,
|
||||||
|
status.obstruction_stats.fraction_obstructed,
|
||||||
|
status.obstruction_stats.currently_obstructed,
|
||||||
|
status.obstruction_stats.last_24h_obstructed_s,
|
||||||
|
])
|
||||||
|
csv_data.extend(str(x) for x in status.obstruction_stats.wedge_abs_fraction_obstructed)
|
||||||
|
rc = 0
|
||||||
|
except grpc.RpcError:
|
||||||
|
if loop_time <= 0:
|
||||||
|
logging.error("Failed getting status info")
|
||||||
|
csv_data = [
|
||||||
|
timestamp.replace(microsecond=0).isoformat(), "", "", "", "DISH_UNREACHABLE"
|
||||||
|
]
|
||||||
|
rc = 1
|
||||||
|
|
||||||
|
print(",".join(csv_data))
|
||||||
|
|
||||||
|
return rc
|
||||||
|
|
||||||
|
next_loop = time.monotonic()
|
||||||
|
while True:
|
||||||
|
rc = loop_body()
|
||||||
|
if loop_time > 0:
|
||||||
|
now = time.monotonic()
|
||||||
|
next_loop = max(next_loop + loop_time, now)
|
||||||
|
time.sleep(next_loop - now)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
sys.exit(rc)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
|
@ -8,249 +8,276 @@
|
||||||
#
|
#
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
import time
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import getopt
|
import getopt
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
import grpc
|
||||||
from influxdb import InfluxDBClient
|
from influxdb import InfluxDBClient
|
||||||
from influxdb import SeriesHelper
|
from influxdb import SeriesHelper
|
||||||
|
|
||||||
import grpc
|
|
||||||
|
|
||||||
import spacex.api.device.device_pb2
|
import spacex.api.device.device_pb2
|
||||||
import spacex.api.device.device_pb2_grpc
|
import spacex.api.device.device_pb2_grpc
|
||||||
|
|
||||||
arg_error = False
|
|
||||||
|
|
||||||
try:
|
class Terminated(Exception):
|
||||||
opts, args = getopt.getopt(sys.argv[1:], "hn:p:t:vC:D:IP:R:SU:")
|
pass
|
||||||
except getopt.GetoptError as err:
|
|
||||||
print(str(err))
|
|
||||||
arg_error = True
|
|
||||||
|
|
||||||
print_usage = False
|
|
||||||
verbose = False
|
|
||||||
host_default = "localhost"
|
|
||||||
database_default = "starlinkstats"
|
|
||||||
icargs = {"host": host_default, "timeout": 5, "database": database_default}
|
|
||||||
rp = None
|
|
||||||
default_sleep_time = 30
|
|
||||||
sleep_time = default_sleep_time
|
|
||||||
|
|
||||||
# For each of these check they are both set and not empty string
|
def handle_sigterm(signum, frame):
|
||||||
influxdb_host = os.environ.get("INFLUXDB_HOST")
|
# Turn SIGTERM into an exception so main loop can clean up
|
||||||
if influxdb_host:
|
raise Terminated()
|
||||||
icargs["host"] = influxdb_host
|
|
||||||
influxdb_port = os.environ.get("INFLUXDB_PORT")
|
|
||||||
if influxdb_port:
|
|
||||||
icargs["port"] = int(influxdb_port)
|
|
||||||
influxdb_user = os.environ.get("INFLUXDB_USER")
|
|
||||||
if influxdb_user:
|
|
||||||
icargs["username"] = influxdb_user
|
|
||||||
influxdb_pwd = os.environ.get("INFLUXDB_PWD")
|
|
||||||
if influxdb_pwd:
|
|
||||||
icargs["password"] = influxdb_pwd
|
|
||||||
influxdb_db = os.environ.get("INFLUXDB_DB")
|
|
||||||
if influxdb_db:
|
|
||||||
icargs["database"] = influxdb_db
|
|
||||||
influxdb_rp = os.environ.get("INFLUXDB_RP")
|
|
||||||
if influxdb_rp:
|
|
||||||
rp = influxdb_rp
|
|
||||||
influxdb_ssl = os.environ.get("INFLUXDB_SSL")
|
|
||||||
if influxdb_ssl:
|
|
||||||
icargs["ssl"] = True
|
|
||||||
if influxdb_ssl.lower() == "secure":
|
|
||||||
icargs["verify_ssl"] = True
|
|
||||||
elif influxdb_ssl.lower() == "insecure":
|
|
||||||
icargs["verify_ssl"] = False
|
|
||||||
else:
|
|
||||||
icargs["verify_ssl"] = influxdb_ssl
|
|
||||||
|
|
||||||
if not arg_error:
|
|
||||||
if len(args) > 0:
|
def main():
|
||||||
|
arg_error = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(sys.argv[1:], "hn:p:t:vC:D:IP:R:SU:")
|
||||||
|
except getopt.GetoptError as err:
|
||||||
|
print(str(err))
|
||||||
arg_error = True
|
arg_error = True
|
||||||
else:
|
|
||||||
for opt, arg in opts:
|
|
||||||
if opt == "-h":
|
|
||||||
print_usage = True
|
|
||||||
elif opt == "-n":
|
|
||||||
icargs["host"] = arg
|
|
||||||
elif opt == "-p":
|
|
||||||
icargs["port"] = int(arg)
|
|
||||||
elif opt == "-t":
|
|
||||||
sleep_time = int(arg)
|
|
||||||
elif opt == "-v":
|
|
||||||
verbose = True
|
|
||||||
elif opt == "-C":
|
|
||||||
icargs["ssl"] = True
|
|
||||||
icargs["verify_ssl"] = arg
|
|
||||||
elif opt == "-D":
|
|
||||||
icargs["database"] = arg
|
|
||||||
elif opt == "-I":
|
|
||||||
icargs["ssl"] = True
|
|
||||||
icargs["verify_ssl"] = False
|
|
||||||
elif opt == "-P":
|
|
||||||
icargs["password"] = arg
|
|
||||||
elif opt == "-R":
|
|
||||||
rp = arg
|
|
||||||
elif opt == "-S":
|
|
||||||
icargs["ssl"] = True
|
|
||||||
icargs["verify_ssl"] = True
|
|
||||||
elif opt == "-U":
|
|
||||||
icargs["username"] = arg
|
|
||||||
|
|
||||||
if "password" in icargs and "username" not in icargs:
|
print_usage = False
|
||||||
print("Password authentication requires username to be set")
|
verbose = False
|
||||||
arg_error = True
|
default_loop_time = 0
|
||||||
|
loop_time = default_loop_time
|
||||||
|
host_default = "localhost"
|
||||||
|
database_default = "starlinkstats"
|
||||||
|
icargs = {"host": host_default, "timeout": 5, "database": database_default}
|
||||||
|
rp = None
|
||||||
|
flush_limit = 6
|
||||||
|
|
||||||
if print_usage or arg_error:
|
# For each of these check they are both set and not empty string
|
||||||
print("Usage: " + sys.argv[0] + " [options...]")
|
influxdb_host = os.environ.get("INFLUXDB_HOST")
|
||||||
print("Options:")
|
if influxdb_host:
|
||||||
print(" -h: Be helpful")
|
icargs["host"] = influxdb_host
|
||||||
print(" -n <name>: Hostname of InfluxDB server, default: " + host_default)
|
influxdb_port = os.environ.get("INFLUXDB_PORT")
|
||||||
print(" -p <num>: Port number to use on InfluxDB server")
|
if influxdb_port:
|
||||||
print(" -t <num>: Loop interval in seconds or 0 for no loop, default: " +
|
icargs["port"] = int(influxdb_port)
|
||||||
str(default_sleep_time))
|
influxdb_user = os.environ.get("INFLUXDB_USER")
|
||||||
print(" -v: Be verbose")
|
if influxdb_user:
|
||||||
print(" -C <filename>: Enable SSL/TLS using specified CA cert to verify server")
|
icargs["username"] = influxdb_user
|
||||||
print(" -D <name>: Database name to use, default: " + database_default)
|
influxdb_pwd = os.environ.get("INFLUXDB_PWD")
|
||||||
print(" -I: Enable SSL/TLS but disable certificate verification (INSECURE!)")
|
if influxdb_pwd:
|
||||||
print(" -P <word>: Set password for authentication")
|
icargs["password"] = influxdb_pwd
|
||||||
print(" -R <name>: Retention policy name to use")
|
influxdb_db = os.environ.get("INFLUXDB_DB")
|
||||||
print(" -S: Enable SSL/TLS using default CA cert")
|
if influxdb_db:
|
||||||
print(" -U <name>: Set username for authentication")
|
icargs["database"] = influxdb_db
|
||||||
sys.exit(1 if arg_error else 0)
|
influxdb_rp = os.environ.get("INFLUXDB_RP")
|
||||||
|
if influxdb_rp:
|
||||||
|
rp = influxdb_rp
|
||||||
|
influxdb_ssl = os.environ.get("INFLUXDB_SSL")
|
||||||
|
if influxdb_ssl:
|
||||||
|
icargs["ssl"] = True
|
||||||
|
if influxdb_ssl.lower() == "secure":
|
||||||
|
icargs["verify_ssl"] = True
|
||||||
|
elif influxdb_ssl.lower() == "insecure":
|
||||||
|
icargs["verify_ssl"] = False
|
||||||
|
else:
|
||||||
|
icargs["verify_ssl"] = influxdb_ssl
|
||||||
|
|
||||||
logging.basicConfig(format="%(levelname)s: %(message)s")
|
if not arg_error:
|
||||||
|
if len(args) > 0:
|
||||||
|
arg_error = True
|
||||||
|
else:
|
||||||
|
for opt, arg in opts:
|
||||||
|
if opt == "-h":
|
||||||
|
print_usage = True
|
||||||
|
elif opt == "-n":
|
||||||
|
icargs["host"] = arg
|
||||||
|
elif opt == "-p":
|
||||||
|
icargs["port"] = int(arg)
|
||||||
|
elif opt == "-t":
|
||||||
|
loop_time = int(arg)
|
||||||
|
elif opt == "-v":
|
||||||
|
verbose = True
|
||||||
|
elif opt == "-C":
|
||||||
|
icargs["ssl"] = True
|
||||||
|
icargs["verify_ssl"] = arg
|
||||||
|
elif opt == "-D":
|
||||||
|
icargs["database"] = arg
|
||||||
|
elif opt == "-I":
|
||||||
|
icargs["ssl"] = True
|
||||||
|
icargs["verify_ssl"] = False
|
||||||
|
elif opt == "-P":
|
||||||
|
icargs["password"] = arg
|
||||||
|
elif opt == "-R":
|
||||||
|
rp = arg
|
||||||
|
elif opt == "-S":
|
||||||
|
icargs["ssl"] = True
|
||||||
|
icargs["verify_ssl"] = True
|
||||||
|
elif opt == "-U":
|
||||||
|
icargs["username"] = arg
|
||||||
|
|
||||||
def conn_error(msg):
|
if "password" in icargs and "username" not in icargs:
|
||||||
# Connection errors that happen while running in an interval loop are
|
print("Password authentication requires username to be set")
|
||||||
# not critical failures, because they can (usually) be retried, or
|
arg_error = True
|
||||||
# because they will be recorded as dish state unavailable. They're still
|
|
||||||
# interesting, though, so print them even in non-verbose mode.
|
|
||||||
if sleep_time > 0:
|
|
||||||
print(msg)
|
|
||||||
else:
|
|
||||||
logging.error(msg)
|
|
||||||
|
|
||||||
class DeviceStatusSeries(SeriesHelper):
|
if print_usage or arg_error:
|
||||||
class Meta:
|
print("Usage: " + sys.argv[0] + " [options...]")
|
||||||
series_name = "spacex.starlink.user_terminal.status"
|
print("Options:")
|
||||||
fields = [
|
print(" -h: Be helpful")
|
||||||
"hardware_version",
|
print(" -n <name>: Hostname of InfluxDB server, default: " + host_default)
|
||||||
"software_version",
|
print(" -p <num>: Port number to use on InfluxDB server")
|
||||||
"state",
|
print(" -t <num>: Loop interval in seconds or 0 for no loop, default: " +
|
||||||
"alert_motors_stuck",
|
str(default_loop_time))
|
||||||
"alert_thermal_throttle",
|
print(" -v: Be verbose")
|
||||||
"alert_thermal_shutdown",
|
print(" -C <filename>: Enable SSL/TLS using specified CA cert to verify server")
|
||||||
"alert_unexpected_location",
|
print(" -D <name>: Database name to use, default: " + database_default)
|
||||||
"snr",
|
print(" -I: Enable SSL/TLS but disable certificate verification (INSECURE!)")
|
||||||
"seconds_to_first_nonempty_slot",
|
print(" -P <word>: Set password for authentication")
|
||||||
"pop_ping_drop_rate",
|
print(" -R <name>: Retention policy name to use")
|
||||||
"downlink_throughput_bps",
|
print(" -S: Enable SSL/TLS using default CA cert")
|
||||||
"uplink_throughput_bps",
|
print(" -U <name>: Set username for authentication")
|
||||||
"pop_ping_latency_ms",
|
sys.exit(1 if arg_error else 0)
|
||||||
"currently_obstructed",
|
|
||||||
"fraction_obstructed"]
|
|
||||||
tags = ["id"]
|
|
||||||
retention_policy = rp
|
|
||||||
|
|
||||||
if "verify_ssl" in icargs and not icargs["verify_ssl"]:
|
logging.basicConfig(format="%(levelname)s: %(message)s")
|
||||||
# user has explicitly said be insecure, so don't warn about it
|
|
||||||
warnings.filterwarnings("ignore", message="Unverified HTTPS request")
|
|
||||||
|
|
||||||
influx_client = InfluxDBClient(**icargs)
|
class GlobalState:
|
||||||
|
pass
|
||||||
|
|
||||||
rc = 0
|
gstate = GlobalState()
|
||||||
try:
|
gstate.dish_channel = None
|
||||||
dish_channel = None
|
gstate.dish_id = None
|
||||||
last_id = None
|
gstate.pending = 0
|
||||||
last_failed = False
|
|
||||||
|
|
||||||
pending = 0
|
class DeviceStatusSeries(SeriesHelper):
|
||||||
count = 0
|
class Meta:
|
||||||
while True:
|
series_name = "spacex.starlink.user_terminal.status"
|
||||||
|
fields = [
|
||||||
|
"hardware_version",
|
||||||
|
"software_version",
|
||||||
|
"state",
|
||||||
|
"alert_motors_stuck",
|
||||||
|
"alert_thermal_throttle",
|
||||||
|
"alert_thermal_shutdown",
|
||||||
|
"alert_unexpected_location",
|
||||||
|
"snr",
|
||||||
|
"seconds_to_first_nonempty_slot",
|
||||||
|
"pop_ping_drop_rate",
|
||||||
|
"downlink_throughput_bps",
|
||||||
|
"uplink_throughput_bps",
|
||||||
|
"pop_ping_latency_ms",
|
||||||
|
"currently_obstructed",
|
||||||
|
"fraction_obstructed",
|
||||||
|
]
|
||||||
|
tags = ["id"]
|
||||||
|
retention_policy = rp
|
||||||
|
|
||||||
|
def conn_error(msg, *args):
|
||||||
|
# Connection errors that happen in an interval loop are not critical
|
||||||
|
# failures, but are interesting enough to print in non-verbose mode.
|
||||||
|
if loop_time > 0:
|
||||||
|
print(msg % args)
|
||||||
|
else:
|
||||||
|
logging.error(msg, *args)
|
||||||
|
|
||||||
|
def flush_pending(client):
|
||||||
try:
|
try:
|
||||||
if dish_channel is None:
|
DeviceStatusSeries.commit(client)
|
||||||
dish_channel = grpc.insecure_channel("192.168.100.1:9200")
|
if verbose:
|
||||||
stub = spacex.api.device.device_pb2_grpc.DeviceStub(dish_channel)
|
print("Data points written: " + str(gstate.pending))
|
||||||
response = stub.Handle(spacex.api.device.device_pb2.Request(get_status={}))
|
gstate.pending = 0
|
||||||
status = response.dish_get_status
|
except Exception as e:
|
||||||
DeviceStatusSeries(
|
conn_error("Failed writing to InfluxDB database: %s", str(e))
|
||||||
id=status.device_info.id,
|
return 1
|
||||||
hardware_version=status.device_info.hardware_version,
|
|
||||||
software_version=status.device_info.software_version,
|
return 0
|
||||||
state=spacex.api.device.dish_pb2.DishState.Name(status.state),
|
|
||||||
alert_motors_stuck=status.alerts.motors_stuck,
|
def get_status_retry():
|
||||||
alert_thermal_throttle=status.alerts.thermal_throttle,
|
"""Try getting the status at most twice"""
|
||||||
alert_thermal_shutdown=status.alerts.thermal_shutdown,
|
|
||||||
alert_unexpected_location=status.alerts.unexpected_location,
|
channel_reused = True
|
||||||
snr=status.snr,
|
while True:
|
||||||
seconds_to_first_nonempty_slot=status.seconds_to_first_nonempty_slot,
|
try:
|
||||||
pop_ping_drop_rate=status.pop_ping_drop_rate,
|
if gstate.dish_channel is None:
|
||||||
downlink_throughput_bps=status.downlink_throughput_bps,
|
gstate.dish_channel = grpc.insecure_channel("192.168.100.1:9200")
|
||||||
uplink_throughput_bps=status.uplink_throughput_bps,
|
channel_reused = False
|
||||||
pop_ping_latency_ms=status.pop_ping_latency_ms,
|
stub = spacex.api.device.device_pb2_grpc.DeviceStub(gstate.dish_channel)
|
||||||
currently_obstructed=status.obstruction_stats.currently_obstructed,
|
response = stub.Handle(spacex.api.device.device_pb2.Request(get_status={}))
|
||||||
fraction_obstructed=status.obstruction_stats.fraction_obstructed)
|
return response.dish_get_status
|
||||||
pending += 1
|
except grpc.RpcError:
|
||||||
last_id = status.device_info.id
|
gstate.dish_channel.close()
|
||||||
last_failed = False
|
gstate.dish_channel = None
|
||||||
except grpc.RpcError:
|
if channel_reused:
|
||||||
if dish_channel is not None:
|
# If the channel was open already, the connection may have
|
||||||
dish_channel.close()
|
# been lost in the time since prior loop iteration, so after
|
||||||
dish_channel = None
|
# closing it, retry once, in case the dish is now reachable.
|
||||||
if last_failed:
|
|
||||||
if last_id is None:
|
|
||||||
conn_error("Dish unreachable and ID unknown, so not recording state")
|
|
||||||
# When not looping, report this as failure exit status
|
|
||||||
rc = 1
|
|
||||||
else:
|
|
||||||
if verbose:
|
if verbose:
|
||||||
print("Dish unreachable")
|
print("Dish RPC channel error")
|
||||||
DeviceStatusSeries(id=last_id, state="DISH_UNREACHABLE")
|
else:
|
||||||
pending += 1
|
raise
|
||||||
|
|
||||||
|
def loop_body(client):
|
||||||
|
try:
|
||||||
|
status = get_status_retry()
|
||||||
|
DeviceStatusSeries(id=status.device_info.id,
|
||||||
|
hardware_version=status.device_info.hardware_version,
|
||||||
|
software_version=status.device_info.software_version,
|
||||||
|
state=spacex.api.device.dish_pb2.DishState.Name(status.state),
|
||||||
|
alert_motors_stuck=status.alerts.motors_stuck,
|
||||||
|
alert_thermal_throttle=status.alerts.thermal_throttle,
|
||||||
|
alert_thermal_shutdown=status.alerts.thermal_shutdown,
|
||||||
|
alert_unexpected_location=status.alerts.unexpected_location,
|
||||||
|
snr=status.snr,
|
||||||
|
seconds_to_first_nonempty_slot=status.seconds_to_first_nonempty_slot,
|
||||||
|
pop_ping_drop_rate=status.pop_ping_drop_rate,
|
||||||
|
downlink_throughput_bps=status.downlink_throughput_bps,
|
||||||
|
uplink_throughput_bps=status.uplink_throughput_bps,
|
||||||
|
pop_ping_latency_ms=status.pop_ping_latency_ms,
|
||||||
|
currently_obstructed=status.obstruction_stats.currently_obstructed,
|
||||||
|
fraction_obstructed=status.obstruction_stats.fraction_obstructed)
|
||||||
|
gstate.dish_id = status.device_info.id
|
||||||
|
except grpc.RpcError:
|
||||||
|
if gstate.dish_id is None:
|
||||||
|
conn_error("Dish unreachable and ID unknown, so not recording state")
|
||||||
|
return 1
|
||||||
else:
|
else:
|
||||||
if verbose:
|
if verbose:
|
||||||
print("Dish RPC channel error")
|
print("Dish unreachable")
|
||||||
# Retry once, because the connection may have been lost while
|
DeviceStatusSeries(id=gstate.dish_id, state="DISH_UNREACHABLE")
|
||||||
# we were sleeping
|
|
||||||
last_failed = True
|
gstate.pending += 1
|
||||||
continue
|
|
||||||
if verbose:
|
if verbose:
|
||||||
print("Samples queued: " + str(pending))
|
print("Data points queued: " + str(gstate.pending))
|
||||||
count += 1
|
if gstate.pending >= flush_limit:
|
||||||
if count > 5:
|
return flush_pending(client)
|
||||||
try:
|
|
||||||
if pending:
|
return 0
|
||||||
DeviceStatusSeries.commit(influx_client)
|
|
||||||
rc = 0
|
if "verify_ssl" in icargs and not icargs["verify_ssl"]:
|
||||||
if verbose:
|
# user has explicitly said be insecure, so don't warn about it
|
||||||
print("Samples written: " + str(pending))
|
warnings.filterwarnings("ignore", message="Unverified HTTPS request")
|
||||||
pending = 0
|
|
||||||
except Exception as e:
|
signal.signal(signal.SIGTERM, handle_sigterm)
|
||||||
conn_error("Failed to write: " + str(e))
|
influx_client = InfluxDBClient(**icargs)
|
||||||
rc = 1
|
|
||||||
count = 0
|
|
||||||
if sleep_time > 0:
|
|
||||||
time.sleep(sleep_time)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
finally:
|
|
||||||
# Flush on error/exit
|
|
||||||
try:
|
try:
|
||||||
if pending:
|
next_loop = time.monotonic()
|
||||||
DeviceStatusSeries.commit(influx_client)
|
while True:
|
||||||
rc = 0
|
rc = loop_body(influx_client)
|
||||||
if verbose:
|
if loop_time > 0:
|
||||||
print("Samples written: " + str(pending))
|
now = time.monotonic()
|
||||||
except Exception as e:
|
next_loop = max(next_loop + loop_time, now)
|
||||||
conn_error("Failed to write: " + str(e))
|
time.sleep(next_loop - now)
|
||||||
rc = 1
|
else:
|
||||||
influx_client.close()
|
break
|
||||||
if dish_channel is not None:
|
except Terminated:
|
||||||
dish_channel.close()
|
pass
|
||||||
|
finally:
|
||||||
|
# Flush on error/exit
|
||||||
|
if gstate.pending:
|
||||||
|
rc = flush_pending(influx_client)
|
||||||
|
influx_client.close()
|
||||||
|
if gstate.dish_channel is not None:
|
||||||
|
gstate.dish_channel.close()
|
||||||
|
|
||||||
sys.exit(rc)
|
sys.exit(rc)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
|
@ -3,14 +3,15 @@
|
||||||
#
|
#
|
||||||
# Publish Starlink user terminal status info to a MQTT broker.
|
# Publish Starlink user terminal status info to a MQTT broker.
|
||||||
#
|
#
|
||||||
# This script pulls the current status once and publishes it to the
|
# This script pulls the current status and publishes it to the
|
||||||
# specified MQTT broker.
|
# specified MQTT broker either once or in a periodic loop.
|
||||||
#
|
#
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
import sys
|
|
||||||
import getopt
|
import getopt
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import ssl
|
import ssl
|
||||||
|
@ -18,116 +19,170 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
ssl_ok = False
|
ssl_ok = False
|
||||||
|
|
||||||
import paho.mqtt.publish
|
|
||||||
|
|
||||||
import grpc
|
import grpc
|
||||||
|
import paho.mqtt.publish
|
||||||
|
|
||||||
import spacex.api.device.device_pb2
|
import spacex.api.device.device_pb2
|
||||||
import spacex.api.device.device_pb2_grpc
|
import spacex.api.device.device_pb2_grpc
|
||||||
|
|
||||||
arg_error = False
|
|
||||||
|
|
||||||
try:
|
def main():
|
||||||
opts, args = getopt.getopt(sys.argv[1:], "hn:p:C:ISP:U:")
|
arg_error = False
|
||||||
except getopt.GetoptError as err:
|
|
||||||
print(str(err))
|
|
||||||
arg_error = True
|
|
||||||
|
|
||||||
print_usage = False
|
try:
|
||||||
host_default = "localhost"
|
opts, args = getopt.getopt(sys.argv[1:], "hn:p:t:vC:ISP:U:")
|
||||||
mqargs = {"hostname": host_default}
|
except getopt.GetoptError as err:
|
||||||
username = None
|
print(str(err))
|
||||||
password = None
|
|
||||||
|
|
||||||
if not arg_error:
|
|
||||||
if len(args) > 0:
|
|
||||||
arg_error = True
|
arg_error = True
|
||||||
else:
|
|
||||||
for opt, arg in opts:
|
|
||||||
if opt == "-h":
|
|
||||||
print_usage = True
|
|
||||||
elif opt == "-n":
|
|
||||||
mqargs["hostname"] = arg
|
|
||||||
elif opt == "-p":
|
|
||||||
mqargs["port"] = int(arg)
|
|
||||||
elif opt == "-C":
|
|
||||||
mqargs["tls"] = {"ca_certs": arg}
|
|
||||||
elif opt == "-I":
|
|
||||||
if ssl_ok:
|
|
||||||
mqargs["tls"] = {"cert_reqs": ssl.CERT_NONE}
|
|
||||||
else:
|
|
||||||
print("No SSL support found")
|
|
||||||
sys.exit(1)
|
|
||||||
elif opt == "-P":
|
|
||||||
password = arg
|
|
||||||
elif opt == "-S":
|
|
||||||
mqargs["tls"] = {}
|
|
||||||
elif opt == "-U":
|
|
||||||
username = arg
|
|
||||||
|
|
||||||
if username is None and password is not None:
|
print_usage = False
|
||||||
print("Password authentication requires username to be set")
|
verbose = False
|
||||||
arg_error = True
|
default_loop_time = 0
|
||||||
|
loop_time = default_loop_time
|
||||||
|
host_default = "localhost"
|
||||||
|
mqargs = {"hostname": host_default}
|
||||||
|
username = None
|
||||||
|
password = None
|
||||||
|
|
||||||
if print_usage or arg_error:
|
if not arg_error:
|
||||||
print("Usage: " + sys.argv[0] + " [options...]")
|
if len(args) > 0:
|
||||||
print("Options:")
|
arg_error = True
|
||||||
print(" -h: Be helpful")
|
else:
|
||||||
print(" -n <name>: Hostname of MQTT broker, default: " + host_default)
|
for opt, arg in opts:
|
||||||
print(" -p <num>: Port number to use on MQTT broker")
|
if opt == "-h":
|
||||||
print(" -C <filename>: Enable SSL/TLS using specified CA cert to verify broker")
|
print_usage = True
|
||||||
print(" -I: Enable SSL/TLS but disable certificate verification (INSECURE!)")
|
elif opt == "-n":
|
||||||
print(" -P: Set password for username/password authentication")
|
mqargs["hostname"] = arg
|
||||||
print(" -S: Enable SSL/TLS using default CA cert")
|
elif opt == "-p":
|
||||||
print(" -U: Set username for authentication")
|
mqargs["port"] = int(arg)
|
||||||
sys.exit(1 if arg_error else 0)
|
elif opt == "-t":
|
||||||
|
loop_time = float(arg)
|
||||||
|
elif opt == "-v":
|
||||||
|
verbose = True
|
||||||
|
elif opt == "-C":
|
||||||
|
mqargs["tls"] = {"ca_certs": arg}
|
||||||
|
elif opt == "-I":
|
||||||
|
if ssl_ok:
|
||||||
|
mqargs["tls"] = {"cert_reqs": ssl.CERT_NONE}
|
||||||
|
else:
|
||||||
|
print("No SSL support found")
|
||||||
|
sys.exit(1)
|
||||||
|
elif opt == "-P":
|
||||||
|
password = arg
|
||||||
|
elif opt == "-S":
|
||||||
|
mqargs["tls"] = {}
|
||||||
|
elif opt == "-U":
|
||||||
|
username = arg
|
||||||
|
|
||||||
logging.basicConfig(format="%(levelname)s: %(message)s")
|
if username is None and password is not None:
|
||||||
|
print("Password authentication requires username to be set")
|
||||||
|
arg_error = True
|
||||||
|
|
||||||
try:
|
if print_usage or arg_error:
|
||||||
with grpc.insecure_channel("192.168.100.1:9200") as channel:
|
print("Usage: " + sys.argv[0] + " [options...]")
|
||||||
stub = spacex.api.device.device_pb2_grpc.DeviceStub(channel)
|
print("Options:")
|
||||||
response = stub.Handle(spacex.api.device.device_pb2.Request(get_status={}))
|
print(" -h: Be helpful")
|
||||||
except grpc.RpcError:
|
print(" -n <name>: Hostname of MQTT broker, default: " + host_default)
|
||||||
logging.error("Failed getting status info")
|
print(" -p <num>: Port number to use on MQTT broker")
|
||||||
sys.exit(1)
|
print(" -t <num>: Loop interval in seconds or 0 for no loop, default: " +
|
||||||
|
str(default_loop_time))
|
||||||
|
print(" -v: Be verbose")
|
||||||
|
print(" -C <filename>: Enable SSL/TLS using specified CA cert to verify broker")
|
||||||
|
print(" -I: Enable SSL/TLS but disable certificate verification (INSECURE!)")
|
||||||
|
print(" -P: Set password for username/password authentication")
|
||||||
|
print(" -S: Enable SSL/TLS using default CA cert")
|
||||||
|
print(" -U: Set username for authentication")
|
||||||
|
sys.exit(1 if arg_error else 0)
|
||||||
|
|
||||||
status = response.dish_get_status
|
if username is not None:
|
||||||
|
mqargs["auth"] = {"username": username}
|
||||||
|
if password is not None:
|
||||||
|
mqargs["auth"]["password"] = password
|
||||||
|
|
||||||
# More alerts may be added in future, so rather than list them individually,
|
logging.basicConfig(format="%(levelname)s: %(message)s")
|
||||||
# build a bit field based on field numbers of the DishAlerts message.
|
|
||||||
alert_bits = 0
|
|
||||||
for alert in status.alerts.ListFields():
|
|
||||||
alert_bits |= (1 if alert[1] else 0) << (alert[0].number - 1)
|
|
||||||
|
|
||||||
topic_prefix = "starlink/dish_status/" + status.device_info.id + "/"
|
class GlobalState:
|
||||||
msgs = [(topic_prefix + "hardware_version", status.device_info.hardware_version, 0, False),
|
pass
|
||||||
(topic_prefix + "software_version", status.device_info.software_version, 0, False),
|
|
||||||
(topic_prefix + "state", spacex.api.device.dish_pb2.DishState.Name(status.state), 0, False),
|
|
||||||
(topic_prefix + "uptime", status.device_state.uptime_s, 0, False),
|
|
||||||
(topic_prefix + "snr", status.snr, 0, False),
|
|
||||||
(topic_prefix + "seconds_to_first_nonempty_slot", status.seconds_to_first_nonempty_slot, 0, False),
|
|
||||||
(topic_prefix + "pop_ping_drop_rate", status.pop_ping_drop_rate, 0, False),
|
|
||||||
(topic_prefix + "downlink_throughput_bps", status.downlink_throughput_bps, 0, False),
|
|
||||||
(topic_prefix + "uplink_throughput_bps", status.uplink_throughput_bps, 0, False),
|
|
||||||
(topic_prefix + "pop_ping_latency_ms", status.pop_ping_latency_ms, 0, False),
|
|
||||||
(topic_prefix + "alerts", alert_bits, 0, False),
|
|
||||||
(topic_prefix + "fraction_obstructed", status.obstruction_stats.fraction_obstructed, 0, False),
|
|
||||||
(topic_prefix + "currently_obstructed", status.obstruction_stats.currently_obstructed, 0, False),
|
|
||||||
# While the field name for this one implies it covers 24 hours, the
|
|
||||||
# empirical evidence suggests it only covers 12 hours. It also resets
|
|
||||||
# on dish reboot, so may not cover that whole period. Rather than try
|
|
||||||
# to convey that complexity in the topic label, just be a bit vague:
|
|
||||||
(topic_prefix + "seconds_obstructed", status.obstruction_stats.last_24h_obstructed_s, 0, False),
|
|
||||||
(topic_prefix + "wedges_fraction_obstructed", ",".join(str(x) for x in status.obstruction_stats.wedge_abs_fraction_obstructed), 0, False)]
|
|
||||||
|
|
||||||
if username is not None:
|
gstate = GlobalState()
|
||||||
mqargs["auth"] = {"username": username}
|
gstate.dish_id = None
|
||||||
if password is not None:
|
|
||||||
mqargs["auth"]["password"] = password
|
|
||||||
|
|
||||||
try:
|
def conn_error(msg, *args):
|
||||||
paho.mqtt.publish.multiple(msgs, client_id=status.device_info.id, **mqargs)
|
# Connection errors that happen in an interval loop are not critical
|
||||||
except Exception as e:
|
# failures, but are interesting enough to print in non-verbose mode.
|
||||||
logging.error("Failed publishing to MQTT broker: " + str(e))
|
if loop_time > 0:
|
||||||
sys.exit(1)
|
print(msg % args)
|
||||||
|
else:
|
||||||
|
logging.error(msg, *args)
|
||||||
|
|
||||||
|
def loop_body():
|
||||||
|
try:
|
||||||
|
with grpc.insecure_channel("192.168.100.1:9200") as channel:
|
||||||
|
stub = spacex.api.device.device_pb2_grpc.DeviceStub(channel)
|
||||||
|
response = stub.Handle(spacex.api.device.device_pb2.Request(get_status={}))
|
||||||
|
|
||||||
|
status = response.dish_get_status
|
||||||
|
|
||||||
|
# More alerts may be added in future, so rather than list them individually,
|
||||||
|
# build a bit field based on field numbers of the DishAlerts message.
|
||||||
|
alert_bits = 0
|
||||||
|
for alert in status.alerts.ListFields():
|
||||||
|
alert_bits |= (1 if alert[1] else 0) << (alert[0].number - 1)
|
||||||
|
|
||||||
|
gstate.dish_id = status.device_info.id
|
||||||
|
topic_prefix = "starlink/dish_status/" + gstate.dish_id + "/"
|
||||||
|
msgs = [
|
||||||
|
(topic_prefix + "hardware_version", status.device_info.hardware_version, 0, False),
|
||||||
|
(topic_prefix + "software_version", status.device_info.software_version, 0, False),
|
||||||
|
(topic_prefix + "state", spacex.api.device.dish_pb2.DishState.Name(status.state), 0, False),
|
||||||
|
(topic_prefix + "uptime", status.device_state.uptime_s, 0, False),
|
||||||
|
(topic_prefix + "snr", status.snr, 0, False),
|
||||||
|
(topic_prefix + "seconds_to_first_nonempty_slot", status.seconds_to_first_nonempty_slot, 0, False),
|
||||||
|
(topic_prefix + "pop_ping_drop_rate", status.pop_ping_drop_rate, 0, False),
|
||||||
|
(topic_prefix + "downlink_throughput_bps", status.downlink_throughput_bps, 0, False),
|
||||||
|
(topic_prefix + "uplink_throughput_bps", status.uplink_throughput_bps, 0, False),
|
||||||
|
(topic_prefix + "pop_ping_latency_ms", status.pop_ping_latency_ms, 0, False),
|
||||||
|
(topic_prefix + "alerts", alert_bits, 0, False),
|
||||||
|
(topic_prefix + "fraction_obstructed", status.obstruction_stats.fraction_obstructed, 0, False),
|
||||||
|
(topic_prefix + "currently_obstructed", status.obstruction_stats.currently_obstructed, 0, False),
|
||||||
|
# While the field name for this one implies it covers 24 hours, the
|
||||||
|
# empirical evidence suggests it only covers 12 hours. It also resets
|
||||||
|
# on dish reboot, so may not cover that whole period. Rather than try
|
||||||
|
# to convey that complexity in the topic label, just be a bit vague:
|
||||||
|
(topic_prefix + "seconds_obstructed", status.obstruction_stats.last_24h_obstructed_s, 0, False),
|
||||||
|
(topic_prefix + "wedges_fraction_obstructed", ",".join(str(x) for x in status.obstruction_stats.wedge_abs_fraction_obstructed), 0, False),
|
||||||
|
]
|
||||||
|
except grpc.RpcError:
|
||||||
|
if gstate.dish_id is None:
|
||||||
|
conn_error("Dish unreachable and ID unknown, so not recording state")
|
||||||
|
return 1
|
||||||
|
if verbose:
|
||||||
|
print("Dish unreachable")
|
||||||
|
topic_prefix = "starlink/dish_status/" + gstate.dish_id + "/"
|
||||||
|
msgs = [(topic_prefix + "state", "DISH_UNREACHABLE", 0, False)]
|
||||||
|
|
||||||
|
try:
|
||||||
|
paho.mqtt.publish.multiple(msgs, client_id=gstate.dish_id, **mqargs)
|
||||||
|
if verbose:
|
||||||
|
print("Successfully published to MQTT broker")
|
||||||
|
except Exception as e:
|
||||||
|
conn_error("Failed publishing to MQTT broker: %s", str(e))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
next_loop = time.monotonic()
|
||||||
|
while True:
|
||||||
|
rc = loop_body()
|
||||||
|
if loop_time > 0:
|
||||||
|
now = time.monotonic()
|
||||||
|
next_loop = max(next_loop + loop_time, now)
|
||||||
|
time.sleep(next_loop - now)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
sys.exit(rc)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
printenv >> /etc/environment
|
printenv >> /etc/environment
|
||||||
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||||
grpcurl -plaintext -protoset-out dish.protoset 192.168.100.1:9200 describe SpaceX.API.Device.Device
|
grpcurl -plaintext -protoset-out dish.protoset 192.168.100.1:9200 describe SpaceX.API.Device.Device > /dev/null
|
||||||
python3 -m grpc_tools.protoc --descriptor_set_in=dish.protoset --python_out=. --grpc_python_out=. spacex/api/device/device.proto
|
python3 -m grpc_tools.protoc --descriptor_set_in=dish.protoset --python_out=. --grpc_python_out=. spacex/api/device/device.proto
|
||||||
python3 -m grpc_tools.protoc --descriptor_set_in=dish.protoset --python_out=. --grpc_python_out=. spacex/api/common/status/status.proto
|
python3 -m grpc_tools.protoc --descriptor_set_in=dish.protoset --python_out=. --grpc_python_out=. spacex/api/common/status/status.proto
|
||||||
python3 -m grpc_tools.protoc --descriptor_set_in=dish.protoset --python_out=. --grpc_python_out=. spacex/api/device/command.proto
|
python3 -m grpc_tools.protoc --descriptor_set_in=dish.protoset --python_out=. --grpc_python_out=. spacex/api/device/command.proto
|
||||||
|
@ -10,4 +10,4 @@ python3 -m grpc_tools.protoc --descriptor_set_in=dish.protoset --python_out=. --
|
||||||
python3 -m grpc_tools.protoc --descriptor_set_in=dish.protoset --python_out=. --grpc_python_out=. spacex/api/device/dish.proto
|
python3 -m grpc_tools.protoc --descriptor_set_in=dish.protoset --python_out=. --grpc_python_out=. spacex/api/device/dish.proto
|
||||||
python3 -m grpc_tools.protoc --descriptor_set_in=dish.protoset --python_out=. --grpc_python_out=. spacex/api/device/wifi.proto
|
python3 -m grpc_tools.protoc --descriptor_set_in=dish.protoset --python_out=. --grpc_python_out=. spacex/api/device/wifi.proto
|
||||||
python3 -m grpc_tools.protoc --descriptor_set_in=dish.protoset --python_out=. --grpc_python_out=. spacex/api/device/wifi_config.proto
|
python3 -m grpc_tools.protoc --descriptor_set_in=dish.protoset --python_out=. --grpc_python_out=. spacex/api/device/wifi_config.proto
|
||||||
/usr/local/bin/python3 $@
|
exec /usr/local/bin/python3 $@
|
||||||
|
|
|
@ -89,7 +89,7 @@ try:
|
||||||
g_stats, pd_stats, rl_stats = starlink_json.history_ping_stats(args[0] if args else "-",
|
g_stats, pd_stats, rl_stats = starlink_json.history_ping_stats(args[0] if args else "-",
|
||||||
samples, verbose)
|
samples, verbose)
|
||||||
except starlink_json.JsonError as e:
|
except starlink_json.JsonError as e:
|
||||||
logging.error("Failure getting ping stats: " + str(e))
|
logging.error("Failure getting ping stats: %s", str(e))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if verbose:
|
if verbose:
|
||||||
|
|
|
@ -108,6 +108,7 @@ def get_status():
|
||||||
response = stub.Handle(spacex.api.device.device_pb2.Request(get_status={}))
|
response = stub.Handle(spacex.api.device.device_pb2.Request(get_status={}))
|
||||||
return response.dish_get_status
|
return response.dish_get_status
|
||||||
|
|
||||||
|
|
||||||
def get_id():
|
def get_id():
|
||||||
"""Return the ID from the dish status information.
|
"""Return the ID from the dish status information.
|
||||||
|
|
||||||
|
@ -124,6 +125,7 @@ def get_id():
|
||||||
except grpc.RpcError as e:
|
except grpc.RpcError as e:
|
||||||
raise GrpcError(e)
|
raise GrpcError(e)
|
||||||
|
|
||||||
|
|
||||||
def history_ping_field_names():
|
def history_ping_field_names():
|
||||||
"""Return the field names of the packet loss stats.
|
"""Return the field names of the packet loss stats.
|
||||||
|
|
||||||
|
@ -133,7 +135,7 @@ def history_ping_field_names():
|
||||||
stat names.
|
stat names.
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
"samples"
|
"samples",
|
||||||
], [
|
], [
|
||||||
"total_ping_drop",
|
"total_ping_drop",
|
||||||
"count_full_ping_drop",
|
"count_full_ping_drop",
|
||||||
|
@ -142,14 +144,15 @@ def history_ping_field_names():
|
||||||
"count_full_obstructed_ping_drop",
|
"count_full_obstructed_ping_drop",
|
||||||
"count_unscheduled",
|
"count_unscheduled",
|
||||||
"total_unscheduled_ping_drop",
|
"total_unscheduled_ping_drop",
|
||||||
"count_full_unscheduled_ping_drop"
|
"count_full_unscheduled_ping_drop",
|
||||||
], [
|
], [
|
||||||
"init_run_fragment",
|
"init_run_fragment",
|
||||||
"final_run_fragment",
|
"final_run_fragment",
|
||||||
"run_seconds",
|
"run_seconds",
|
||||||
"run_minutes"
|
"run_minutes",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_history():
|
def get_history():
|
||||||
"""Fetch history data and return it in grpc structure format.
|
"""Fetch history data and return it in grpc structure format.
|
||||||
|
|
||||||
|
@ -161,6 +164,7 @@ def get_history():
|
||||||
response = stub.Handle(spacex.api.device.device_pb2.Request(get_history={}))
|
response = stub.Handle(spacex.api.device.device_pb2.Request(get_history={}))
|
||||||
return response.dish_get_history
|
return response.dish_get_history
|
||||||
|
|
||||||
|
|
||||||
def history_ping_stats(parse_samples, verbose=False):
|
def history_ping_stats(parse_samples, verbose=False):
|
||||||
"""Fetch, parse, and compute the packet loss stats.
|
"""Fetch, parse, and compute the packet loss stats.
|
||||||
|
|
||||||
|
@ -239,7 +243,7 @@ def history_ping_stats(parse_samples, verbose=False):
|
||||||
if run_length <= 60:
|
if run_length <= 60:
|
||||||
second_runs[run_length - 1] += run_length
|
second_runs[run_length - 1] += run_length
|
||||||
else:
|
else:
|
||||||
minute_runs[min((run_length - 1)//60 - 1, 59)] += run_length
|
minute_runs[min((run_length-1) // 60 - 1, 59)] += run_length
|
||||||
run_length = 0
|
run_length = 0
|
||||||
elif init_run_length is None:
|
elif init_run_length is None:
|
||||||
init_run_length = 0
|
init_run_length = 0
|
||||||
|
@ -267,7 +271,7 @@ def history_ping_stats(parse_samples, verbose=False):
|
||||||
run_length = 0
|
run_length = 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"samples": parse_samples
|
"samples": parse_samples,
|
||||||
}, {
|
}, {
|
||||||
"total_ping_drop": tot,
|
"total_ping_drop": tot,
|
||||||
"count_full_ping_drop": count_full_drop,
|
"count_full_ping_drop": count_full_drop,
|
||||||
|
@ -276,10 +280,10 @@ def history_ping_stats(parse_samples, verbose=False):
|
||||||
"count_full_obstructed_ping_drop": count_full_obstruct,
|
"count_full_obstructed_ping_drop": count_full_obstruct,
|
||||||
"count_unscheduled": count_unsched,
|
"count_unscheduled": count_unsched,
|
||||||
"total_unscheduled_ping_drop": total_unsched_drop,
|
"total_unscheduled_ping_drop": total_unsched_drop,
|
||||||
"count_full_unscheduled_ping_drop": count_full_unsched
|
"count_full_unscheduled_ping_drop": count_full_unsched,
|
||||||
}, {
|
}, {
|
||||||
"init_run_fragment": init_run_length,
|
"init_run_fragment": init_run_length,
|
||||||
"final_run_fragment": run_length,
|
"final_run_fragment": run_length,
|
||||||
"run_seconds": second_runs,
|
"run_seconds": second_runs,
|
||||||
"run_minutes": minute_runs
|
"run_minutes": minute_runs,
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ def history_ping_field_names():
|
||||||
stat names.
|
stat names.
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
"samples"
|
"samples",
|
||||||
], [
|
], [
|
||||||
"total_ping_drop",
|
"total_ping_drop",
|
||||||
"count_full_ping_drop",
|
"count_full_ping_drop",
|
||||||
|
@ -37,14 +37,15 @@ def history_ping_field_names():
|
||||||
"count_full_obstructed_ping_drop",
|
"count_full_obstructed_ping_drop",
|
||||||
"count_unscheduled",
|
"count_unscheduled",
|
||||||
"total_unscheduled_ping_drop",
|
"total_unscheduled_ping_drop",
|
||||||
"count_full_unscheduled_ping_drop"
|
"count_full_unscheduled_ping_drop",
|
||||||
], [
|
], [
|
||||||
"init_run_fragment",
|
"init_run_fragment",
|
||||||
"final_run_fragment",
|
"final_run_fragment",
|
||||||
"run_seconds",
|
"run_seconds",
|
||||||
"run_minutes"
|
"run_minutes",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_history(filename):
|
def get_history(filename):
|
||||||
"""Read JSON data and return the raw history in dict format.
|
"""Read JSON data and return the raw history in dict format.
|
||||||
|
|
||||||
|
@ -63,6 +64,7 @@ def get_history(filename):
|
||||||
json_data = json.load(json_file)
|
json_data = json.load(json_file)
|
||||||
return json_data["dishGetHistory"]
|
return json_data["dishGetHistory"]
|
||||||
|
|
||||||
|
|
||||||
def history_ping_stats(filename, parse_samples, verbose=False):
|
def history_ping_stats(filename, parse_samples, verbose=False):
|
||||||
"""Fetch, parse, and compute the packet loss stats.
|
"""Fetch, parse, and compute the packet loss stats.
|
||||||
|
|
||||||
|
@ -144,7 +146,7 @@ def history_ping_stats(filename, parse_samples, verbose=False):
|
||||||
if run_length <= 60:
|
if run_length <= 60:
|
||||||
second_runs[run_length - 1] += run_length
|
second_runs[run_length - 1] += run_length
|
||||||
else:
|
else:
|
||||||
minute_runs[min((run_length - 1)//60 - 1, 59)] += run_length
|
minute_runs[min((run_length-1) // 60 - 1, 59)] += run_length
|
||||||
run_length = 0
|
run_length = 0
|
||||||
elif init_run_length is None:
|
elif init_run_length is None:
|
||||||
init_run_length = 0
|
init_run_length = 0
|
||||||
|
@ -172,7 +174,7 @@ def history_ping_stats(filename, parse_samples, verbose=False):
|
||||||
run_length = 0
|
run_length = 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"samples": parse_samples
|
"samples": parse_samples,
|
||||||
}, {
|
}, {
|
||||||
"total_ping_drop": tot,
|
"total_ping_drop": tot,
|
||||||
"count_full_ping_drop": count_full_drop,
|
"count_full_ping_drop": count_full_drop,
|
||||||
|
@ -181,10 +183,10 @@ def history_ping_stats(filename, parse_samples, verbose=False):
|
||||||
"count_full_obstructed_ping_drop": count_full_obstruct,
|
"count_full_obstructed_ping_drop": count_full_obstruct,
|
||||||
"count_unscheduled": count_unsched,
|
"count_unscheduled": count_unsched,
|
||||||
"total_unscheduled_ping_drop": total_unsched_drop,
|
"total_unscheduled_ping_drop": total_unsched_drop,
|
||||||
"count_full_unscheduled_ping_drop": count_full_unsched
|
"count_full_unscheduled_ping_drop": count_full_unsched,
|
||||||
}, {
|
}, {
|
||||||
"init_run_fragment": init_run_length,
|
"init_run_fragment": init_run_length,
|
||||||
"final_run_fragment": run_length,
|
"final_run_fragment": run_length,
|
||||||
"run_seconds": second_runs,
|
"run_seconds": second_runs,
|
||||||
"run_minutes": minute_runs
|
"run_minutes": minute_runs,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue