1- """DOCSIS channel health analysis with configurable thresholds."""
1+ """DOCSIS channel health analysis with configurable thresholds.
22
3+ Thresholds are loaded from thresholds.json (Vodafone VFKD guidelines).
4+ The file supports per-modulation thresholds for DS power, US power, and SNR.
5+ """
6+
7+ import json
38import logging
9+ import os
410
511log = logging .getLogger ("docsis.analyzer" )
612
7- # --- Reference thresholds (based on Vodafone VFKD guidelines, 256QAM) ---
8- # Downstream Power (dBmV): asymmetric range
9- # Regelkonform: -3.9 to 13.0, toleriert: -5.9/-4.0 / 13.1-18.0, sofort: <-8 / >20
10- DS_POWER_LOW_GOOD = - 4.0
11- DS_POWER_HIGH_GOOD = 13.0
12- DS_POWER_LOW_WARN = - 6.0
13- DS_POWER_HIGH_WARN = 18.0
14- DS_POWER_LOW_CRIT = - 8.0
15- DS_POWER_HIGH_CRIT = 20.0
16-
17- # Upstream Power (dBmV): based on VFKD 64QAM
18- # Regelkonform: 41.1-47.0, toleriert: 38.1-41.0 / 47.1-50.0, sofort: <35 / >53
19- US_POWER_LOW_GOOD = 41.0
20- US_POWER_HIGH_GOOD = 47.0
21- US_POWER_LOW_WARN = 38.0
22- US_POWER_HIGH_WARN = 50.0
23- US_POWER_LOW_CRIT = 35.0
24- US_POWER_HIGH_CRIT = 53.0
25-
26- # SNR / MER (dB): good >33, marginal 29-33, bad <29
27- # Based on Vodafone VFKD guidelines: 256QAM regelkonform > 33.1 dB
28- SNR_WARN = 33.0
29- SNR_CRIT = 29.0
30-
31- # Uncorrectable errors threshold
32- UNCORR_ERRORS_CRIT = 10000
13+ # --- Load thresholds from JSON ---
14+ _THRESHOLDS_PATH = os .path .join (os .path .dirname (__file__ ), "thresholds.json" )
15+ _thresholds = {}
16+
17+
18+ def _load_thresholds ():
19+ """Load thresholds from JSON file. Falls back to hardcoded defaults."""
20+ global _thresholds
21+ try :
22+ with open (_THRESHOLDS_PATH , "r" ) as f :
23+ _thresholds = json .load (f )
24+ log .info ("Loaded thresholds from %s" , _THRESHOLDS_PATH )
25+ except (FileNotFoundError , json .JSONDecodeError ) as e :
26+ log .warning ("Could not load thresholds.json (%s), using defaults" , e )
27+ _thresholds = {}
28+
29+
30+ def _get_ds_power_thresholds (modulation = None ):
31+ """Get DS power thresholds for a given modulation."""
32+ ds = _thresholds .get ("downstream_power" , {})
33+ default_mod = ds .get ("_default" , "256QAM" )
34+ mod = modulation if modulation in ds else default_mod
35+ t = ds .get (mod , {})
36+ return {
37+ "good_min" : t .get ("good_min" , - 4.0 ),
38+ "good_max" : t .get ("good_max" , 13.0 ),
39+ "crit_min" : t .get ("immediate_min" , - 8.0 ),
40+ "crit_max" : t .get ("immediate_max" , 20.0 ),
41+ }
42+
43+
44+ def _get_us_power_thresholds (modulation = None ):
45+ """Get US power thresholds for a given modulation."""
46+ us = _thresholds .get ("upstream_power" , {})
47+ default_mod = us .get ("_default" , "64QAM" )
48+ mod = modulation if modulation in us else default_mod
49+ t = us .get (mod , {})
50+ return {
51+ "good_min" : t .get ("good_min" , 41.0 ),
52+ "good_max" : t .get ("good_max" , 47.0 ),
53+ "crit_min" : t .get ("immediate_min" , 35.0 ),
54+ "crit_max" : t .get ("immediate_max" , 53.0 ),
55+ }
56+
57+
58+ def _get_snr_thresholds (modulation = None ):
59+ """Get SNR thresholds for a given modulation."""
60+ snr = _thresholds .get ("snr" , {})
61+ default_mod = snr .get ("_default" , "256QAM" )
62+ mod = modulation if modulation in snr else default_mod
63+ t = snr .get (mod , {})
64+ return {
65+ "good_min" : t .get ("good_min" , 33.0 ),
66+ "crit_min" : t .get ("immediate_min" , 29.0 ),
67+ }
68+
69+
70+ def _get_uncorr_threshold ():
71+ return _thresholds .get ("errors" , {}).get ("uncorrectable_threshold" , 10000 )
72+
73+
74+ # Load on module import
75+ _load_thresholds ()
3376
3477
3578def _parse_float (val , default = 0.0 ):
@@ -59,23 +102,25 @@ def _assess_ds_channel(ch, docsis_ver):
59102 """Assess a single downstream channel. Returns (health, health_detail)."""
60103 issues = []
61104 power = _parse_float (ch .get ("powerLevel" ))
105+ modulation = (ch .get ("modulation" ) or ch .get ("type" ) or "" ).upper ().replace ("-" , "" )
62106
63- if power < DS_POWER_LOW_CRIT or power > DS_POWER_HIGH_CRIT :
107+ pt = _get_ds_power_thresholds (modulation )
108+ if power < pt ["crit_min" ] or power > pt ["crit_max" ]:
64109 issues .append ("power critical" )
65- elif power < DS_POWER_LOW_GOOD or power > DS_POWER_HIGH_GOOD :
110+ elif power < pt [ "good_min" ] or power > pt [ "good_max" ] :
66111 issues .append ("power warning" )
67112
113+ snr_val = None
68114 if docsis_ver == "3.0" and ch .get ("mse" ):
69- snr = abs (_parse_float (ch ["mse" ]))
70- if snr < SNR_CRIT :
71- issues .append ("snr critical" )
72- elif snr < SNR_WARN :
73- issues .append ("snr warning" )
115+ snr_val = abs (_parse_float (ch ["mse" ]))
74116 elif docsis_ver == "3.1" and ch .get ("mer" ):
75- snr = _parse_float (ch ["mer" ])
76- if snr < SNR_CRIT :
117+ snr_val = _parse_float (ch ["mer" ])
118+
119+ if snr_val is not None :
120+ st = _get_snr_thresholds (modulation )
121+ if snr_val < st ["crit_min" ]:
77122 issues .append ("snr critical" )
78- elif snr < SNR_WARN :
123+ elif snr_val < st [ "good_min" ] :
79124 issues .append ("snr warning" )
80125
81126 return _channel_health (issues ), _health_detail (issues )
@@ -85,10 +130,12 @@ def _assess_us_channel(ch):
85130 """Assess a single upstream channel. Returns (health, health_detail)."""
86131 issues = []
87132 power = _parse_float (ch .get ("powerLevel" ))
133+ modulation = (ch .get ("modulation" ) or ch .get ("type" ) or "" ).upper ().replace ("-" , "" )
88134
89- if power < US_POWER_LOW_CRIT or power > US_POWER_HIGH_CRIT :
135+ pt = _get_us_power_thresholds (modulation )
136+ if power < pt ["crit_min" ] or power > pt ["crit_max" ]:
90137 issues .append ("power critical" )
91- elif power < US_POWER_LOW_GOOD or power > US_POWER_HIGH_GOOD :
138+ elif power < pt [ "good_min" ] or power > pt [ "good_max" ] :
92139 issues .append ("power warning" )
93140
94141 return _channel_health (issues ), _health_detail (issues )
@@ -201,19 +248,24 @@ def analyze(data: dict) -> dict:
201248
202249 # --- Overall health ---
203250 issues = []
204- if ds_powers and (min (ds_powers ) < DS_POWER_LOW_CRIT or max (ds_powers ) > DS_POWER_HIGH_CRIT ):
251+ # Summary uses default (256QAM) thresholds for overall health
252+ ds_pt = _get_ds_power_thresholds ()
253+ us_pt = _get_us_power_thresholds ()
254+ snr_t = _get_snr_thresholds ()
255+
256+ if ds_powers and (min (ds_powers ) < ds_pt ["crit_min" ] or max (ds_powers ) > ds_pt ["crit_max" ]):
205257 issues .append ("ds_power_critical" )
206- elif ds_powers and (min (ds_powers ) < DS_POWER_LOW_GOOD or max (ds_powers ) > DS_POWER_HIGH_GOOD ):
258+ elif ds_powers and (min (ds_powers ) < ds_pt [ "good_min" ] or max (ds_powers ) > ds_pt [ "good_max" ] ):
207259 issues .append ("ds_power_warn" )
208- if us_powers and (min (us_powers ) < US_POWER_LOW_CRIT or max (us_powers ) > US_POWER_HIGH_CRIT ):
260+ if us_powers and (min (us_powers ) < us_pt [ "crit_min" ] or max (us_powers ) > us_pt [ "crit_max" ] ):
209261 issues .append ("us_power_critical" )
210- elif us_powers and (min (us_powers ) < US_POWER_LOW_GOOD or max (us_powers ) > US_POWER_HIGH_GOOD ):
262+ elif us_powers and (min (us_powers ) < us_pt [ "good_min" ] or max (us_powers ) > us_pt [ "good_max" ] ):
211263 issues .append ("us_power_warn" )
212- if ds_snrs and min (ds_snrs ) < SNR_CRIT :
264+ if ds_snrs and min (ds_snrs ) < snr_t [ "crit_min" ] :
213265 issues .append ("snr_critical" )
214- elif ds_snrs and min (ds_snrs ) < SNR_WARN :
266+ elif ds_snrs and min (ds_snrs ) < snr_t [ "good_min" ] :
215267 issues .append ("snr_warn" )
216- if total_uncorr > UNCORR_ERRORS_CRIT :
268+ if total_uncorr > _get_uncorr_threshold () :
217269 issues .append ("uncorr_errors_high" )
218270
219271 if not issues :
0 commit comments