├── scraper
├── __init__.py
├── targets
│ ├── __init__.py
│ ├── arris_modem_TM1602AP2.py
│ ├── arris_modem_CM820A.py
│ ├── arris_modem.py
│ ├── arris_modem_SB6183.py
│ └── arris_modem_SB6190.py
├── downloaders
│ ├── __init__.py
│ ├── downloader.py
│ ├── local.py
│ └── requests.py
├── outputters
│ ├── __init__.py
│ ├── printer.py
│ ├── outputter.py
│ └── influxdb.py
├── target.py
└── items.py
├── full-service
├── env.grafana
├── env.influxdb
├── grafana_datasources
│ └── grafana_influx.yaml
├── grafana_dashboard_providers
│ └── default.yml
└── grafana_dashboards
│ └── default.json
├── requirements.txt
├── media
└── modem_scrape.png
├── docker-compose.yml
├── Dockerfile
├── tools
├── config_sample.py
└── debugger.py
├── config_sample.py
├── full-service.yml
├── LICENSE
├── .gitignore
├── scrape.py
├── README.md
└── pylintrc
/scraper/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/full-service/env.grafana:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/full-service/env.influxdb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scraper/targets/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scraper/downloaders/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scraper/outputters/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | influxdb
2 | lxml
3 | requests
4 | bs4
5 | html5lib
6 |
--------------------------------------------------------------------------------
/media/modem_scrape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/twstokes/arris-scrape/HEAD/media/modem_scrape.png
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.0"
2 |
3 | services:
4 | modem_scraper:
5 | build: .
6 | restart: always
7 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY requirements.txt ./
6 | RUN pip install --no-cache-dir -r requirements.txt
7 |
8 | COPY . .
9 |
10 | CMD [ "python3", "scrape.py" ]
11 |
--------------------------------------------------------------------------------
/full-service/grafana_datasources/grafana_influx.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: 1
2 |
3 | datasources:
4 | - name: Modem
5 | type: influxdb
6 | access: proxy
7 | database: modem
8 | user: grafana
9 | password: grafana
10 | url: http://influxdb:8086
--------------------------------------------------------------------------------
/full-service/grafana_dashboard_providers/default.yml:
--------------------------------------------------------------------------------
1 | apiVersion: 1
2 |
3 | providers:
4 | - name: 'default'
5 | orgId: 1
6 | folder: ''
7 | type: file
8 | disableDeletion: false
9 | updateIntervalSeconds: 3 #how often Grafana will scan for changed dashboards
10 | options:
11 | path: /var/lib/grafana/dashboards
--------------------------------------------------------------------------------
/scraper/outputters/printer.py:
--------------------------------------------------------------------------------
1 | from .outputter import Outputter
2 |
3 | class PrinterOutputter(Outputter):
4 | """
5 | Prints items to the screen.
6 | """
7 | def reset(self):
8 | pass
9 |
10 | @staticmethod
11 | def output(items):
12 | for item in items:
13 | print(item)
14 |
--------------------------------------------------------------------------------
/tools/config_sample.py:
--------------------------------------------------------------------------------
1 | """
2 | Parameters for the modem scraper debugger.
3 | """
4 | debug_config = {
5 | 'modem_model': '', # POPULATE MODEM MODEL HERE
6 | 'modem_url': '', # POPULATE MODEM URL HERE OR LOCAL FILE PATH
7 | 'is_remote': False # True TO LOAD FROM MODEM OVER THE NETWORK, False TO LOAD FROM A LOCAL URL
8 | }
9 |
--------------------------------------------------------------------------------
/scraper/target.py:
--------------------------------------------------------------------------------
1 | """
2 | Target module.
3 | """
4 | class Target():
5 | """
6 | Subclass this for scraping targets.
7 | """
8 | def extract_items_from_html(self, html_string):
9 | """
10 | Generates Item subclasses by parsing HTML.
11 |
12 | Args:
13 | html_string (string): HTML
14 | """
15 |
--------------------------------------------------------------------------------
/scraper/downloaders/downloader.py:
--------------------------------------------------------------------------------
1 | """
2 | Downloader module.
3 | """
4 | class Downloader():
5 | """
6 | Subclass this for downloading content from URLs.
7 | """
8 | def download(self, url):
9 | """
10 | Given a URL, return the HTML content body as a string, otherwise raise.
11 |
12 | Args:
13 | url (string): URL to download HTML from.
14 | """
15 |
--------------------------------------------------------------------------------
/scraper/downloaders/local.py:
--------------------------------------------------------------------------------
1 | from .downloader import Downloader
2 |
3 | class LocalDownloader(Downloader):
4 | """
5 | Downloader subclass that loads local HTML files.
6 |
7 | Raises:
8 | Exception: Content couldn't be loaded successfully.
9 |
10 | Returns:
11 | string: HTML string of content.
12 | """
13 | @staticmethod
14 | def download(url):
15 | file = open(url, 'r')
16 |
17 | return file.read()
--------------------------------------------------------------------------------
/scraper/outputters/outputter.py:
--------------------------------------------------------------------------------
1 | """
2 | Outputters module.
3 | """
4 |
5 | class Outputter():
6 | """
7 | Subclass this for outputting Items to something.
8 | """
9 | def output(self, items):
10 | """
11 | Output list of items.
12 |
13 | Args:
14 | items ([Item]): List of items to output.
15 | """
16 |
17 | def reset(self):
18 | """
19 | Reset the outputter.
20 | """
21 |
--------------------------------------------------------------------------------
/config_sample.py:
--------------------------------------------------------------------------------
1 | """
2 | Parameters for the modem scraper.
3 | """
4 | scraper_config = {
5 | 'modem_model': '', # POPULATE MODEM MODEL HERE
6 | 'modem_url': '', # POPULATE MODEM URL HERE
7 | 'max_retries': 5,
8 | 'poll_interval_seconds': 30,
9 | 'outputter': 'influxdb' # Output to influxdb or print
10 | }
11 |
12 | """
13 | Parameters for InfluxDBClient. See influxdb.InfluxDBClient for all options.
14 | """
15 | influx_config = {
16 | 'host': '', # POPULATE INFLUX_DB HOST HERE
17 | 'port': 8086,
18 | 'database': 'modem'
19 | }
20 |
--------------------------------------------------------------------------------
/scraper/downloaders/requests.py:
--------------------------------------------------------------------------------
1 | from .downloader import Downloader
2 | import requests
3 |
4 | class RequestsDownloader(Downloader):
5 | """
6 | Downloader subclass that uses Requests to download content.
7 |
8 | Raises:
9 | Exception: Content couldn't be downloaded successfully.
10 |
11 | Returns:
12 | string: HTML string of content.
13 | """
14 | @staticmethod
15 | def download(url):
16 | result = requests.get(url,timeout=10)
17 |
18 | if result.status_code != 200:
19 | raise Exception("Received non-200 response.")
20 |
21 | return result.content
22 |
--------------------------------------------------------------------------------
/full-service.yml:
--------------------------------------------------------------------------------
1 | version: "3.0"
2 |
3 | services:
4 | modem_scraper:
5 | build: .
6 | links:
7 | - influxdb
8 |
9 | influxdb:
10 | image: influxdb:1.8
11 | env_file:
12 | - './full-service/env.influxdb'
13 |
14 | grafana:
15 | image: grafana/grafana:latest
16 | volumes:
17 | - ./full-service/grafana_datasources:/etc/grafana/provisioning/datasources
18 | - ./full-service/grafana_dashboard_providers:/etc/grafana/provisioning/dashboards
19 | - ./full-service/grafana_dashboards:/var/lib/grafana/dashboards
20 | ports:
21 | - "3000:3000"
22 | env_file:
23 | - './full-service/env.grafana'
24 | links:
25 | - influxdb
--------------------------------------------------------------------------------
/scraper/outputters/influxdb.py:
--------------------------------------------------------------------------------
1 | from influxdb import InfluxDBClient
2 | from .outputter import Outputter
3 |
4 | class InfluxDBOutputter(Outputter):
5 | """
6 | InfluxDB Outputter subclass.
7 | """
8 | client = None
9 | config = None
10 |
11 | def __init__(self, config):
12 | self.config = config
13 | self._setup()
14 |
15 | def __del__(self):
16 | if self.client is not None:
17 | self.client.close()
18 |
19 | def _setup(self):
20 | if self.client is not None:
21 | self.client.close()
22 | self.client = InfluxDBClient(**self.config)
23 | self.client.create_database(self.config['database'])
24 |
25 | def reset(self):
26 | self._setup()
27 |
28 | def output(self, items):
29 | points = list(map(lambda i: i.output_for_influxdb(), items))
30 | self.client.write_points(points)
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Tanner Stokes
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/scraper/items.py:
--------------------------------------------------------------------------------
1 | """
2 | Items module.
3 | """
4 | class Item():
5 | """
6 | Subclass this to represent a scraped Item.
7 | """
8 | def __init__(self, fieldDict):
9 | """
10 | Create an item by passing in a dict of fields.
11 |
12 | Args:
13 | fieldDict (dict): Dict to populate properties of the Item.
14 | """
15 | for key, val in fieldDict:
16 | setattr(self, key, val)
17 |
18 | class InfluxableItem(Item):
19 | """
20 | Gives us the ability to print out InfluxableItems into
21 | a readable format.
22 | """
23 | def __str__(self):
24 | return str(self.output_for_influxdb())
25 |
26 | def output_for_influxdb(self):
27 | """
28 | Method that should be implemented to represent the item
29 | as an InfluxDB point.
30 | """
31 |
32 | @staticmethod
33 | def int_at_pos(val, pos = 0):
34 | """
35 | Splits a string v and converts element at position p to an int.
36 | """
37 | return int(str(val).split()[pos])
38 |
39 | @staticmethod
40 | def float_at_pos(val, pos = 0):
41 | """
42 | Splits a string v and converts element at position p to a float.
43 | """
44 | return float(str(val).split()[pos])
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 |
49 | # Translations
50 | *.mo
51 | *.pot
52 |
53 | # Django stuff:
54 | *.log
55 | local_settings.py
56 |
57 | # Flask stuff:
58 | instance/
59 | .webassets-cache
60 |
61 | # Scrapy stuff:
62 | .scrapy
63 |
64 | # Sphinx documentation
65 | docs/_build/
66 |
67 | # PyBuilder
68 | target/
69 |
70 | # Jupyter Notebook
71 | .ipynb_checkpoints
72 |
73 | # pyenv
74 | .python-version
75 |
76 | # celery beat schedule file
77 | celerybeat-schedule
78 |
79 | # SageMath parsed files
80 | *.sage.py
81 |
82 | # dotenv
83 | .env
84 |
85 | # virtualenv
86 | .venv
87 | venv/
88 | ENV/
89 |
90 | # Spyder project settings
91 | .spyderproject
92 | .spyproject
93 |
94 | # Rope project settings
95 | .ropeproject
96 |
97 | # mkdocs documentation
98 | /site
99 |
100 | # mypy
101 | .mypy_cache/
102 |
103 | .DS_Store
104 | config.py
105 | tools/config.py
106 | tools/*.html
107 | .vscode
108 |
--------------------------------------------------------------------------------
/tools/debugger.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import traceback
4 |
5 | # adds the parent to the path
6 | parent_path = os.path.abspath('..')
7 | sys.path.insert(1, parent_path)
8 |
9 | from config import debug_config
10 | from scraper.outputters.printer import PrinterOutputter
11 |
12 | MODEM_URL = debug_config['modem_url']
13 | MODEM_MODEL = debug_config['modem_model']
14 | IS_REMOTE = debug_config['is_remote']
15 |
16 | if IS_REMOTE:
17 | from scraper.downloaders.requests import RequestsDownloader
18 | else:
19 | from scraper.downloaders.local import LocalDownloader
20 |
21 | def run_debugger():
22 | """
23 | Starts the debugger.
24 | """
25 | print("Starting debugger.")
26 |
27 | target = get_target(MODEM_MODEL)
28 | outputter = PrinterOutputter
29 | downloader = get_downloader()
30 |
31 | try:
32 | body = downloader.download(MODEM_URL)
33 | items = target.extract_items_from_html(body)
34 | outputter.output(items)
35 | except BaseException as err:
36 | traceback.print_exception(err)
37 |
38 | def get_target(model):
39 | if model == "SB6183":
40 | from scraper.targets.arris_modem_SB6183 import ArrisModemSB6183
41 | return ArrisModemSB6183()
42 | if model == "TM1602AP2":
43 | from scraper.targets.arris_modem_TM1602AP2 import ArrisModemTM1602AP2
44 | return ArrisModemTM1602AP2()
45 | if model == "CM802A":
46 | from scraper.targets.arris_modem_CM820A import ArrisModemCM820A
47 | return ArrisModemCM820A()
48 | if model == "SB6190":
49 | from scraper.targets.arris_modem_SB6190 import ArrisModemSB6190
50 | return ArrisModemSB6190()
51 |
52 | def get_downloader():
53 | if IS_REMOTE:
54 | return RequestsDownloader()
55 |
56 | return LocalDownloader()
57 |
58 | if __name__ == "__main__":
59 | run_debugger()
60 |
--------------------------------------------------------------------------------
/scrape.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import time
3 |
4 | from config import scraper_config, influx_config
5 | from scraper.outputters.influxdb import InfluxDBOutputter
6 | from scraper.outputters.printer import PrinterOutputter
7 | from scraper.downloaders.requests import RequestsDownloader
8 |
9 | MAX_RETRIES = scraper_config['max_retries']
10 | MODEM_URL = scraper_config['modem_url']
11 | MODEM_MODEL = scraper_config['modem_model']
12 | OUTPUTTER = scraper_config['outputter']
13 |
14 | def run_scraper():
15 | """
16 | Starts the scraper.
17 | """
18 | print("Modem scraper running.")
19 |
20 | target = get_target(MODEM_MODEL)
21 | outputter = get_outputter(OUTPUTTER)
22 | downloader = RequestsDownloader()
23 |
24 | retries = 0
25 | while retries < MAX_RETRIES:
26 | try:
27 | body = downloader.download(MODEM_URL)
28 | items = target.extract_items_from_html(body)
29 | outputter.output(items)
30 |
31 | retries = 0
32 | except KeyboardInterrupt:
33 | sys.exit()
34 | except:
35 | outputter.reset()
36 | retries += 1
37 | print("Error:", sys.exc_info())
38 |
39 | time.sleep(scraper_config['poll_interval_seconds'])
40 |
41 | print("Abort! Max retries reached:", MAX_RETRIES)
42 | sys.exit(1)
43 |
44 | def get_target(model):
45 | if model == "SB6183":
46 | from scraper.targets.arris_modem_SB6183 import ArrisModemSB6183
47 | return ArrisModemSB6183()
48 | if model == "TM1602AP2":
49 | from scraper.targets.arris_modem_TM1602AP2 import ArrisModemTM1602AP2
50 | return ArrisModemTM1602AP2()
51 | if model == "CM802A":
52 | from scraper.targets.arris_modem_CM820A import ArrisModemCM820A
53 | return ArrisModemCM820A()
54 | if model == "SB6190":
55 | from scraper.targets.arris_modem_SB6190 import ArrisModemSB6190
56 | return ArrisModemSB6190()
57 |
58 | def get_outputter(output):
59 | if output == 'influxdb':
60 | return InfluxDBOutputter(influx_config)
61 | else:
62 | return PrinterOutputter()
63 |
64 | if __name__ == "__main__":
65 | run_scraper()
66 |
--------------------------------------------------------------------------------
/scraper/targets/arris_modem_TM1602AP2.py:
--------------------------------------------------------------------------------
1 | """
2 | Arris modem module.
3 | """
4 | from lxml import html
5 |
6 | from .arris_modem import ArrisModem, UpstreamItem, DownstreamItem
7 |
8 | class ArrisModemTM1602AP2(ArrisModem):
9 | """
10 | Target subclass that represents an Arris modem model TM1602AP2
11 | running software 9.1.103J6J.
12 | """
13 |
14 | def get_downstream_items(self,html_string):
15 | """
16 | Function to convert an HTML string to a list of DownstreamItems.
17 |
18 | Args:
19 | html_string (string): HTML
20 |
21 | Returns:
22 | [DownstreamItem]: List of DownstreamItems
23 | """
24 | tree = html.fromstring(html_string)
25 | # grab the downstream table and skip the first row
26 | rows = tree.xpath('/html/body/div[1]/div[3]/table[2]/tbody//tr[position()>1]')
27 |
28 | # key order must match the table column layout
29 | keys = [
30 | 'downstream_id',
31 | 'dcid',
32 | 'freq',
33 | 'power',
34 | 'snr',
35 | 'modulation',
36 | 'octets',
37 | 'correcteds',
38 | 'uncorrectables'
39 | ]
40 |
41 | items = []
42 |
43 | for row in rows:
44 | values = row.xpath('td/text()')
45 | zipped = dict(zip(keys, values))
46 | items.append(DownstreamItem(zipped.items()))
47 |
48 | return items
49 |
50 | def get_upstream_items(self,html_string):
51 | """
52 | Function to convert an HTML string to a list of UpstreamItems.
53 |
54 | Args:
55 | html_string (string): HTML
56 |
57 | Returns:
58 | [UpstreamItem]: List of UpstreamItems
59 | """
60 | tree = html.fromstring(html_string)
61 | # grab the upstream table and skip the first row
62 | rows = tree.xpath('/html/body/div[1]/div[3]/table[4]/tbody//tr[position()>1]')
63 |
64 | # key order must match the table column layout
65 | keys = [
66 | 'upstream_id',
67 | 'ucid',
68 | 'freq',
69 | 'power',
70 | 'channel_type',
71 | 'symbol_rate',
72 | 'modulation'
73 | ]
74 |
75 | items = []
76 |
77 | for row in rows:
78 | values = row.xpath('td/text()')
79 | zipped = dict(zip(keys, values))
80 | items.append(UpstreamItem(zipped.items()))
81 |
82 | return items
83 |
--------------------------------------------------------------------------------
/scraper/targets/arris_modem_CM820A.py:
--------------------------------------------------------------------------------
1 | """
2 | Arris modem module.
3 | """
4 | from lxml import html
5 |
6 | from .arris_modem import ArrisModem, UpstreamItem, DownstreamItem
7 |
8 | class ArrisModemCM820A(ArrisModem):
9 | """
10 | ArrisModem subclass that represents an Arris modem model CM820A
11 | running software 9.1.103S.
12 | """
13 |
14 | def get_downstream_items(self,html_string):
15 | """
16 | Function to convert an HTML string to a list of DownstreamItems.
17 |
18 | Args:
19 | html_string (string): HTML
20 |
21 | Returns:
22 | [DownstreamItem]: List of DownstreamItems
23 | """
24 | tree = html.fromstring(html_string)
25 | # grab the downstream table and skip the first row
26 | rows = tree.xpath('/html/body/div[1]/div[3]/table[2]/tbody//tr[position()>1]')
27 |
28 | # key order must match the table column layout
29 | keys = [
30 | 'downstream_id',
31 | 'dcid',
32 | 'freq',
33 | 'power',
34 | 'snr',
35 | 'modulation',
36 | 'octets',
37 | 'correcteds',
38 | 'uncorrectables'
39 | ]
40 |
41 | items = []
42 |
43 | for row in rows:
44 | values = row.xpath('td/text()')
45 | zipped = dict(zip(keys, values))
46 | items.append(DownstreamItem(zipped.items()))
47 |
48 | return items
49 |
50 | def get_upstream_items(self,html_string):
51 | """
52 | Function to convert an HTML string to a list of UpstreamItems.
53 |
54 | Args:
55 | html_string (string): HTML
56 |
57 | Returns:
58 | [UpstreamItem]: List of UpstreamItems
59 | """
60 | tree = html.fromstring(html_string)
61 | # grab the upstream table and skip the first row
62 | # CM820A upstream tables have a blank record so set start record to 2 or higher
63 | rows = tree.xpath('/html/body/div[1]/div[3]/table[4]/tbody//tr[position()>2]')
64 |
65 | # key order must match the table column layout
66 | keys = [
67 | 'upstream_id',
68 | 'ucid',
69 | 'freq',
70 | 'power',
71 | 'channel_type',
72 | 'symbol_rate',
73 | 'modulation'
74 | ]
75 |
76 | items = []
77 |
78 | for row in rows:
79 | values = row.xpath('td/text()')
80 | zipped = dict(zip(keys, values))
81 | items.append(UpstreamItem(zipped.items()))
82 |
83 | return items
84 |
--------------------------------------------------------------------------------
/scraper/targets/arris_modem.py:
--------------------------------------------------------------------------------
1 | """
2 | Arris modem module.
3 | """
4 | from lxml import html
5 |
6 | from ..target import Target
7 | from ..items import InfluxableItem
8 |
9 | class ArrisModem(Target):
10 | """
11 | Target subclass that represents an Arris modem model
12 |
13 | Args:
14 | Target (string): [HTML]
15 | """
16 |
17 | def extract_items_from_html(self, html_string):
18 | # get items from the downstream table
19 | downstream_items = self.get_downstream_items(html_string)
20 | # get items from the upstream table
21 | upstream_items = self.get_upstream_items(html_string)
22 |
23 | return downstream_items + upstream_items
24 |
25 | def get_downstream_items(self,html_string):
26 | """
27 | Function to convert an HTML string to a list of DownstreamItems.
28 |
29 | Args:
30 | html_string (string): HTML
31 |
32 | Returns:
33 | [DownstreamItem]: List of DownstreamItems
34 | """
35 | # Override in subclass for individual modem model
36 | pass
37 |
38 |
39 | def get_upstream_items(self,html_string):
40 | """
41 | Function to convert an HTML string to a list of UpstreamItems.
42 |
43 | Args:
44 | html_string (string): HTML
45 |
46 | Returns:
47 | [UpstreamItem]: List of UpstreamItems
48 | """
49 | # Override in subclass for individual modem model
50 | pass
51 |
52 | class DownstreamItem(InfluxableItem):
53 | """
54 | InfluxableItem subclass that represents a downstream table row.
55 | """
56 | snr = None
57 | dcid = None
58 | freq = None
59 | power = None
60 | octets = None
61 | correcteds = None
62 | modulation = None
63 | downstream_id = None
64 | uncorrectables = None
65 |
66 | def output_for_influxdb(self):
67 | return {
68 | 'measurement': 'downstream',
69 | 'tags': {
70 | 'downstream_id': self.downstream_id,
71 | 'modulation': self.modulation
72 | },
73 | 'fields': {
74 | 'snr': self.float_at_pos(self.snr),
75 | 'dcid': self.int_at_pos(self.dcid),
76 | 'freq': self.float_at_pos(self.freq),
77 | 'power': self.float_at_pos(self.power),
78 | 'octets': self.int_at_pos(self.octets),
79 | 'correcteds': self.int_at_pos(self.correcteds),
80 | 'uncorrectables': self.int_at_pos(self.uncorrectables)
81 | }
82 | }
83 |
84 | class UpstreamItem(InfluxableItem):
85 | """
86 | InfluxableItem subclass that represents an upstream table row.
87 | """
88 | freq = None
89 | ucid = None
90 | power = None
91 | modulation = None
92 | symbol_rate = None
93 | upstream_id = None
94 | channel_type = None
95 |
96 | def output_for_influxdb(self):
97 | return {
98 | 'measurement': 'upstream',
99 | 'tags': {
100 | 'upstream_id': self.upstream_id,
101 | 'modulation': self.modulation,
102 | 'channel_type': self.channel_type
103 | },
104 | 'fields': {
105 | 'ucid': self.int_at_pos(self.ucid),
106 | 'freq': self.float_at_pos(self.freq),
107 | 'power': self.float_at_pos(self.power),
108 | 'symbol_rate': self.int_at_pos(self.symbol_rate)
109 | }
110 | }
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/scraper/targets/arris_modem_SB6183.py:
--------------------------------------------------------------------------------
1 | """
2 | Arris modem module.
3 | """
4 | from lxml import html
5 |
6 | from bs4 import BeautifulSoup
7 | from .arris_modem import ArrisModem, UpstreamItem, DownstreamItem
8 |
9 | class ArrisModemSB6183(ArrisModem):
10 | """
11 | ArrisModem subclass that represents an Arris modem model SB6183
12 | running software
13 | """
14 |
15 | def get_downstream_items(self,html_string):
16 | """
17 | Function to convert an HTML string to a list of DownstreamItems.
18 |
19 | Args:
20 | html_string (string): HTML
21 |
22 | Returns:
23 | [DownstreamItem]: List of DownstreamItems
24 | """
25 |
26 | # Use BeautifulSoup with html5lib. lxml doesn't play well with the SB6183.
27 | soup = BeautifulSoup(html_string, 'html5lib')
28 |
29 | # key order must match the table column layout
30 | keys = [
31 | 'downstream_id',
32 | 'lock_status',
33 | 'modulation',
34 | 'dcid',
35 | 'freq',
36 | 'power',
37 | 'snr',
38 | 'correcteds',
39 | 'uncorrectables',
40 | 'octets',
41 | ]
42 | items = []
43 |
44 | for table_row in soup.find_all("table")[2].find_all("tr")[2:]:
45 | if table_row.th:
46 | continue
47 | channel = table_row.find_all('td')[0].text.strip()
48 | lock_status = table_row.find_all('td')[1].text.strip()
49 | modulation = table_row.find_all('td')[2].text.strip()
50 | channel_id = table_row.find_all('td')[3].text.strip()
51 | frequency = table_row.find_all('td')[4].text.replace(" Hz", "").strip()
52 | power = table_row.find_all('td')[5].text.replace(" dBmV", "").strip()
53 | snr = table_row.find_all('td')[6].text.replace(" dB", "").strip()
54 | corrected = table_row.find_all('td')[7].text.strip()
55 | uncorrectables = table_row.find_all('td')[8].text.strip()
56 | octets = "0"
57 |
58 | values = ( channel, lock_status, modulation, channel_id, frequency, power, snr, corrected, uncorrectables, octets )
59 | zipped = dict(zip(keys, values))
60 | items.append(DownstreamItem(zipped.items()))
61 |
62 | return items
63 |
64 | def get_upstream_items(self,html_string):
65 | """
66 | Function to convert an HTML string to a list of UpstreamItems.
67 |
68 | Args:
69 | html_string (string): HTML
70 |
71 | Returns:
72 | [UpstreamItem]: List of UpstreamItems
73 | """
74 | # Use BeautifulSoup with html5lib. lxml doesn't play well with the SB6183.
75 | soup = BeautifulSoup(html_string, 'html5lib')
76 |
77 | # key order must match the table column layout
78 | keys = [
79 | 'upstream_id',
80 | 'lock_status',
81 | 'channel_type',
82 | 'ucid',
83 | 'symbol_rate',
84 | 'freq',
85 | 'power',
86 | ]
87 |
88 | items = []
89 |
90 | # upstream table
91 | for table_row in soup.find_all("table")[3].find_all("tr")[2:]:
92 | if table_row.th:
93 | continue
94 | upstream_id = table_row.find_all('td')[0].text.strip()
95 | lock_status = table_row.find_all('td')[1].text.strip()
96 | channel_type = table_row.find_all('td')[2].text.strip()
97 | ucid = table_row.find_all('td')[3].text.strip()
98 | symbol_rate = table_row.find_all('td')[4].text.replace(" Ksym/sec", "").strip()
99 | freq = table_row.find_all('td')[5].text.replace(" Hz", "").strip()
100 | power = table_row.find_all('td')[6].text.replace(" dBmV", "").strip()
101 |
102 | values = (upstream_id, lock_status, channel_type, ucid, symbol_rate, freq, power )
103 | zipped = dict(zip(keys, values))
104 | items.append(UpstreamItem(zipped.items()))
105 |
106 | return items
107 |
--------------------------------------------------------------------------------
/scraper/targets/arris_modem_SB6190.py:
--------------------------------------------------------------------------------
1 | """
2 | Arris modem module.
3 | """
4 | from lxml import html
5 |
6 | from bs4 import BeautifulSoup
7 | from .arris_modem import ArrisModem, UpstreamItem, DownstreamItem
8 |
9 | class ArrisModemSB6190(ArrisModem):
10 | """
11 | ArrisModem subclass that represents an Arris modem model SB6190
12 | running software
13 | """
14 |
15 | def get_downstream_items(self,html_string):
16 | """
17 | Function to convert an HTML string to a list of DownstreamItems.
18 |
19 | Args:
20 | html_string (string): HTML
21 |
22 | Returns:
23 | [DownstreamItem]: List of DownstreamItems
24 | """
25 |
26 | # Use BeautifulSoup with html5lib. lxml doesn't play well with the SB6190.
27 | soup = BeautifulSoup(html_string, 'html5lib')
28 |
29 | # key order must match the table column layout
30 | keys = [
31 | 'downstream_id',
32 | 'lock_status',
33 | 'modulation',
34 | 'dcid',
35 | 'freq',
36 | 'power',
37 | 'snr',
38 | 'correcteds',
39 | 'uncorrectables',
40 | 'octets',
41 | ]
42 | items = []
43 |
44 | for table_row in soup.find_all("table")[2].find_all("tr")[2:]:
45 | if table_row.th:
46 | continue
47 | channel = table_row.find_all('td')[0].text.strip()
48 | lock_status = table_row.find_all('td')[1].text.strip()
49 | modulation = table_row.find_all('td')[2].text.strip()
50 | channel_id = table_row.find_all('td')[3].text.strip()
51 | frequency = table_row.find_all('td')[4].text.replace(" Hz", "").strip()
52 | power = table_row.find_all('td')[5].text.replace(" dBmV", "").strip()
53 | snr = table_row.find_all('td')[6].text.replace(" dB", "").strip()
54 | corrected = table_row.find_all('td')[7].text.strip()
55 | uncorrectables = table_row.find_all('td')[8].text.strip()
56 | octets = "0"
57 |
58 | values = ( channel, lock_status, modulation, channel_id, frequency, power, snr, corrected, uncorrectables, octets)
59 | zipped = dict(zip(keys, values))
60 | items.append(DownstreamItem(zipped.items()))
61 |
62 | return items
63 |
64 | def get_upstream_items(self,html_string):
65 | """
66 | Function to convert an HTML string to a list of UpstreamItems.
67 |
68 | Args:
69 | html_string (string): HTML
70 |
71 | Returns:
72 | [UpstreamItem]: List of UpstreamItems
73 | """
74 | # Use BeautifulSoup with html5lib. lxml doesn't play well with the SB6190.
75 | soup = BeautifulSoup(html_string, 'html5lib')
76 |
77 | # key order must match the table column layout
78 | keys = [
79 | 'upstream_id',
80 | 'lock_status',
81 | 'channel_type',
82 | 'ucid',
83 | 'symbol_rate',
84 | 'freq',
85 | 'power',
86 | ]
87 |
88 | items = []
89 |
90 | # upstream table
91 | for table_row in soup.find_all("table")[3].find_all("tr")[2:]:
92 | if table_row.th:
93 | continue
94 | upstream_id = table_row.find_all('td')[0].text.strip()
95 | lock_status = table_row.find_all('td')[1].text.strip()
96 | channel_type = table_row.find_all('td')[2].text.strip()
97 | ucid = table_row.find_all('td')[3].text.strip()
98 | symbol_rate = table_row.find_all('td')[4].text.replace(" kSym/sec", "").strip()
99 | freq = table_row.find_all('td')[5].text.replace(" Hz", "").strip()
100 | power = table_row.find_all('td')[6].text.replace(" dBmV", "").strip()
101 |
102 | values = (upstream_id, lock_status, channel_type, ucid, symbol_rate, freq, power )
103 | zipped = dict(zip(keys, values))
104 | items.append(UpstreamItem(zipped.items()))
105 |
106 | return items
107 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Arris Scrape
2 |
3 | 
4 |
5 | ## Overview
6 |
7 | A Python script that scrapes an Arris modem status page to insert signal levels into InfluxDB and ultimately Grafana.
8 |
9 | The scraper is flexible in that you can subclass pieces to scrape other modems and upload to other databases. With a few tweaks it could be an all-purpose HTML -> Grafana scraper for something entirely unrelated to cable modems!
10 |
11 | A preset Grafana dashboard is included with this project with some alerts that monitor the modem's signal levels. The level thresholds were created from the data on [this website](https://pickmymodem.com/signal-levels-docsis-3-03-1-cable-modem/).
12 |
13 |
14 | Expand for list of fields and tags
15 | - Fields:
16 | - Downstream
17 | - SNR
18 | - DCID
19 | - Frequency
20 | - Power
21 | - Octets
22 | - Correcteds
23 | - Uncorrectables
24 | - Upstream
25 | - UCID
26 | - Frequency
27 | - Power
28 | - Symbol Rate
29 | - Tags:
30 | - Downstream ID
31 | - Modulation (downstream / upstream)
32 | - Upstream ID
33 | - Channel Type (upstream)
34 |
35 |
36 | ## Getting Started
37 |
38 | 1. You must know your modem's IP address. Typically these status pages are accessible without authentication from your LAN by going to a URL such as http://192.168.100.1.
39 | 2. Hopefully your status page matches the screenshot above. If not, see the section below entitled [Extending](#extending).
40 |
41 | ### Option A: I already have InfluxDB and Grafana and I want to scrape my modem.
42 |
43 | #### First
44 |
45 | Copy `config_sample.py` to `config.py` and fill in your modem's URL and model as well as your InfluxDB hostname. Depending on your InfluxDB configuration, you may need to add more options (e.g. authentication). See the `influxdb.InfluxDBClient` Python module for all possibilities.
46 |
47 |
48 | Python only
49 |
50 | 1. Follow the instructions above to set up the config file.
51 | 2. `pip install -r requirements.txt`
52 | 3. `python scrape.py`
53 |
54 |
55 |
56 | Docker
57 |
58 | 1. Follow the instructions above to set up the config file.
59 | 2. Install Docker
60 | 3. Run `docker-compose build`
61 | 4. Run `docker-compose up`
62 |
63 |
64 | #### Last
65 |
66 | Import the included [Grafana JSON](full-service/grafana_dashboards) and tweak appropriately.
67 |
68 | ### Option B: I don't run InfluxDB or Grafana but I want to scrape my modem, or I want to test drive this project.
69 |
70 | 1. Install Docker
71 | 2. Copy `config_sample.py` to `config.py` and configure the modem URL. Set the host for the InfluxDB section to `influxdb` (`'host': 'influxdb'`).
72 | 3. Run `docker-compose -f full-service.yml build`
73 | 4. Run `docker-compose -f full-service.yml up`
74 | 5. Login to Grafana at http://localhost:3000 with the credentials `admin` / `admin`
75 | 6. Open the Modem dashboard
76 |
77 | ## Extending
78 |
79 | There's a good chance your modem status page doesn't match this one, but you want to accomplish the same task. If your modem allows you to see a status page without any authentication but it just has a different layout, then adapting is straightforward. Just create a new subclass of `arris_modem.py` target and change the fields and xpath queries as needed. Use the `PrinterOutputter` to see what data points would be uploaded to InfluxDB before doing any real uploading.
80 |
81 | If your modem requires some form of authentication, you'll need to implement that part. It may be as simple as snooping on HTTP headers using your browser to see what the script needs to send.
82 |
83 | ## Debugger
84 |
85 | A simple debugging tool exists in `tools` that will process and output either a remote or local HTML status page.
86 |
87 | 1. Run `pip install -r requirements.txt` in the root of the repo (or install modules as needed)
88 | 2. Copy `config_sample.py` to `config.py` (in its directory) and populate the values
89 | 3. Run `python3 debugger.py`
90 |
91 | ### Debugging using a local file
92 |
93 | Save the modem's status page as an HTML file locally and point `config.py` to its path, then set `is_remote` to `True`. Relative paths are supported, so if dropping this file in the `tools` directory just supply the filename.
94 |
--------------------------------------------------------------------------------
/pylintrc:
--------------------------------------------------------------------------------
1 | [MASTER]
2 |
3 | # A comma-separated list of package or module names from where C extensions may
4 | # be loaded. Extensions are loading into the active Python interpreter and may
5 | # run arbitrary code.
6 | extension-pkg-whitelist=
7 |
8 | # Specify a score threshold to be exceeded before program exits with error.
9 | fail-under=10.0
10 |
11 | # Add files or directories to the blacklist. They should be base names, not
12 | # paths.
13 | ignore=CVS
14 |
15 | # Add files or directories matching the regex patterns to the blacklist. The
16 | # regex matches against base names, not paths.
17 | ignore-patterns=
18 |
19 | # Python code to execute, usually for sys.path manipulation such as
20 | # pygtk.require().
21 | #init-hook=
22 |
23 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
24 | # number of processors available to use.
25 | jobs=1
26 |
27 | # Control the amount of potential inferred values when inferring a single
28 | # object. This can help the performance when dealing with large functions or
29 | # complex, nested conditions.
30 | limit-inference-results=100
31 |
32 | # List of plugins (as comma separated values of python module names) to load,
33 | # usually to register additional checkers.
34 | load-plugins=
35 |
36 | # Pickle collected data for later comparisons.
37 | persistent=yes
38 |
39 | # When enabled, pylint would attempt to guess common misconfiguration and emit
40 | # user-friendly hints instead of false-positive error messages.
41 | suggestion-mode=yes
42 |
43 | # Allow loading of arbitrary C extensions. Extensions are imported into the
44 | # active Python interpreter and may run arbitrary code.
45 | unsafe-load-any-extension=no
46 |
47 |
48 | [MESSAGES CONTROL]
49 |
50 | # Only show warnings with the listed confidence levels. Leave empty to show
51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
52 | confidence=
53 |
54 | # Disable the message, report, category or checker with the given id(s). You
55 | # can either give multiple identifiers separated by comma (,) or put this
56 | # option multiple times (only on the command line, not in the configuration
57 | # file where it should appear only once). You can also use "--disable=all" to
58 | # disable everything first and then reenable specific checks. For example, if
59 | # you want to run only the similarities checker, you can use "--disable=all
60 | # --enable=similarities". If you want to run only the classes checker, but have
61 | # no Warning level messages displayed, use "--disable=all --enable=classes
62 | # --disable=W".
63 | disable=print-statement,
64 | parameter-unpacking,
65 | unpacking-in-except,
66 | old-raise-syntax,
67 | backtick,
68 | long-suffix,
69 | old-ne-operator,
70 | old-octal-literal,
71 | import-star-module-level,
72 | non-ascii-bytes-literal,
73 | raw-checker-failed,
74 | bad-inline-option,
75 | locally-disabled,
76 | file-ignored,
77 | suppressed-message,
78 | useless-suppression,
79 | deprecated-pragma,
80 | use-symbolic-message-instead,
81 | apply-builtin,
82 | basestring-builtin,
83 | buffer-builtin,
84 | cmp-builtin,
85 | coerce-builtin,
86 | execfile-builtin,
87 | file-builtin,
88 | long-builtin,
89 | raw_input-builtin,
90 | reduce-builtin,
91 | standarderror-builtin,
92 | unicode-builtin,
93 | xrange-builtin,
94 | coerce-method,
95 | delslice-method,
96 | getslice-method,
97 | setslice-method,
98 | no-absolute-import,
99 | old-division,
100 | dict-iter-method,
101 | dict-view-method,
102 | next-method-called,
103 | metaclass-assignment,
104 | indexing-exception,
105 | raising-string,
106 | reload-builtin,
107 | oct-method,
108 | hex-method,
109 | nonzero-method,
110 | cmp-method,
111 | input-builtin,
112 | round-builtin,
113 | intern-builtin,
114 | unichr-builtin,
115 | map-builtin-not-iterating,
116 | zip-builtin-not-iterating,
117 | range-builtin-not-iterating,
118 | filter-builtin-not-iterating,
119 | using-cmp-argument,
120 | eq-without-hash,
121 | div-method,
122 | idiv-method,
123 | rdiv-method,
124 | exception-message-attribute,
125 | invalid-str-codec,
126 | sys-max-int,
127 | bad-python3-import,
128 | deprecated-string-function,
129 | deprecated-str-translate-call,
130 | deprecated-itertools-function,
131 | deprecated-types-field,
132 | next-method-defined,
133 | dict-items-not-iterating,
134 | dict-keys-not-iterating,
135 | dict-values-not-iterating,
136 | deprecated-operator-function,
137 | deprecated-urllib-function,
138 | xreadlines-attribute,
139 | deprecated-sys-function,
140 | exception-escape,
141 | comprehension-escape
142 |
143 | # Enable the message, report, category or checker with the given id(s). You can
144 | # either give multiple identifier separated by comma (,) or put this option
145 | # multiple time (only on the command line, not in the configuration file where
146 | # it should appear only once). See also the "--disable" option for examples.
147 | enable=c-extension-no-member
148 |
149 |
150 | [REPORTS]
151 |
152 | # Python expression which should return a score less than or equal to 10. You
153 | # have access to the variables 'error', 'warning', 'refactor', and 'convention'
154 | # which contain the number of messages in each category, as well as 'statement'
155 | # which is the total number of statements analyzed. This score is used by the
156 | # global evaluation report (RP0004).
157 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
158 |
159 | # Template used to display messages. This is a python new-style format string
160 | # used to format the message information. See doc for all details.
161 | #msg-template=
162 |
163 | # Set the output format. Available formats are text, parseable, colorized, json
164 | # and msvs (visual studio). You can also give a reporter class, e.g.
165 | # mypackage.mymodule.MyReporterClass.
166 | output-format=text
167 |
168 | # Tells whether to display a full report or only the messages.
169 | reports=no
170 |
171 | # Activate the evaluation score.
172 | score=yes
173 |
174 |
175 | [REFACTORING]
176 |
177 | # Maximum number of nested blocks for function / method body
178 | max-nested-blocks=5
179 |
180 | # Complete name of functions that never returns. When checking for
181 | # inconsistent-return-statements if a never returning function is called then
182 | # it will be considered as an explicit return statement and no message will be
183 | # printed.
184 | never-returning-functions=sys.exit
185 |
186 |
187 | [LOGGING]
188 |
189 | # The type of string formatting that logging methods do. `old` means using %
190 | # formatting, `new` is for `{}` formatting.
191 | logging-format-style=old
192 |
193 | # Logging modules to check that the string format arguments are in logging
194 | # function parameter format.
195 | logging-modules=logging
196 |
197 |
198 | [SPELLING]
199 |
200 | # Limits count of emitted suggestions for spelling mistakes.
201 | max-spelling-suggestions=4
202 |
203 | # Spelling dictionary name. Available dictionaries: none. To make it work,
204 | # install the python-enchant package.
205 | spelling-dict=
206 |
207 | # List of comma separated words that should not be checked.
208 | spelling-ignore-words=
209 |
210 | # A path to a file that contains the private dictionary; one word per line.
211 | spelling-private-dict-file=
212 |
213 | # Tells whether to store unknown words to the private dictionary (see the
214 | # --spelling-private-dict-file option) instead of raising a message.
215 | spelling-store-unknown-words=no
216 |
217 |
218 | [MISCELLANEOUS]
219 |
220 | # List of note tags to take in consideration, separated by a comma.
221 | notes=FIXME,
222 | XXX,
223 | TODO
224 |
225 | # Regular expression of note tags to take in consideration.
226 | #notes-rgx=
227 |
228 |
229 | [TYPECHECK]
230 |
231 | # List of decorators that produce context managers, such as
232 | # contextlib.contextmanager. Add to this list to register other decorators that
233 | # produce valid context managers.
234 | contextmanager-decorators=contextlib.contextmanager
235 |
236 | # List of members which are set dynamically and missed by pylint inference
237 | # system, and so shouldn't trigger E1101 when accessed. Python regular
238 | # expressions are accepted.
239 | generated-members=
240 |
241 | # Tells whether missing members accessed in mixin class should be ignored. A
242 | # mixin class is detected if its name ends with "mixin" (case insensitive).
243 | ignore-mixin-members=yes
244 |
245 | # Tells whether to warn about missing members when the owner of the attribute
246 | # is inferred to be None.
247 | ignore-none=yes
248 |
249 | # This flag controls whether pylint should warn about no-member and similar
250 | # checks whenever an opaque object is returned when inferring. The inference
251 | # can return multiple potential results while evaluating a Python object, but
252 | # some branches might not be evaluated, which results in partial inference. In
253 | # that case, it might be useful to still emit no-member and other checks for
254 | # the rest of the inferred objects.
255 | ignore-on-opaque-inference=yes
256 |
257 | # List of class names for which member attributes should not be checked (useful
258 | # for classes with dynamically set attributes). This supports the use of
259 | # qualified names.
260 | ignored-classes=optparse.Values,thread._local,_thread._local
261 |
262 | # List of module names for which member attributes should not be checked
263 | # (useful for modules/projects where namespaces are manipulated during runtime
264 | # and thus existing member attributes cannot be deduced by static analysis). It
265 | # supports qualified module names, as well as Unix pattern matching.
266 | ignored-modules=
267 |
268 | # Show a hint with possible names when a member name was not found. The aspect
269 | # of finding the hint is based on edit distance.
270 | missing-member-hint=yes
271 |
272 | # The minimum edit distance a name should have in order to be considered a
273 | # similar match for a missing member name.
274 | missing-member-hint-distance=1
275 |
276 | # The total number of similar names that should be taken in consideration when
277 | # showing a hint for a missing member.
278 | missing-member-max-choices=1
279 |
280 | # List of decorators that change the signature of a decorated function.
281 | signature-mutators=
282 |
283 |
284 | [VARIABLES]
285 |
286 | # List of additional names supposed to be defined in builtins. Remember that
287 | # you should avoid defining new builtins when possible.
288 | additional-builtins=
289 |
290 | # Tells whether unused global variables should be treated as a violation.
291 | allow-global-unused-variables=yes
292 |
293 | # List of strings which can identify a callback function by name. A callback
294 | # name must start or end with one of those strings.
295 | callbacks=cb_,
296 | _cb
297 |
298 | # A regular expression matching the name of dummy variables (i.e. expected to
299 | # not be used).
300 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
301 |
302 | # Argument names that match this expression will be ignored. Default to name
303 | # with leading underscore.
304 | ignored-argument-names=_.*|^ignored_|^unused_
305 |
306 | # Tells whether we should check for unused import in __init__ files.
307 | init-import=no
308 |
309 | # List of qualified module names which can have objects that can redefine
310 | # builtins.
311 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
312 |
313 |
314 | [FORMAT]
315 |
316 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
317 | expected-line-ending-format=
318 |
319 | # Regexp for a line that is allowed to be longer than the limit.
320 | ignore-long-lines=^\s*(# )??$
321 |
322 | # Number of spaces of indent required inside a hanging or continued line.
323 | indent-after-paren=4
324 |
325 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
326 | # tab).
327 | indent-string=' '
328 |
329 | # Maximum number of characters on a single line.
330 | max-line-length=100
331 |
332 | # Maximum number of lines in a module.
333 | max-module-lines=1000
334 |
335 | # Allow the body of a class to be on the same line as the declaration if body
336 | # contains single statement.
337 | single-line-class-stmt=no
338 |
339 | # Allow the body of an if to be on the same line as the test if there is no
340 | # else.
341 | single-line-if-stmt=no
342 |
343 |
344 | [SIMILARITIES]
345 |
346 | # Ignore comments when computing similarities.
347 | ignore-comments=yes
348 |
349 | # Ignore docstrings when computing similarities.
350 | ignore-docstrings=yes
351 |
352 | # Ignore imports when computing similarities.
353 | ignore-imports=no
354 |
355 | # Minimum lines number of a similarity.
356 | min-similarity-lines=4
357 |
358 |
359 | [BASIC]
360 |
361 | # Naming style matching correct argument names.
362 | argument-naming-style=snake_case
363 |
364 | # Regular expression matching correct argument names. Overrides argument-
365 | # naming-style.
366 | #argument-rgx=
367 |
368 | # Naming style matching correct attribute names.
369 | attr-naming-style=snake_case
370 |
371 | # Regular expression matching correct attribute names. Overrides attr-naming-
372 | # style.
373 | #attr-rgx=
374 |
375 | # Bad variable names which should always be refused, separated by a comma.
376 | bad-names=foo,
377 | bar,
378 | baz,
379 | toto,
380 | tutu,
381 | tata
382 |
383 | # Bad variable names regexes, separated by a comma. If names match any regex,
384 | # they will always be refused
385 | bad-names-rgxs=
386 |
387 | # Naming style matching correct class attribute names.
388 | class-attribute-naming-style=any
389 |
390 | # Regular expression matching correct class attribute names. Overrides class-
391 | # attribute-naming-style.
392 | #class-attribute-rgx=
393 |
394 | # Naming style matching correct class names.
395 | class-naming-style=PascalCase
396 |
397 | # Regular expression matching correct class names. Overrides class-naming-
398 | # style.
399 | #class-rgx=
400 |
401 | # Naming style matching correct constant names.
402 | const-naming-style=UPPER_CASE
403 |
404 | # Regular expression matching correct constant names. Overrides const-naming-
405 | # style.
406 | #const-rgx=
407 |
408 | # Minimum line length for functions/classes that require docstrings, shorter
409 | # ones are exempt.
410 | docstring-min-length=-1
411 |
412 | # Naming style matching correct function names.
413 | function-naming-style=snake_case
414 |
415 | # Regular expression matching correct function names. Overrides function-
416 | # naming-style.
417 | #function-rgx=
418 |
419 | # Good variable names which should always be accepted, separated by a comma.
420 | good-names=i,
421 | j,
422 | k,
423 | ex,
424 | Run,
425 | _
426 |
427 | # Good variable names regexes, separated by a comma. If names match any regex,
428 | # they will always be accepted
429 | good-names-rgxs=
430 |
431 | # Include a hint for the correct naming format with invalid-name.
432 | include-naming-hint=no
433 |
434 | # Naming style matching correct inline iteration names.
435 | inlinevar-naming-style=any
436 |
437 | # Regular expression matching correct inline iteration names. Overrides
438 | # inlinevar-naming-style.
439 | #inlinevar-rgx=
440 |
441 | # Naming style matching correct method names.
442 | method-naming-style=snake_case
443 |
444 | # Regular expression matching correct method names. Overrides method-naming-
445 | # style.
446 | #method-rgx=
447 |
448 | # Naming style matching correct module names.
449 | module-naming-style=snake_case
450 |
451 | # Regular expression matching correct module names. Overrides module-naming-
452 | # style.
453 | #module-rgx=
454 |
455 | # Colon-delimited sets of names that determine each other's naming style when
456 | # the name regexes allow several styles.
457 | name-group=
458 |
459 | # Regular expression which should only match function or class names that do
460 | # not require a docstring.
461 | no-docstring-rgx=^_
462 |
463 | # List of decorators that produce properties, such as abc.abstractproperty. Add
464 | # to this list to register other decorators that produce valid properties.
465 | # These decorators are taken in consideration only for invalid-name.
466 | property-classes=abc.abstractproperty
467 |
468 | # Naming style matching correct variable names.
469 | variable-naming-style=snake_case
470 |
471 | # Regular expression matching correct variable names. Overrides variable-
472 | # naming-style.
473 | #variable-rgx=
474 |
475 |
476 | [STRING]
477 |
478 | # This flag controls whether inconsistent-quotes generates a warning when the
479 | # character used as a quote delimiter is used inconsistently within a module.
480 | check-quote-consistency=no
481 |
482 | # This flag controls whether the implicit-str-concat should generate a warning
483 | # on implicit string concatenation in sequences defined over several lines.
484 | check-str-concat-over-line-jumps=no
485 |
486 |
487 | [IMPORTS]
488 |
489 | # List of modules that can be imported at any level, not just the top level
490 | # one.
491 | allow-any-import-level=
492 |
493 | # Allow wildcard imports from modules that define __all__.
494 | allow-wildcard-with-all=no
495 |
496 | # Analyse import fallback blocks. This can be used to support both Python 2 and
497 | # 3 compatible code, which means that the block might have code that exists
498 | # only in one or another interpreter, leading to false positives when analysed.
499 | analyse-fallback-blocks=no
500 |
501 | # Deprecated modules which should not be used, separated by a comma.
502 | deprecated-modules=optparse,tkinter.tix
503 |
504 | # Create a graph of external dependencies in the given file (report RP0402 must
505 | # not be disabled).
506 | ext-import-graph=
507 |
508 | # Create a graph of every (i.e. internal and external) dependencies in the
509 | # given file (report RP0402 must not be disabled).
510 | import-graph=
511 |
512 | # Create a graph of internal dependencies in the given file (report RP0402 must
513 | # not be disabled).
514 | int-import-graph=
515 |
516 | # Force import order to recognize a module as part of the standard
517 | # compatibility libraries.
518 | known-standard-library=
519 |
520 | # Force import order to recognize a module as part of a third party library.
521 | known-third-party=enchant
522 |
523 | # Couples of modules and preferred modules, separated by a comma.
524 | preferred-modules=
525 |
526 |
527 | [CLASSES]
528 |
529 | # Warn about protected attribute access inside special methods
530 | check-protected-access-in-special-methods=no
531 |
532 | # List of method names used to declare (i.e. assign) instance attributes.
533 | defining-attr-methods=__init__,
534 | __new__,
535 | setUp,
536 | __post_init__
537 |
538 | # List of member names, which should be excluded from the protected access
539 | # warning.
540 | exclude-protected=_asdict,
541 | _fields,
542 | _replace,
543 | _source,
544 | _make
545 |
546 | # List of valid names for the first argument in a class method.
547 | valid-classmethod-first-arg=cls
548 |
549 | # List of valid names for the first argument in a metaclass class method.
550 | valid-metaclass-classmethod-first-arg=cls
551 |
552 |
553 | [DESIGN]
554 |
555 | # Maximum number of arguments for function / method.
556 | max-args=5
557 |
558 | # Maximum number of attributes for a class (see R0902).
559 | max-attributes=7
560 |
561 | # Maximum number of boolean expressions in an if statement (see R0916).
562 | max-bool-expr=5
563 |
564 | # Maximum number of branch for function / method body.
565 | max-branches=12
566 |
567 | # Maximum number of locals for function / method body.
568 | max-locals=15
569 |
570 | # Maximum number of parents for a class (see R0901).
571 | max-parents=7
572 |
573 | # Maximum number of public methods for a class (see R0904).
574 | max-public-methods=20
575 |
576 | # Maximum number of return / yield for function / method body.
577 | max-returns=6
578 |
579 | # Maximum number of statements in function / method body.
580 | max-statements=50
581 |
582 | # Minimum number of public methods for a class (see R0903).
583 | min-public-methods=0
584 |
585 |
586 | [EXCEPTIONS]
587 |
588 | # Exceptions that will emit a warning when being caught. Defaults to
589 | # "BaseException, Exception".
590 | overgeneral-exceptions=BaseException,
591 | Exception
592 |
--------------------------------------------------------------------------------
/full-service/grafana_dashboards/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "annotations": {
3 | "list": [
4 | {
5 | "builtIn": 1,
6 | "datasource": "-- Grafana --",
7 | "enable": true,
8 | "hide": true,
9 | "iconColor": "rgba(0, 211, 255, 1)",
10 | "name": "Annotations & Alerts",
11 | "type": "dashboard"
12 | }
13 | ]
14 | },
15 | "editable": true,
16 | "gnetId": null,
17 | "graphTooltip": 0,
18 | "id": 7,
19 | "links": [],
20 | "panels": [
21 | {
22 | "collapsed": false,
23 | "datasource": null,
24 | "gridPos": {
25 | "h": 1,
26 | "w": 24,
27 | "x": 0,
28 | "y": 0
29 | },
30 | "id": 63,
31 | "panels": [],
32 | "title": "Arris Modem",
33 | "type": "row"
34 | },
35 | {
36 | "alert": {
37 | "alertRuleTags": {},
38 | "conditions": [
39 | {
40 | "evaluator": {
41 | "params": [
42 | -7,
43 | 7
44 | ],
45 | "type": "outside_range"
46 | },
47 | "operator": {
48 | "type": "and"
49 | },
50 | "query": {
51 | "params": [
52 | "A",
53 | "5m",
54 | "now"
55 | ]
56 | },
57 | "reducer": {
58 | "params": [],
59 | "type": "avg"
60 | },
61 | "type": "query"
62 | }
63 | ],
64 | "executionErrorState": "alerting",
65 | "for": "5m",
66 | "frequency": "1m",
67 | "handler": 1,
68 | "name": "Downstream Power alert",
69 | "noDataState": "no_data",
70 | "notifications": []
71 | },
72 | "aliasColors": {},
73 | "bars": false,
74 | "dashLength": 10,
75 | "dashes": false,
76 | "datasource": "Modem",
77 | "description": "",
78 | "fieldConfig": {
79 | "defaults": {
80 | "custom": {},
81 | "unit": "dBmV"
82 | },
83 | "overrides": []
84 | },
85 | "fill": 0,
86 | "fillGradient": 0,
87 | "gridPos": {
88 | "h": 8,
89 | "w": 12,
90 | "x": 0,
91 | "y": 1
92 | },
93 | "hiddenSeries": false,
94 | "id": 61,
95 | "legend": {
96 | "avg": false,
97 | "current": false,
98 | "max": false,
99 | "min": false,
100 | "show": true,
101 | "total": false,
102 | "values": false
103 | },
104 | "lines": true,
105 | "linewidth": 1,
106 | "nullPointMode": "null",
107 | "options": {
108 | "alertThreshold": true
109 | },
110 | "percentage": false,
111 | "pluginVersion": "7.3.4",
112 | "pointradius": 2,
113 | "points": false,
114 | "renderer": "flot",
115 | "seriesOverrides": [],
116 | "spaceLength": 10,
117 | "stack": false,
118 | "steppedLine": false,
119 | "targets": [
120 | {
121 | "alias": "$tag_downstream_id",
122 | "groupBy": [
123 | {
124 | "params": [
125 | "$__interval"
126 | ],
127 | "type": "time"
128 | },
129 | {
130 | "params": [
131 | "downstream_id"
132 | ],
133 | "type": "tag"
134 | },
135 | {
136 | "params": [
137 | "none"
138 | ],
139 | "type": "fill"
140 | }
141 | ],
142 | "measurement": "downstream",
143 | "orderByTime": "ASC",
144 | "policy": "default",
145 | "query": "SELECT mean(\"value\") FROM \"measurement\" WHERE $timeFilter GROUP BY time($__interval) fill(null)",
146 | "rawQuery": false,
147 | "refId": "A",
148 | "resultFormat": "time_series",
149 | "select": [
150 | [
151 | {
152 | "params": [
153 | "power"
154 | ],
155 | "type": "field"
156 | },
157 | {
158 | "params": [],
159 | "type": "mean"
160 | }
161 | ]
162 | ],
163 | "tags": []
164 | }
165 | ],
166 | "thresholds": [
167 | {
168 | "colorMode": "critical",
169 | "fill": true,
170 | "line": true,
171 | "op": "lt",
172 | "value": -7
173 | },
174 | {
175 | "colorMode": "critical",
176 | "fill": true,
177 | "line": true,
178 | "op": "gt",
179 | "value": 7
180 | }
181 | ],
182 | "timeFrom": null,
183 | "timeRegions": [],
184 | "timeShift": null,
185 | "title": "Downstream Power",
186 | "tooltip": {
187 | "shared": true,
188 | "sort": 0,
189 | "value_type": "individual"
190 | },
191 | "type": "graph",
192 | "xaxis": {
193 | "buckets": null,
194 | "mode": "time",
195 | "name": null,
196 | "show": true,
197 | "values": []
198 | },
199 | "yaxes": [
200 | {
201 | "decimals": 2,
202 | "format": "dBmV",
203 | "label": "",
204 | "logBase": 1,
205 | "max": "15",
206 | "min": "-15",
207 | "show": true
208 | },
209 | {
210 | "format": "short",
211 | "label": null,
212 | "logBase": 1,
213 | "max": null,
214 | "min": null,
215 | "show": true
216 | }
217 | ],
218 | "yaxis": {
219 | "align": false,
220 | "alignLevel": null
221 | }
222 | },
223 | {
224 | "alert": {
225 | "alertRuleTags": {},
226 | "conditions": [
227 | {
228 | "evaluator": {
229 | "params": [
230 | 35,
231 | 49
232 | ],
233 | "type": "outside_range"
234 | },
235 | "operator": {
236 | "type": "and"
237 | },
238 | "query": {
239 | "params": [
240 | "A",
241 | "5m",
242 | "now"
243 | ]
244 | },
245 | "reducer": {
246 | "params": [],
247 | "type": "avg"
248 | },
249 | "type": "query"
250 | }
251 | ],
252 | "executionErrorState": "alerting",
253 | "for": "5m",
254 | "frequency": "1m",
255 | "handler": 1,
256 | "name": "Upstream Power alert",
257 | "noDataState": "no_data",
258 | "notifications": []
259 | },
260 | "aliasColors": {},
261 | "bars": false,
262 | "dashLength": 10,
263 | "dashes": false,
264 | "datasource": "Modem",
265 | "description": "",
266 | "fieldConfig": {
267 | "defaults": {
268 | "custom": {},
269 | "unit": "dBmV"
270 | },
271 | "overrides": []
272 | },
273 | "fill": 0,
274 | "fillGradient": 0,
275 | "gridPos": {
276 | "h": 8,
277 | "w": 12,
278 | "x": 12,
279 | "y": 1
280 | },
281 | "hiddenSeries": false,
282 | "id": 66,
283 | "legend": {
284 | "avg": false,
285 | "current": false,
286 | "max": false,
287 | "min": false,
288 | "show": true,
289 | "total": false,
290 | "values": false
291 | },
292 | "lines": true,
293 | "linewidth": 1,
294 | "nullPointMode": "null",
295 | "options": {
296 | "alertThreshold": true
297 | },
298 | "percentage": false,
299 | "pluginVersion": "7.3.4",
300 | "pointradius": 2,
301 | "points": false,
302 | "renderer": "flot",
303 | "seriesOverrides": [],
304 | "spaceLength": 10,
305 | "stack": false,
306 | "steppedLine": false,
307 | "targets": [
308 | {
309 | "alias": "$tag_upstream_id",
310 | "groupBy": [
311 | {
312 | "params": [
313 | "$__interval"
314 | ],
315 | "type": "time"
316 | },
317 | {
318 | "params": [
319 | "upstream_id"
320 | ],
321 | "type": "tag"
322 | },
323 | {
324 | "params": [
325 | "none"
326 | ],
327 | "type": "fill"
328 | }
329 | ],
330 | "measurement": "upstream",
331 | "orderByTime": "ASC",
332 | "policy": "default",
333 | "query": "SELECT mean(\"value\") FROM \"measurement\" WHERE $timeFilter GROUP BY time($__interval) fill(null)",
334 | "rawQuery": false,
335 | "refId": "A",
336 | "resultFormat": "time_series",
337 | "select": [
338 | [
339 | {
340 | "params": [
341 | "power"
342 | ],
343 | "type": "field"
344 | },
345 | {
346 | "params": [],
347 | "type": "mean"
348 | }
349 | ]
350 | ],
351 | "tags": []
352 | }
353 | ],
354 | "thresholds": [
355 | {
356 | "colorMode": "critical",
357 | "fill": true,
358 | "line": true,
359 | "op": "lt",
360 | "value": 35
361 | },
362 | {
363 | "colorMode": "critical",
364 | "fill": true,
365 | "line": true,
366 | "op": "gt",
367 | "value": 49
368 | }
369 | ],
370 | "timeFrom": null,
371 | "timeRegions": [],
372 | "timeShift": null,
373 | "title": "Upstream Power",
374 | "tooltip": {
375 | "shared": true,
376 | "sort": 0,
377 | "value_type": "individual"
378 | },
379 | "type": "graph",
380 | "xaxis": {
381 | "buckets": null,
382 | "mode": "time",
383 | "name": null,
384 | "show": true,
385 | "values": []
386 | },
387 | "yaxes": [
388 | {
389 | "decimals": 2,
390 | "format": "dBmV",
391 | "label": "",
392 | "logBase": 1,
393 | "max": "60",
394 | "min": "20",
395 | "show": true
396 | },
397 | {
398 | "format": "short",
399 | "label": null,
400 | "logBase": 1,
401 | "max": null,
402 | "min": null,
403 | "show": true
404 | }
405 | ],
406 | "yaxis": {
407 | "align": false,
408 | "alignLevel": null
409 | }
410 | },
411 | {
412 | "aliasColors": {},
413 | "bars": false,
414 | "dashLength": 10,
415 | "dashes": false,
416 | "datasource": "Modem",
417 | "description": "",
418 | "fieldConfig": {
419 | "defaults": {
420 | "custom": {}
421 | },
422 | "overrides": []
423 | },
424 | "fill": 1,
425 | "fillGradient": 0,
426 | "gridPos": {
427 | "h": 8,
428 | "w": 12,
429 | "x": 0,
430 | "y": 9
431 | },
432 | "hiddenSeries": false,
433 | "id": 65,
434 | "legend": {
435 | "avg": false,
436 | "current": false,
437 | "max": false,
438 | "min": false,
439 | "show": true,
440 | "total": false,
441 | "values": false
442 | },
443 | "lines": true,
444 | "linewidth": 1,
445 | "nullPointMode": "null",
446 | "options": {
447 | "alertThreshold": true
448 | },
449 | "percentage": false,
450 | "pluginVersion": "7.3.4",
451 | "pointradius": 2,
452 | "points": false,
453 | "renderer": "flot",
454 | "seriesOverrides": [],
455 | "spaceLength": 10,
456 | "stack": false,
457 | "steppedLine": false,
458 | "targets": [
459 | {
460 | "alias": "$tag_downstream_id",
461 | "groupBy": [
462 | {
463 | "params": [
464 | "$__interval"
465 | ],
466 | "type": "time"
467 | },
468 | {
469 | "params": [
470 | "downstream_id"
471 | ],
472 | "type": "tag"
473 | },
474 | {
475 | "params": [
476 | "none"
477 | ],
478 | "type": "fill"
479 | }
480 | ],
481 | "measurement": "downstream",
482 | "orderByTime": "ASC",
483 | "policy": "default",
484 | "query": "SELECT mean(\"value\") FROM \"measurement\" WHERE $timeFilter GROUP BY time($__interval) fill(null)",
485 | "rawQuery": false,
486 | "refId": "A",
487 | "resultFormat": "time_series",
488 | "select": [
489 | [
490 | {
491 | "params": [
492 | "correcteds"
493 | ],
494 | "type": "field"
495 | },
496 | {
497 | "params": [],
498 | "type": "last"
499 | },
500 | {
501 | "params": [],
502 | "type": "non_negative_difference"
503 | }
504 | ]
505 | ],
506 | "tags": []
507 | }
508 | ],
509 | "thresholds": [],
510 | "timeFrom": null,
511 | "timeRegions": [],
512 | "timeShift": null,
513 | "title": "Correcteds",
514 | "tooltip": {
515 | "shared": true,
516 | "sort": 0,
517 | "value_type": "individual"
518 | },
519 | "type": "graph",
520 | "xaxis": {
521 | "buckets": null,
522 | "mode": "time",
523 | "name": null,
524 | "show": true,
525 | "values": []
526 | },
527 | "yaxes": [
528 | {
529 | "format": "short",
530 | "label": null,
531 | "logBase": 1,
532 | "max": null,
533 | "min": "0",
534 | "show": true
535 | },
536 | {
537 | "format": "short",
538 | "label": null,
539 | "logBase": 1,
540 | "max": null,
541 | "min": null,
542 | "show": true
543 | }
544 | ],
545 | "yaxis": {
546 | "align": false,
547 | "alignLevel": null
548 | }
549 | },
550 | {
551 | "aliasColors": {},
552 | "bars": false,
553 | "dashLength": 10,
554 | "dashes": false,
555 | "datasource": "Modem",
556 | "description": "",
557 | "fieldConfig": {
558 | "defaults": {
559 | "custom": {}
560 | },
561 | "overrides": []
562 | },
563 | "fill": 1,
564 | "fillGradient": 0,
565 | "gridPos": {
566 | "h": 8,
567 | "w": 12,
568 | "x": 12,
569 | "y": 9
570 | },
571 | "hiddenSeries": false,
572 | "id": 67,
573 | "legend": {
574 | "avg": false,
575 | "current": false,
576 | "max": false,
577 | "min": false,
578 | "show": true,
579 | "total": false,
580 | "values": false
581 | },
582 | "lines": true,
583 | "linewidth": 1,
584 | "nullPointMode": "null",
585 | "options": {
586 | "alertThreshold": true
587 | },
588 | "percentage": false,
589 | "pluginVersion": "7.3.4",
590 | "pointradius": 2,
591 | "points": false,
592 | "renderer": "flot",
593 | "seriesOverrides": [],
594 | "spaceLength": 10,
595 | "stack": false,
596 | "steppedLine": false,
597 | "targets": [
598 | {
599 | "alias": "$tag_downstream_id",
600 | "groupBy": [
601 | {
602 | "params": [
603 | "$__interval"
604 | ],
605 | "type": "time"
606 | },
607 | {
608 | "params": [
609 | "downstream_id"
610 | ],
611 | "type": "tag"
612 | },
613 | {
614 | "params": [
615 | "none"
616 | ],
617 | "type": "fill"
618 | }
619 | ],
620 | "measurement": "downstream",
621 | "orderByTime": "ASC",
622 | "policy": "default",
623 | "query": "SELECT mean(\"value\") FROM \"measurement\" WHERE $timeFilter GROUP BY time($__interval) fill(null)",
624 | "rawQuery": false,
625 | "refId": "A",
626 | "resultFormat": "time_series",
627 | "select": [
628 | [
629 | {
630 | "params": [
631 | "uncorrectables"
632 | ],
633 | "type": "field"
634 | },
635 | {
636 | "params": [],
637 | "type": "last"
638 | },
639 | {
640 | "params": [],
641 | "type": "non_negative_difference"
642 | }
643 | ]
644 | ],
645 | "tags": []
646 | }
647 | ],
648 | "thresholds": [],
649 | "timeFrom": null,
650 | "timeRegions": [],
651 | "timeShift": null,
652 | "title": "Uncorrectables",
653 | "tooltip": {
654 | "shared": true,
655 | "sort": 0,
656 | "value_type": "individual"
657 | },
658 | "type": "graph",
659 | "xaxis": {
660 | "buckets": null,
661 | "mode": "time",
662 | "name": null,
663 | "show": true,
664 | "values": []
665 | },
666 | "yaxes": [
667 | {
668 | "format": "short",
669 | "label": null,
670 | "logBase": 1,
671 | "max": null,
672 | "min": "0",
673 | "show": true
674 | },
675 | {
676 | "format": "short",
677 | "label": null,
678 | "logBase": 1,
679 | "max": null,
680 | "min": null,
681 | "show": true
682 | }
683 | ],
684 | "yaxis": {
685 | "align": false,
686 | "alignLevel": null
687 | }
688 | },
689 | {
690 | "alert": {
691 | "alertRuleTags": {},
692 | "conditions": [
693 | {
694 | "evaluator": {
695 | "params": [
696 | 30
697 | ],
698 | "type": "lt"
699 | },
700 | "operator": {
701 | "type": "and"
702 | },
703 | "query": {
704 | "params": [
705 | "A",
706 | "5m",
707 | "now"
708 | ]
709 | },
710 | "reducer": {
711 | "params": [],
712 | "type": "avg"
713 | },
714 | "type": "query"
715 | }
716 | ],
717 | "executionErrorState": "alerting",
718 | "for": "5m",
719 | "frequency": "1m",
720 | "handler": 1,
721 | "name": "SNR alert",
722 | "noDataState": "no_data",
723 | "notifications": []
724 | },
725 | "aliasColors": {},
726 | "bars": false,
727 | "dashLength": 10,
728 | "dashes": false,
729 | "datasource": "Modem",
730 | "description": "",
731 | "fieldConfig": {
732 | "defaults": {
733 | "custom": {},
734 | "unit": "dB"
735 | },
736 | "overrides": []
737 | },
738 | "fill": 0,
739 | "fillGradient": 0,
740 | "gridPos": {
741 | "h": 8,
742 | "w": 12,
743 | "x": 0,
744 | "y": 17
745 | },
746 | "hiddenSeries": false,
747 | "id": 64,
748 | "legend": {
749 | "avg": false,
750 | "current": false,
751 | "max": false,
752 | "min": false,
753 | "show": true,
754 | "total": false,
755 | "values": false
756 | },
757 | "lines": true,
758 | "linewidth": 1,
759 | "nullPointMode": "null",
760 | "options": {
761 | "alertThreshold": true
762 | },
763 | "percentage": false,
764 | "pluginVersion": "7.3.4",
765 | "pointradius": 2,
766 | "points": false,
767 | "renderer": "flot",
768 | "seriesOverrides": [],
769 | "spaceLength": 10,
770 | "stack": false,
771 | "steppedLine": false,
772 | "targets": [
773 | {
774 | "alias": "$tag_downstream_id",
775 | "groupBy": [
776 | {
777 | "params": [
778 | "$__interval"
779 | ],
780 | "type": "time"
781 | },
782 | {
783 | "params": [
784 | "downstream_id"
785 | ],
786 | "type": "tag"
787 | },
788 | {
789 | "params": [
790 | "none"
791 | ],
792 | "type": "fill"
793 | }
794 | ],
795 | "measurement": "downstream",
796 | "orderByTime": "ASC",
797 | "policy": "default",
798 | "query": "SELECT mean(\"value\") FROM \"measurement\" WHERE $timeFilter GROUP BY time($__interval) fill(null)",
799 | "rawQuery": false,
800 | "refId": "A",
801 | "resultFormat": "time_series",
802 | "select": [
803 | [
804 | {
805 | "params": [
806 | "snr"
807 | ],
808 | "type": "field"
809 | },
810 | {
811 | "params": [],
812 | "type": "mean"
813 | }
814 | ]
815 | ],
816 | "tags": []
817 | }
818 | ],
819 | "thresholds": [
820 | {
821 | "colorMode": "critical",
822 | "fill": true,
823 | "line": true,
824 | "op": "lt",
825 | "value": 30
826 | }
827 | ],
828 | "timeFrom": null,
829 | "timeRegions": [],
830 | "timeShift": null,
831 | "title": "SNR",
832 | "tooltip": {
833 | "shared": true,
834 | "sort": 0,
835 | "value_type": "individual"
836 | },
837 | "type": "graph",
838 | "xaxis": {
839 | "buckets": null,
840 | "mode": "time",
841 | "name": null,
842 | "show": true,
843 | "values": []
844 | },
845 | "yaxes": [
846 | {
847 | "decimals": null,
848 | "format": "dB",
849 | "label": null,
850 | "logBase": 1,
851 | "max": "55",
852 | "min": "25",
853 | "show": true
854 | },
855 | {
856 | "format": "short",
857 | "label": null,
858 | "logBase": 1,
859 | "max": null,
860 | "min": null,
861 | "show": true
862 | }
863 | ],
864 | "yaxis": {
865 | "align": false,
866 | "alignLevel": null
867 | }
868 | }
869 | ],
870 | "refresh": "1m",
871 | "schemaVersion": 26,
872 | "style": "dark",
873 | "tags": [],
874 | "templating": {
875 | "list": []
876 | },
877 | "time": {
878 | "from": "now-1h",
879 | "to": "now"
880 | },
881 | "timepicker": {
882 | "refresh_intervals": [
883 | "10s",
884 | "30s",
885 | "1m",
886 | "5m",
887 | "15m",
888 | "30m",
889 | "1h",
890 | "2h",
891 | "1d"
892 | ],
893 | "time_options": [
894 | "5m",
895 | "15m",
896 | "1h",
897 | "6h",
898 | "12h",
899 | "24h",
900 | "2d",
901 | "7d",
902 | "30d"
903 | ]
904 | },
905 | "timezone": "browser",
906 | "title": "Modem",
907 | "uid": "KRgNDKwMz",
908 | "version": 11
909 | }
910 |
--------------------------------------------------------------------------------