Skip to content
 
 

Repository files navigation

arris_cable_modem_stats

This is a Python script to scrape stats from a cable modem's web interface. Results are meant to be sent to InfluxDB for use with Grafana, but other targets could be added. Supported modems are the Arris SB8200 and the Netgear CM1000 / CM1000v2. Credit goes to https://github.com/billimek/SB6183-stats-for-influxdb

Which modem is scraped is selected with the modem_model config setting (sb8200 or cm1000).

Authentication

In late Oct 2020, Comcast deployed firmware updates to the SB8200 which now require authenticating against the modem. If your modem requires authentication (you get a login page when browsing to https://192.168.100.1/), then you must edit your config.ini file (or set the matching ENV variables) and set modem_auth_required to True, and set modem_password appropriately. By default, your modem's password is the last eight characters of the serial number, located on a sticker on the bottom of the modem.

There is some kind of bug (at least with Comcast's firmware) where the modem cannot handle more than ~10 sessions. Once those sessions have been used up, it seems you must wait for them to expire or reboot the modem. I have not been able to successfully log out of the sessions, but this script attempts to keep reusing the same session as long as it can.

Netgear CM1000 / CM1000v2

The Netgear CM1000 (and CM1000v2) DOCSIS 3.1 modem is supported by setting modem_model = cm1000. A few differences from the SB8200:

  • The status page lives at http://192.168.100.1/DocsisStatus.asp (plain HTTP — the management interface does not offer HTTPS).
  • Authentication is not HTTP Basic Auth. Login is a form POST guarded by a CSRF-like webToken: the script GETs /GenieLogin.asp (to pick up cookies and scrape the token), POSTs the credentials to /goform/GenieLogin, and then fetches the status page with the resulting session cookie. The default username is admin; the password is whatever you set when you first configured the modem.
  • The modem returns HTTP 200 with a JavaScript redirect stub when you are not logged in, so success is detected by inspecting the page content rather than the status code.
  • Both the DOCSIS 3.0 (QAM/ATDMA) and DOCSIS 3.1 (OFDM/OFDMA) channel tables are parsed; un-provisioned "Not Locked" channels are skipped.

Example config for a CM1000 behind Comcast (the rest of the settings are the same as the SB8200):

modem_model = cm1000
modem_url = http://192.168.100.1/DocsisStatus.asp
modem_verify_ssl = False
modem_auth_required = True
modem_username = admin
modem_password = your_modem_password

The CM1000 writes to the same InfluxDB measurements (downstream_statistics / upstream_statistics) as the SB8200, so the existing SB8200 Grafana dashboard works for it without changes.

One admin session at a time. The CM1000 only allows a single admin session, so logging in from the script will log out a browser session on the modem (and vice versa). The script keeps and reuses one session across polls to minimize this; if the session expires it simply logs in again on the next interval.

Netgear firmware revisions vary in how DocsisStatus.asp is laid out. If you get "Failed to get any downstream/upstream stats" errors, run with --debug to log the parsed channel lists, and open an issue with a copy of your modem's page source so the parser can be adjusted.

CM1000 Event Log (optional)

The CM1000 also exposes a DOCSIS event log at http://192.168.100.1/EventLog.asp (T3 timeouts, re-registrations, CM-STATUS messages, etc.). Set poll_event_log = True to have the script scrape it each interval and write the entries to a separate docsis_events InfluxDB measurement, which you can overlay as annotations in Grafana to correlate events with downstream power/SNR dips.

  • poll_event_log = False
    • When True (and the modem supports it — currently cm1000), each poll also fetches the event log and writes docsis_events points (tags: level, priority, event_id; fields: count, description).

Each event is timestamped with the modem's own "first seen" time, so re-polling overwrites the same point instead of duplicating. Note the CM1000 reports times in its local clock — if that isn't UTC, the events will be offset from the channel-stats points; set the modem's time zone (or account for the offset in Grafana) if you need them to line up exactly.

Run Locally

  • Install Python 3.8.x or later

  • Clone repo and

    • $ cd arris_cable_modem_stats/src
  • Install virtualenv

    • $ python3 -m pip install virtualenv
  • Create and activate virtualenv

    • $ python3 -m venv venv
    • $ source venv/bin/activate
  • Install pip dependencies

    • python3 -m pip install -r requirements.txt
  • Edit config.ini and change influx_host to your influxdb server

  • If your cable modem requires authentication, edit config.ini and set:

    • modem_auth_required = True
    • modem_password = last_8_chars_of_modem_serial
  • Run arris_stats.py

    • python3 arris_stats.py --config config.ini

Docker

Run in a Docker container with:

docker build -t arris_stats .
docker run arris_stats

Note that the same parameters from config.ini can be set as ENV variables, ENV overrides config.ini.

Local monitoring stack (InfluxDB + Grafana)

For a full local setup on a Mac — the scraper plus InfluxDB and Grafana via Docker Compose, how to keep it all running across reboots, the home-network-vs-Docker-network gotcha, and how to split InfluxDB or Grafana onto another host later — see docs/local-stack.md. A ready-to-use docker-compose.yml (InfluxDB + Grafana, with the Grafana datasource auto-provisioned) is included.

Config Settings

Config settings can be provided by the config.ini file, or set as ENV variables. ENV variables override config.ini.

  • arris_stats_debug = False
    • enables debug logs
  • destination = influxdb
    • influxdb or file. Use file to dump each poll's data as JSON Lines (see below) when you don't have InfluxDB set up.
  • file_path = modem_stats.jsonl
    • Output file used when destination = file (relative to the working directory unless absolute).
  • sleep_interval = 300
  • modem_url = https://192.168.100.1/cmconnectionstatus.html
  • modem_verify_ssl = False
  • modem_auth_required = False
  • modem_username = admin
  • modem_password = None
  • modem_model = sb8200
    • sb8200 (Arris) and cm1000 (Netgear CM1000 / CM1000v2) are supported
  • poll_event_log = False
    • When True, also scrape the modem's DOCSIS event log into a docsis_events measurement (currently supported by cm1000). See the CM1000 Event Log section below.
  • exit_on_auth_error = True
    • Any auth error will cause an exit, useful when running in a Docker container to get a new session
  • exit_on_html_error = True
    • Any error retrieving the html will cause an exit, mostly redundant with exit_on_auth_error
  • clear_auth_token_on_html_error = True
    • This is useful if you don't want to exit, but do want to get a new session if/when getting the stats fails
  • sleep_before_exit = True
    • If you want to sleep before exiting on errors, useful for Docker container when you have restart = always
  • heartbeat_url = None
    • Optional. If set, the scraper pings this URL after every successful poll — point it at a dead-man's-switch service (e.g. Healthchecks.io) to be alerted when the monitor stops running. See "Heartbeat monitoring" below.
  • influx_host = localhost
  • influx_port = 8086
  • influx_database = cable_modem_stats
    • This will be created automatically if it can
  • influx_username = None
  • influx_password = None
  • influx_use_ssl = False
  • influx_verify_ssl = True

Debugging

You can enable debug logs in three ways:

  1. Use --debug when running from cli
    • pipenv run python3 sb8200_stats.py --debug --config config.ini
  2. Set ENV variable arris_stats_debug = true
  3. Set config.ini arris_stats_debug = true

Heartbeat monitoring (know when the monitor dies)

The scraper runs on a machine on your LAN (it has to reach the modem), so if that box reboots, loses power, or the process dies, your graphs just quietly stop. Set heartbeat_url to a dead-man's-switch endpoint and the scraper pings it after every successful poll; if the pings stop, that service alerts you.

The easiest option is the free tier of Healthchecks.io:

  1. Create a check and copy its ping URL (looks like https://hc-ping.com/<uuid>).
  2. Set the check's period to just above your sleep_interval (e.g. period 1 min for sleep_interval = 60) and a grace of a few minutes so a single slow/missed poll doesn't false-alarm.
  3. Add its email / SMS / push integrations.
  4. In config.ini:
heartbeat_url = https://hc-ping.com/your-uuid-here
sleep_interval = 60

The ping fires once per completed poll cycle (modem scraped + parsed). The same URL works with an AWS API Gateway → Lambda → SNS setup if you'd rather stay AWS-native; only the URL changes.

No InfluxDB? Dump to a file

If you just want to try it (or don't have a time-series DB yet), set destination = file. Each poll appends the same points InfluxDB would receive to file_path as JSON Lines (one JSON object per line):

destination = file
file_path = modem_stats.jsonl
python3 src/arris_stats.py --config src/config.ini --debug
tail -f modem_stats.jsonl

Each line looks like {"measurement": "downstream_statistics", "time": "...", "fields": {...}, "tags": {"channel_id": 20}}, so it's easy to inspect with jq or import later.

InfluxDB

The database will be created automatically if the user has permissions (config.ini defaults to anonymous access). You can set the database name in config.ini using the [INFLUXDB] database parameter.

Grafana

There are two Grafana examples. The first only relies on the Python script from this repo, while the second relies on Telegraf.

SB8200 Dashboard

  • Setup arrris_stats.py to run from somewhere (There's a Docker example below)
  • Import a new dashboard using the grafana/sb8200_grafana.json file. Originally exported from Grafana v6.3.3

SB8200 Dashboard 1 SB8200 Dashboard 2

Internet Uptime Dashboard

Internet Uptime

GoDaddy Dynamic DNS (keep a DNS record pointed at your home IP)

The Arris SB8200 is a bridge modem and its status page does not expose your public WAN IP, so it can't be scraped for this. Instead, src/godaddy_ddns.py discovers your public IP by querying an external "what's my IP" echo service, then keeps a specific GoDaddy DNS record in sync with it. It's a standalone companion to arris_stats.py and follows the same config.ini / ENV pattern, so you can run it on the same box (e.g. once an hour) to keep a reliable DNS name pointed at your home network.

It only writes to GoDaddy when your IP has actually changed (it reads the current record value first and compares), so it's safe to run frequently.

GoDaddy credentials

GoDaddy exposes a Domains API. There are two ways to authenticate, and the script supports both:

  1. Personal Access Token (PAT) — recommended. Create one at developer.godaddy.com/en/personal-access-token. A PAT is a single scoped token that can be given an expiry and revoked independently. Put it in godaddy_api_token; the script sends it as an Authorization: Bearer <token> header. Make sure the token has DNS read + write scope.
  2. Legacy sso-key key/secret. Older accounts may have a key + secret pair (from the classic keys page). Set godaddy_api_key and godaddy_api_secret instead and the script sends an Authorization: sso-key <key>:<secret> header. This is only used if godaddy_api_token is not set, and GoDaddy has this method slated for deprecation, so prefer a PAT.

(GoDaddy formerly required 10+ domains or a Discount Domain Club membership to use this API; that threshold has since been lowered so a single-domain account works.)

Run Locally

Uses the same virtualenv / dependencies as arris_stats.py (only requests is needed).

  • Edit src/godaddy_ddns_config.ini and set at least:
    • godaddy_api_token (your Personal Access Token) — or, if using legacy auth, both godaddy_api_key and godaddy_api_secret
    • godaddy_domain (e.g. example.com)
    • godaddy_record_name (e.g. home, or @ for the root domain)
  • Run it:
    • python3 godaddy_ddns.py --config godaddy_ddns_config.ini

Run as a service (Linux / systemd)

To have it run continuously like the modem monitor — starting at boot and restarting on failure — install the systemd unit at deploy/godaddy-ddns.service. The script has its own hourly loop, so it runs as a long-lived service (no timer needed). The unit can read your token/domain from a locked-down /etc/godaddy-ddns.env file so the Personal Access Token never lives in the repo. Setup steps (venv, env file, install, enable) are in the header comments of that file; the short version:

sudo cp deploy/godaddy-ddns.service /etc/systemd/system/godaddy-ddns.service
sudo sed -i -e "s#/ABSOLUTE/PATH/TO/arris_cable_modem_stats#$PWD#g" -e "s#REPLACE_WITH_USERNAME#$USER#g" /etc/systemd/system/godaddy-ddns.service
sudo systemctl enable --now godaddy-ddns.service
journalctl -u godaddy-ddns -f

Docker

docker build -t godaddy_ddns -f Dockerfile.godaddy_ddns .
docker run -e godaddy_api_token=... -e godaddy_domain=example.com -e godaddy_record_name=home godaddy_ddns

As with arris_stats.py, any config.ini setting can be provided as an ENV variable, and ENV overrides config.ini.

GoDaddy DDNS Config Settings

  • godaddy_ddns_debug = False
    • enables debug logs
  • sleep_interval = 3600
    • how often to check, in seconds (default is once per hour)
  • ip_lookup_urls = https://api.ipify.org,https://checkip.amazonaws.com,https://ifconfig.me/ip
    • comma separated list of services that return your public IP as plain text, tried in order until one works
  • ip_lookup_timeout = 10
  • godaddy_api_url = https://api.godaddy.com
  • godaddy_api_token = None
    • a GoDaddy Personal Access Token (recommended); sent as an Authorization: Bearer header
  • godaddy_api_key = None
    • legacy sso-key API key (only used if godaddy_api_token is not set)
  • godaddy_api_secret = None
    • legacy sso-key API secret (only used if godaddy_api_token is not set)
  • godaddy_domain = None
    • the domain to update, e.g. example.com
  • godaddy_record_name = @
    • the record/host to update, e.g. home, or @ for the root domain
  • godaddy_record_type = A
  • godaddy_record_ttl = 3600
  • godaddy_api_timeout = 15
  • exit_on_error = False
    • if True, any error will cause an exit (useful when running in a Docker container with restart = always to retry)
  • sleep_before_exit = True
    • if you want to sleep before exiting on errors, useful for Docker containers

About

Retrieves stats from Arris cable modems and sends to InfluxDB

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages