From 96ca12130dcc2fe2ff842154fee9d7763430d48b Mon Sep 17 00:00:00 2001 From: Andy Petralli <6251451+andresp@users.noreply.github.com> Date: Sun, 6 Oct 2024 12:27:46 -0700 Subject: [PATCH] Coda56 (#38) * Coda56 modem support * Parsing through Json --- .gitignore | 5 +- pyproject.toml | 7 + setup.py | 16 ++ src/docsismodem/__init__.py | 7 +- src/docsismodem/collectionJob.py | 6 +- src/docsismodem/modems/hitron_coda56.py | 152 ++++++++++++++++++ src/docsismodem/modems/modemtype.py | 3 +- src/docsismodem/modems/observablemodem.py | 2 +- .../modems/observablemodemfactory.py | 4 +- src/docsismodem/modems/technicolor_xb7.py | 2 +- src/docsismodem/probe.py | 5 +- src/docsismodem/retriever.py | 11 +- 12 files changed, 201 insertions(+), 19 deletions(-) create mode 100644 setup.py create mode 100644 src/docsismodem/modems/hitron_coda56.py diff --git a/.gitignore b/.gitignore index 26ae215..d843588 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ __pycache__/ # Distribution / packaging .Python +bin/ build/ develop-eggs/ dist/ @@ -139,4 +140,6 @@ cython_debug/ cablemodem-status.last data/configuration.ini -.kube/ \ No newline at end of file +.kube/ + +Scripts/ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 19a65c1..d2d3c20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,3 +13,10 @@ testpaths = [ pythonpath = [ "src" ] + +[tool.setuptools.packages.find] +# All the following settings are optional: +where = ["src"] # ["."] by default +include = ["docsismodem*"] # ["*"] by default +exclude = ["docsismodem.tests*"] # empty by default +namespaces = false # true by default \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..07d5af6 --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup, find_packages + +setup( + name='docsismodem', + version='0.0.34', + install_requires=[ + 'requests', + 'importlib-metadata; python_version<"3.11"', + ], + packages=find_packages( + # All keyword arguments below are optional: + where='src', # '.' by default + include=['mypackage*'], # ['*'] by default + exclude=['mypackage.tests'], # empty by default + ), +) \ No newline at end of file diff --git a/src/docsismodem/__init__.py b/src/docsismodem/__init__.py index 98c9edd..23284ac 100644 --- a/src/docsismodem/__init__.py +++ b/src/docsismodem/__init__.py @@ -1,3 +1,6 @@ -# from . import retriever +from .exceptions import ModemConnectionError, ModemCredentialsError +from .collectionJob import CollectionJob -__version__ = '0.0.33' +__all__ = ["CollectionJob", "ModemConnectionError", "ModemCredentialsError"] + +__version__ = '0.0.33' \ No newline at end of file diff --git a/src/docsismodem/collectionJob.py b/src/docsismodem/collectionJob.py index 5331634..24eaef5 100644 --- a/src/docsismodem/collectionJob.py +++ b/src/docsismodem/collectionJob.py @@ -2,9 +2,9 @@ import functools from logging import Logger from sched import scheduler -from .modems.observablemodem import ObservableModem +from modems.observablemodem import ObservableModem -class collectionJob(): +class CollectionJob(): modem = None collectLogs = False @@ -31,7 +31,7 @@ def wrapper(*args, **kwargs): return catch_exceptions_decorator @catch_exceptions(cancel_on_failure=False) - def collectionJob(self): + def CollectionJob(self): self.modem.login() diff --git a/src/docsismodem/modems/hitron_coda56.py b/src/docsismodem/modems/hitron_coda56.py new file mode 100644 index 0000000..c8c5a23 --- /dev/null +++ b/src/docsismodem/modems/hitron_coda56.py @@ -0,0 +1,152 @@ +import json +from datetime import datetime, timezone +from influxdb_client import Point +import requests + +from .observablemodem import ObservableModem + +class HitronCoda56(ObservableModem): + baseUrl = "" + hostname = "" + session = None + + def __init__(self, config, logger): + self.hostname = config['Modem']['Host'] + self.baseUrl = "https://" + self.hostname + self.session = requests.Session() + + super(HitronCoda56, self).__init__(config, logger) + + def formatUpstreamPoints(self, data, sampleTime): + points = [] + for index in range(0, len(data)): + + values = data[index] + + point = Point("upstreamQam") \ + .tag("channel", values["portId"]) \ + .tag("modulation", values["modtype"]) \ + .tag("mode", values["scdmaMode"]) \ + .tag("symbolRate", int(values["bandwidth"])) \ + .tag("channelId", int(values["channelId"])) \ + .tag("frequency", values["frequency"]) \ + .time(sampleTime) \ + .field("power", float(values["signalStrength"])) + + points.append(point) + + return points + + def formatUpstreamOfdmaPoints(self, data, sampleTime): + points = [] + for index in range(0, len(data)): + + values = data[index] + + if values["state"] == ' DISABLED': + continue + + point = Point("upstreamOfdma") \ + .tag("channel", values["uschindex"]) \ + .tag("modulation", "OFDMA") \ + .tag("channelId", int(values["uschindex"])) \ + .tag("frequency", values["frequency"]) \ + .tag("fftsize", values["fftVal"]) \ + .time(sampleTime) \ + .field("digatten", float(values["digAtten"])) \ + .field("digattenbo", float(values["digAttenBo"])) \ + .field("channelbw", float(values["channelBw"])) \ + .field("reppower", float(values["repPower"])) \ + .field("reppower1_6", float(values["repPower1_6"])) + + points.append(point) + + return points + + def formatDownstreamPoints(self, data, sampleTime): + points = [] + + for index in range(0, len(data)): + + values = data[index] + + point = Point("downstreamQam") \ + .tag("channel", values["portId"]) \ + .tag("modulation", "256QAM" if values["modulation"] == "2" else '"') \ + .tag("channelId", int(values["channelId"])) \ + .tag("frequency", values["frequency"]) \ + .time(sampleTime) \ + .field("power", float(values["signalStrength"])) \ + .field("snr", float(values["snr"])) \ + .field("octets", int(values["dsoctets"])) \ + .field("correctables", int(values["correcteds"])) \ + .field("uncorrectables", int(values["uncorrect"])) + + points.append(point) + + return points + + def formatDownstreamOfdmPoints(self, data, sampleTime): + points = [] + + for index in range(0, len(data)): + + values = data[index] + + if values["plclock"] != 'YES': + continue + + point = Point("downstreamOFDM") \ + .tag("receiver", values["receive"]) \ + .tag("modulation", "OFDM") \ + .tag("ffttype", values["ffttype"]) \ + .tag("frequency", values["Subcarr0freqFreq"]) \ + .time(sampleTime) \ + .field("power", float(values["plcpower"])) \ + .field("snr", float(values["SNR"])) \ + .field("octets", int(values["dsoctets"])) \ + .field("correctables", int(values["correcteds"])) \ + .field("uncorrectables", int(values["uncorrect"])) + + points.append(point) + + return points + + def login(self): + pass + + def collectStatus(self): + self.logger.info("Getting modem status") + + sampleTime = datetime.now(timezone.utc).isoformat() + now = datetime.now(timezone.utc).timestamp + + # QAM down + response = self.session.get(self.baseUrl + "/data/dsinfo.asp?_=" + str(now), verify=False) + downstreamData = json.loads(response.text) + downstreamPoints = self.formatDownstreamPoints(downstreamData, sampleTime) + + # OFDM down + response = self.session.get(self.baseUrl + "/data/dsofdminfo.asp?_=" + str(now), verify=False) + downstreamTableOfdm = json.loads(response.text) + downstreamOfdmPoints = self.formatDownstreamOfdmPoints(downstreamTableOfdm, sampleTime) + + # TDMA up + response = self.session.get(self.baseUrl + "/data/usinfo.asp?_=" + str(now), verify=False) + upstreamData = json.loads(response.text) + upstreamPoints = self.formatUpstreamPoints(upstreamData, sampleTime) + + # OFDMA up + response = self.session.get(self.baseUrl + "/data/usofdminfo.asp?_=" + str(now), verify=False) + upstreamOfdmaData = json.loads(response.text) + upstreamOfdmaPoints = self.formatUpstreamOfdmaPoints(upstreamOfdmaData, sampleTime) + + # Store data to InfluxDB + self.timeseriesWriter.write(record=downstreamPoints) + self.timeseriesWriter.write(record=downstreamOfdmPoints) + self.timeseriesWriter.write(record=upstreamPoints) + self.timeseriesWriter.write(record=upstreamOfdmaPoints) + + def collectLogs(self): + # Not implemented yet + pass \ No newline at end of file diff --git a/src/docsismodem/modems/modemtype.py b/src/docsismodem/modems/modemtype.py index 2c755a8..cfc8d14 100644 --- a/src/docsismodem/modems/modemtype.py +++ b/src/docsismodem/modems/modemtype.py @@ -4,4 +4,5 @@ class ModemType(StrEnum): MotorolaMB8600 = "MotorolaMB8600" NetgearCM2000 = "NetgearCM2000" TechnicolorXB7 = "TechnicolorXB7" - TouchstoneTG3492UPCCH = "TouchstoneTG3492UPCCH" \ No newline at end of file + TouchstoneTG3492UPCCH = "TouchstoneTG3492UPCCH" + HitronCoda56 = "HitronCoda56" \ No newline at end of file diff --git a/src/docsismodem/modems/observablemodem.py b/src/docsismodem/modems/observablemodem.py index 6adab38..de1b3e1 100644 --- a/src/docsismodem/modems/observablemodem.py +++ b/src/docsismodem/modems/observablemodem.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod -from ..storage.timeserieswriterfactory import TimeseriesWriterFactory +from storage.timeserieswriterfactory import TimeseriesWriterFactory class ObservableModem(ABC): diff --git a/src/docsismodem/modems/observablemodemfactory.py b/src/docsismodem/modems/observablemodemfactory.py index cd1c668..5a9d261 100644 --- a/src/docsismodem/modems/observablemodemfactory.py +++ b/src/docsismodem/modems/observablemodemfactory.py @@ -1,11 +1,11 @@ from .modemtype import ModemType +from .hitron_coda56 import HitronCoda56 from .motorola_mb8600 import MotorolaMB8600 from .netgear_cm2000 import NetgearCM2000 from .observablemodem import ObservableModem from .technicolor_xb7 import TechnicolorXB7 from .touchstone_tg3492_upc_ch import TouchstoneTG3492UPCCH - class ObservableModemFactory(): @staticmethod def get(type: ModemType, config, logger) -> ObservableModem: @@ -19,5 +19,7 @@ def get(type: ModemType, config, logger) -> ObservableModem: return TechnicolorXB7(config, logger) case ModemType.TouchstoneTG3492UPCCH: return TouchstoneTG3492UPCCH(config, logger) + case ModemType.HitronCoda56: + return HitronCoda56(config, logger) case _: raise ValueError("Invalid modem type selected.") diff --git a/src/docsismodem/modems/technicolor_xb7.py b/src/docsismodem/modems/technicolor_xb7.py index 3f913e8..c628548 100644 --- a/src/docsismodem/modems/technicolor_xb7.py +++ b/src/docsismodem/modems/technicolor_xb7.py @@ -1,4 +1,4 @@ -from ..exceptions import ModemConnectionError, ModemCredentialsError +from exceptions import ModemConnectionError, ModemCredentialsError from .observablemodem import ObservableModem from bs4 import BeautifulSoup from datetime import datetime diff --git a/src/docsismodem/probe.py b/src/docsismodem/probe.py index 60361ea..3a70b8b 100644 --- a/src/docsismodem/probe.py +++ b/src/docsismodem/probe.py @@ -1,14 +1,13 @@ import datetime from flask_healthz import HealthError -from .collectionJob import collectionJob - +from collectionJob import CollectionJob class Probe(): runner = None runEveryMinutes = 0 - def __init__(self, runner: collectionJob, runEveryMinutes: int) -> None: + def __init__(self, runner: CollectionJob, runEveryMinutes: int) -> None: self.runner = runner self.runEveryMinutes = runEveryMinutes diff --git a/src/docsismodem/retriever.py b/src/docsismodem/retriever.py index 892f49c..5f2a95d 100644 --- a/src/docsismodem/retriever.py +++ b/src/docsismodem/retriever.py @@ -5,14 +5,13 @@ from requests.packages import urllib3 import schedule import time -from .modems.observablemodemfactory import ObservableModemFactory +from collectionJob import CollectionJob +from probe import Probe +from modems import ObservableModemFactory from flask import Flask from flask_healthz import healthz -from .probe import Probe -from .collectionJob import collectionJob - def main(): consoleLogger.info("Connecting to InfluxDB") @@ -31,7 +30,7 @@ def main(): modem = ObservableModemFactory.get(config['General']['ModemType'], config, consoleLogger) - jobRunner = collectionJob(modem, config['Modem'].getboolean('CollectLogs'), consoleLogger) + jobRunner = CollectionJob(modem, config['Modem'].getboolean('CollectLogs'), consoleLogger) # Because the modem uses a self-signed certificate and this is expected, disabling the warning to reduce noise. urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -57,7 +56,7 @@ def runDaemon(): time.sleep(1) else: consoleLogger.info("One-time execution") - jobRunner.collectionJob() + jobRunner.CollectionJob()