├── 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 | ![Screenshots of the Arris Cable Modem status page and the scraped data in Grafana](media/modem_scrape.png) 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 | --------------------------------------------------------------------------------