Add Netgear CM1000/CM1000v2 support and per-modem module structure - #1
Closed
mphuff wants to merge 13 commits into
Closed
Add Netgear CM1000/CM1000v2 support and per-modem module structure#1mphuff wants to merge 13 commits into
mphuff wants to merge 13 commits into
Conversation
Introduce monitoring for the Netgear CM1000 / CM1000v2 DOCSIS 3.1 modem alongside the existing Arris SB8200, selectable via modem_model=cm1000. To keep the two vendors from being confused in an "arris"-named file, split modem-specific logic into a src/modems/ package. Each modem module owns its auth flow and status-page parsing behind a small shared interface (USES_LOGIN_TOKEN / login / get_html / parse), while arris_stats.py stays a generic driver (config, poll loop, InfluxDB writer) that dispatches via a registry. - src/modems/common.py: shared HEADERS + http_get helper - src/modems/arris_sb8200.py: token-cookie login + BeautifulSoup table parsing - src/modems/netgear_cm1000.py: HTTP Basic Auth + regex extraction of the pipe-delimited JavaScript tagValueList strings (DOCSIS 3.0 QAM and 3.1 OFDM/OFDMA channels), stripping units into the same stats shape - CM1000 reuses the existing InfluxDB schema and SB8200 Grafana dashboard - Add a unit test for the CM1000 JS parser; document setup in README - Add CLAUDE.md describing architecture, config-consistency invariants, and how to add a new modem Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018bKShZ5y7RFZzqfbkn6WZq
Rework the Netgear module to match a CM1000v2 verified locally: - Auth is a webToken form login, not HTTP Basic Auth: GET /GenieLogin.asp to scrape the (unquoted) webToken + cookies, POST /goform/GenieLogin, then fetch /DocsisStatus.asp with the resulting session. login() now returns an authenticated requests.Session. Success is content-sniffed since the modem serves a 200 + JS redirect stub when unauthenticated. - Fix the status-page URL to DocsisStatus.asp. - Parse the live server-rendered tables by id (dsTable/usTable/d31dsTable/ d31usTable) covering DOCSIS 3.0 and 3.1 OFDM/OFDMA, with the correct column offsets (extra Unerrored-Codewords column downstream; no symbol-rate column upstream). Skip un-provisioned "Not Locked" channels (channel_id 0). - Drop the JavaScript tagValueList parser: on this firmware those strings hold stale placeholder data, so parsing them would emit bogus points. - Rewrite the unit test around a faithful slice of the real DocsisStatus.asp, and update README/CLAUDE.md to describe the form login and table parsing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018bKShZ5y7RFZzqfbkn6WZq
Adds an opt-in capability (poll_event_log, default False) to scrape the Netgear CM1000v2 DOCSIS event log (EventLog.asp) and write it to a separate InfluxDB measurement for Grafana annotations. - netgear_cm1000: get_event_log_html() fetches EventLog.asp on the existing authenticated session; parse_event_log() extracts the events, which live only in the page's JS InitTagValue() pseudo-XML string (<docsDevEventTable>), and splits the level into name + priority. - arris_stats: send_events_to_influx() writes docsis_events points (tags: level/priority/event_id; fields: count/description), timestamped with each event's first-seen time so re-polling overwrites rather than duplicates. The driver calls it only when poll_event_log is set and the modem module exposes parse_event_log, so other modems are unaffected and failures never break the stats loop. - Add poll_event_log to config.ini / Dockerfile / default_config in lockstep; add unit tests for the event-log parser (including a Critical T3 event and the missing-string case); document in README/CLAUDE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018bKShZ5y7RFZzqfbkn6WZq
3 tasks
Allow destination = file to append each poll's data (and event-log entries) to file_path as JSON Lines, so the tool can run without a time-series database. - Factor point-building out of the InfluxDB writers into build_stats_points() / build_event_points(), reused by send_to_file() / send_events_to_file(). - Add file_path config key (default modem_stats.jsonl) to default_config / config.ini / Dockerfile in lockstep; wire destination == 'file' into the loop. - Add a unit test for the file writer; document in README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018bKShZ5y7RFZzqfbkn6WZq
Provide a ready-to-run local monitoring stack and document how to operate it. - docker-compose.yml: InfluxDB 1.8 + Grafana OSS with named volumes, healthcheck, restart: unless-stopped, and an auto-provisioned Grafana datasource. The scraper is included as an optional, commented-out service. - grafana/provisioning/datasources/influxdb.yml: auto-wires Grafana -> InfluxDB. - .env.example: Compose settings (Grafana password, DB name). - deploy/com.arris-stats.plist: launchd agent template to keep the host-side scraper running (RunAtLoad + KeepAlive). - docs/local-stack.md: full spec -- architecture, the Docker-Desktop-on-Mac networking gotcha (why the scraper runs on the host to reach the modem on the home LAN), keep-it-running steps, and carveouts for splitting Grafana or InfluxDB onto another host later. - README: link the spec; .gitignore: local log / jsonl output. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018bKShZ5y7RFZzqfbkn6WZq
- grafana/provisioning/dashboards/cm1000.json: modern (schemaVersion 39) dashboard for Grafana 11-13 with per-channel downstream power, downstream SNR/MER, upstream power, a non_negative_derivative "new uncorrectables" rate panel, and an event-log table + critical-event annotations from docsis_events. - grafana/provisioning/dashboards/dashboards.yml: file provider so it auto-loads. - Pin the provisioned InfluxDB datasource uid to 'influxdb' so the dashboard can reference it deterministically. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018bKShZ5y7RFZzqfbkn6WZq
Two operational fixes for long-running local deployments: - Session expiry is no longer fatal. When a login-token modem (CM1000/SB8200) returns no HTML with an existing credential -- almost always an expired session (the CM1000 serves a redirect stub) -- the driver now re-authenticates in place and retries before falling through to the exit_on_html_error path. Previously a routine timeout exited the process. - Add optional heartbeat_url (default None). After each successful poll the scraper pings the URL; point it at a dead-man's-switch (e.g. Healthchecks.io, or an AWS API Gateway->Lambda->SNS endpoint) to get alerted when the monitor stops running. Uses stdlib urllib; failures are logged and ignored so an unreachable heartbeat never disrupts the loop. Add heartbeat_url to config.ini / Dockerfile / default_config in lockstep; add a heartbeat no-op test; document heartbeat setup in README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018bKShZ5y7RFZzqfbkn6WZq
Add deploy/com.arris-stats.daemon.plist to run the scraper at system boot as a launchd daemon (before/without login), for a dedicated monitor Mac -- an alternative to the login-time LaunchAgent. Runs as a background service and drops to the owning user via UserName. Document the login-vs-boot choice in docs/local-stack.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018bKShZ5y7RFZzqfbkn6WZq
Add deploy/arris-stats.service so the scraper can run as a boot-time systemd service on Linux (Mint/Ubuntu) -- the Linux analog of the macOS launchd files. Runs as the invoking user, restarts on failure, logs to the journal. Document the Linux path in docs/local-stack.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018bKShZ5y7RFZzqfbkn6WZq
…ng' into claude/netgear-eventlog-monitoring
The tracked config is now the git-ignored-real / tracked-template split, so: - test_config_file now reads src/config.ini.example and checks that it lists the same KEYS as default_config (values may differ -- it's a filled-in CM1000 template), instead of asserting exact value equality against the old src/config.ini. - Dockerfile copies config.ini.example -> config.ini at build so `--config config.ini` still works in the image (ENV keeps overriding at runtime; users can -v mount their own config.ini). - CLAUDE.md config-consistency section updated for the template split. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018bKShZ5y7RFZzqfbkn6WZq
Add optional CM1000v2 event-log monitoring
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds monitoring for the Netgear CM1000 / CM1000v2 DOCSIS 3.1 modem alongside the existing Arris SB8200, selectable via
modem_model = cm1000. To avoid confusing the two vendors inside an "arris"-named file, modem-specific logic is split into a newsrc/modems/package, witharris_stats.pyreduced to a generic driver.Changes
Per-modem module structure (
src/modems/)common.py— shared HTTP plumbing (HEADERS,http_get).arris_sb8200.py— the existing token-cookie login + BeautifulSoup table parsing, moved out ofarris_stats.pyunchanged.netgear_cm1000.py— HTTP Basic Auth + regex extraction of the CM1000's pipe-delimited JavaScripttagValueListstrings (the CM1000 does not serve static tables). Parses both DOCSIS 3.0 (QAM) and 3.1 (OFDM/OFDMA) channels and strips units into the same stats shape the SB8200 produces.__init__.py— a small registry (get_modem,SUPPORTED).Each module exposes the same interface —
USES_LOGIN_TOKEN/login/get_html/parse— andarris_stats.pyselects one via the registry and delegates. The generic config loader, poll loop, and InfluxDB writer are unchanged in behavior.Reuse
downstream_statistics/upstream_statisticsmeasurements, so the existing SB8200 Grafana dashboard works without changes.modem_url/modem_auth_required/modem_username/modem_password/modem_modelsettings cover it, soconfig.ini/Dockerfile/default_configare untouched and the config-consistency tests stay green.Docs & tests
CLAUDE.mddocumenting the architecture, the modem-module interface, the config-consistency invariant, and how to add a new modem.http://192.168.100.1/DocsisStatus.htm, Basic Auth,admin/password).Notes / follow-up
DocsisStatus.htmis formatted. The parser targets the common CM1000-family layout and strips units defensively; if a specific firmware returns no stats, run with--debugand capture the modem's page source so field offsets can be tuned.Test plan
bash tests/run_tests.sh— all 4 tests passsrc/, package import from repo root)🤖 Generated with Claude Code
Generated by Claude Code