|
39 | 39 | "sessionstorage.getitem('privatekey')", |
40 | 40 | "sessionstorage.getitem(\"privatekey\")", |
41 | 41 | ) |
| 42 | +_RE_TITLE = re.compile(r"<title[^>]*>(.*?)</title>", re.IGNORECASE | re.DOTALL) |
| 43 | +_RE_FORM_ACTION = re.compile(r"<form[^>]+action=['\"]([^'\"]+)['\"]", re.IGNORECASE) |
42 | 44 |
|
43 | 45 | # Fields per channel for each section (after the leading count value). |
44 | 46 | _DS_QAM_FIELDS = 9 # num|lock|mod|chID|freq|power|snr|corrErr|uncorrErr |
@@ -86,6 +88,9 @@ def login(self) -> None: |
86 | 88 | raise RuntimeError("CM3000 authentication failed: connection refused after retry") |
87 | 89 | except requests.RequestException as e: |
88 | 90 | raise RuntimeError(f"CM3000 authentication failed: {e}") |
| 91 | + except RuntimeError as e: |
| 92 | + self._log_status_page_diagnostics(r.text, "login") |
| 93 | + raise e |
89 | 94 |
|
90 | 95 | def get_docsis_data(self) -> dict: |
91 | 96 | """Retrieve DOCSIS channel data from JavaScript on status page. |
@@ -166,7 +171,11 @@ def _fetch_status_page(self) -> str: |
166 | 171 | r.raise_for_status() |
167 | 172 | except requests.RequestException as e: |
168 | 173 | raise RuntimeError(f"CM3000 status page retrieval failed: {e}") |
169 | | - self._ensure_status_page(r.text) |
| 174 | + try: |
| 175 | + self._ensure_status_page(r.text) |
| 176 | + except RuntimeError: |
| 177 | + self._log_status_page_diagnostics(r.text, "fetch") |
| 178 | + raise |
170 | 179 | return r.text |
171 | 180 |
|
172 | 181 | @staticmethod |
@@ -200,6 +209,59 @@ def _ensure_status_page(html: str) -> None: |
200 | 209 | "CM3000 status page did not contain the expected DOCSIS data blocks" |
201 | 210 | ) |
202 | 211 |
|
| 212 | + @staticmethod |
| 213 | + def _status_page_diagnostics(html: str) -> dict: |
| 214 | + """Summarize the response shape for debugging failed CM3000 auth.""" |
| 215 | + if not html: |
| 216 | + return { |
| 217 | + "length": 0, |
| 218 | + "title": "", |
| 219 | + "form_action": "", |
| 220 | + "login_markers": [], |
| 221 | + "has_sys_info": False, |
| 222 | + "has_channel_data": False, |
| 223 | + } |
| 224 | + |
| 225 | + lower_html = html.lower() |
| 226 | + title_match = _RE_TITLE.search(html) |
| 227 | + form_match = _RE_FORM_ACTION.search(html) |
| 228 | + login_markers = [marker for marker in _LOGIN_MARKERS if marker in lower_html] |
| 229 | + has_sys_info = bool(CM3000Driver._extract_tag_value_list(html, "InitTagValue")) |
| 230 | + has_channel_data = any( |
| 231 | + CM3000Driver._extract_tag_value_list(html, function_name) |
| 232 | + for function_name in ( |
| 233 | + "InitDsTableTagValue", |
| 234 | + "InitUsTableTagValue", |
| 235 | + "InitDsOfdmTableTagValue", |
| 236 | + "InitUsOfdmaTableTagValue", |
| 237 | + ) |
| 238 | + ) |
| 239 | + |
| 240 | + return { |
| 241 | + "length": len(html), |
| 242 | + "title": (title_match.group(1).strip() if title_match else ""), |
| 243 | + "form_action": (form_match.group(1).strip() if form_match else ""), |
| 244 | + "login_markers": login_markers, |
| 245 | + "has_sys_info": has_sys_info, |
| 246 | + "has_channel_data": has_channel_data, |
| 247 | + } |
| 248 | + |
| 249 | + @staticmethod |
| 250 | + def _log_status_page_diagnostics(html: str, context: str) -> None: |
| 251 | + """Emit a compact debug summary of the CM3000 response page.""" |
| 252 | + diag = CM3000Driver._status_page_diagnostics(html) |
| 253 | + log.debug( |
| 254 | + "CM3000 %s diagnostics: len=%s title=%r form_action=%r login_markers=%s " |
| 255 | + "has_sys_info=%s has_channel_data=%s", |
| 256 | + context, |
| 257 | + diag["length"], |
| 258 | + diag["title"], |
| 259 | + diag["form_action"], |
| 260 | + ",".join(diag["login_markers"]) or "(none)", |
| 261 | + diag["has_sys_info"], |
| 262 | + diag["has_channel_data"], |
| 263 | + ) |
| 264 | + |
203 | 265 | # -- Channel parsers -- |
204 | 266 |
|
205 | 267 | def _parse_ds_qam(self, html: str) -> list: |
|
0 commit comments