Skip to content

Conversation

@CoMPaTech
Copy link
Owner

@CoMPaTech CoMPaTech commented Oct 20, 2025

Summary by CodeRabbit

  • New Features

    • Support for v6 XM device authentication alongside v8; raw device status fetch available; public API version exposure.
  • Data

    • Host telemetry adds CPU busy/total and cpuload derivation; wireless frequency, rates, polling capacity and antenna gain normalized; new IEEE mode values; fw_major included in fixtures.
  • Models

    • Expanded device model mappings with added manual entries and improved reverse lookup.
  • Tests

    • Added v6 login test, updated parametrized fixtures, adjusted enum warning expectation, removed legacy test helper.
  • Chores

    • Ignored local test file, bumped project version, updated changelog.

@coderabbitai
Copy link

coderabbitai bot commented Oct 20, 2025

Walkthrough

Adds AirOS v6 XM login flow with cookie activation and api_version tracking; switches auth to raw cookie headers; promotes derived_data to a classmethod and adds fw_major/mac helpers; extends data models for v6 (CPU counters, numeric frequency, polling, antenna_gain, ieeemode); adds v6 fixtures, tests, bump version, and ignores test.py.

Changes

Cohort / File(s) Summary
Gitignore
\.gitignore
Adds test.py to ignored files.
Auth / login flow
airos/base.py
Adds public api_version; introduces _v6_xm_login_url, _v6_form_url, _login_urls list; implements _login_v6() GET/POST/activation flow and fallback in login() on AirOSUrlNotFoundError; stores raw _auth_cookie and uses it in _get_authenticated_headers(); adds raw_status().
Data models / parsing
airos/data.py
Adds Host6 fields cpubusy/cputotal and cpuload computation; Wireless6: frequency -> int, adds polling (dl_capacity/ul_capacity), ieeemode, antenna_gain; expands IeeeMode; adds Derived.fw_major.
AirOS helpers & derived-data API
airos/airos6.py, airos/base.py, script/generate_ha_fixture.py, script/mashumaro-step-debug.py
Makes derived_data a @classmethod and updates callers; renames derived_wireless_data -> _derived_wireless_data for AirOS6; debug script and generator switched to new API.
Model mapping
airos/model_map.py
Splits MODELS into SITE_MODELS and MANUAL_MODELS, composes MODELS = {**SITE_MODELS, **MANUAL_MODELS}, adds manual entries (e.g., "LiteAP GPS", "NanoStation loco M5"), creates reverse _SKUS, trims input before lookups.
Helpers / device detection
airos/helpers.py
Consolidates detection to use raw_status() and AirOS utilities (fw_major, mac, hostname); centralizes logging and error handling; removes per-version parsing branches in favor of unified path.
Tests — additions & updates
tests/test_stations6.py, tests/test_data.py, tests/test_airos6.py, tests/test_airos8.py
Adds test_login_v6_flow (uses MagicMock) and extends parametrization; changes expected unknown-ieee warning to "UNSUPPORTED_IEEE"; adds host.fwversion to some test inputs.
Tests — removals
tests/test_helpers.py
Removes tests/test_helpers.py (including mock_session fixture and test_firmware_detection).
Fixtures — new & updated snapshots
fixtures/..., fixtures/userdata/...
Adds multiple NanoStation loco M5 XM v6.3.16 AP/STA snapshots and userdata fixtures; updates airos_NanoStation_M5_sta_v6.3.16.json (adds fw_major, cpubusy/cputotal, numeric frequency/rates, antenna_gain); inserts derived.fw_major: 8 into many existing v8 fixtures.
Changelog
CHANGELOG.md
Adds release entry for 0.6.0 documenting v6 XM support and parsing improvements.
Project version
pyproject.toml
Bumps project version from 0.5.6 to 0.6.0.
Scripts
script/test.sh
Adds script/test.sh wrapper to run pytest with coverage and optional args.
CI
.github/workflows/verify.yml
Lowers required coverage threshold in verify workflow from 85 to 80.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Caller
    participant AirOS as AirOS.login()
    participant V8 as v8 (_login_url)
    participant XM as v6 (_v6_xm_login_url)
    participant Form as v6 (_v6_form_url)

    Client->>AirOS: login(credentials)
    AirOS->>V8: POST credentials (attempt v8)
    alt v8 accepted
        V8-->>AirOS: auth cookie
        AirOS->>Client: connected=true, api_version=8
    else v8 404 / not found
        AirOS->>XM: GET /login.cgi (fetch cookie)
        XM-->>AirOS: Set-Cookie
        AirOS->>XM: POST /login.cgi (credentials) -> expect 302
        XM-->>AirOS: 302 redirect
        AirOS->>Form: GET /index.cgi (activate session)
        Form-->>AirOS: activation OK (session cookie active)
        AirOS->>Client: connected=true, api_version=6
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 I hopped through code with keys in paw,
v8 closed, v6 waved me in with a cookie-law,
I chased redirects, fetched fw Major and MAC,
Nibbled fixtures, tests, and left a tiny track,
Hop! — api_version set, the bundle's back.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "Alternative v6 XM approach" directly relates to the primary objective of the changeset, which is implementing support for v6 XM firmware through a new login flow and related architectural changes. The title accurately captures the core focus: introducing an alternative method to handle v6 XM devices, including the new _login_v6() method, updated authentication flow, and supporting data model enhancements. While the PR includes additional changes such as model mapping refactoring and version bumps, the v6 XM support remains the central theme. The title is concise, clear, and provides a teammate sufficient context to understand the primary change without requiring excessive detail.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch v6loginfix3

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Oct 20, 2025

Codecov Report

❌ Patch coverage is 76.19048% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.07%. Comparing base (825afe2) to head (af1eb08).
⚠️ Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
airos/helpers.py 0.00% 15 Missing ⚠️
airos/base.py 83.87% 10 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #122      +/-   ##
==========================================
- Coverage   89.57%   87.07%   -2.50%     
==========================================
  Files           8        8              
  Lines         681      743      +62     
==========================================
+ Hits          610      647      +37     
- Misses         71       96      +25     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
airos/base.py (2)

264-274: Handle non-JSON login responses to avoid false failures.

Login endpoints may return 204/HTML. Skip JSON parsing for login URLs.

-                response_text = await response.text()
-                _LOGGER.debug("Successfully fetched JSON from %s", url)
-
-                # If this is the login request, we need to store the new auth data
-                if url in self._login_urls:
-                    self._store_auth_data(response)
-                    self.connected = True
-
-                return json.loads(response_text)
+                _LOGGER.debug("Request to %s succeeded", url)
+                # Login endpoints: store auth and return immediately without JSON parsing
+                if url in self._login_urls:
+                    self._store_auth_data(response)
+                    self.connected = True
+                    return {}
+                # Other endpoints: parse JSON as usual
+                response_text = await response.text()
+                return json.loads(response_text)

Also applies to: 268-272


383-391: Bug: Inconsistent body vs Content-Type in update_check(force=True).

You're sending JSON with form Content-Type. Align with JSON for both branches.

-        if force:
-            return await self._request_json(
-                "POST",
-                self._update_check_url,
-                json_data={"force": True},
-                authenticated=True,
-                ct_form=True,
-            )
+        if force:
+            return await self._request_json(
+                "POST",
+                self._update_check_url,
+                json_data={"force": True},
+                authenticated=True,
+                ct_json=True,
+            )

Also applies to: 396-397

🧹 Nitpick comments (4)
.gitignore (1)

15-15: Anchor ignore to root (or use local exclude) to avoid unintended matches.

Use "/test.py" if you only mean the repo‑root file, or prefer .git/info/exclude locally to avoid committing personal helpers.

Is this meant to ignore only the root-level test.py?

airos/base.py (3)

58-59: Minor: avoid magic number for API versions.

Consider an Enum or constants (e.g., API_V8 = 8) for readability.


95-95: Use an immutable container for login URLs.

Tuple (or set) communicates intent and avoids accidental mutation.

-        self._login_urls = [self._login_url, self._v6_xm_login_url]
+        self._login_urls = (self._login_url, self._v6_xm_login_url)

293-342: _login_v6 flow LGTM; consider reuse and headers parity.

Optional: call _store_auth_data on the initial GET to reuse parsing; ensure Referer/Origin are consistent across calls.

Do you want me to refactor _login_v6 to reuse _store_auth_data after we fix the cookie handling?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a35a60 and 6feacb3.

📒 Files selected for processing (2)
  • .gitignore (1 hunks)
  • airos/base.py (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
airos/base.py (1)
airos/exceptions.py (3)
  • AirOSDeviceConnectionError (24-25)
  • AirOSConnectionAuthenticationError (12-13)
  • AirOSUrlNotFoundError (44-45)
🔇 Additional comments (3)
airos/base.py (3)

82-85: v6 endpoints look reasonable.

LGTM; the leading slash on _v6_form_url matches f"{self.base_url}{...}" usage.


253-256: Pre-login request gating is good.

Prevents accidental calls before authentication. LGTM.


347-349: Graceful fallback to v6 on 404 looks good.

Clear control flow; preserves 8.x first, then v6.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
airos/base.py (2)

213-214: Past review concern has been addressed; remove commented code.

The previous review flagged a critical bug about the "AIROS_" prefix being stripped. This has been resolved—line 229 now stores the full cookie as f"{morsel.key}={morsel.value}", preserving the "AIROS_" prefix. The current implementation is correct.

Remove the commented-out line for cleanliness:

         if self._auth_cookie:  # pragma: no cover
-            # headers["Cookie"] = f"AIROS_{self._auth_cookie}"
             headers["Cookie"] = self._auth_cookie

228-229: Past review concern has been addressed; remove commented code.

The cookie is now correctly stored with the full key name (including "AIROS_" prefix), addressing the previous review's critical concern.

Remove the commented-out line:

             if key.startswith("AIROS_"):
-                # self._auth_cookie = morsel.key[6:] + "=" + morsel.value
                 self._auth_cookie = f"{morsel.key}={morsel.value}"
                 break
🧹 Nitpick comments (3)
airos/base.py (3)

302-304: Optional: Simplify cookie retrieval.

The cookie retrieval logic is correct but could be more readable.

Consider this slightly clearer approach:

-            session_cookie = next(
-                (c for n, c in response.cookies.items() if n.startswith("AIROS")), None
-            )
+            airos_cookies = [c for n, c in response.cookies.items() if n.startswith("AIROS")]
+            session_cookie = airos_cookies[0] if airos_cookies else None

339-340: Consider more robust session activation check.

Checking for "login.cgi" in the URL string works but is fragile—it could match query parameters or other unintended parts of the URL.

Consider a more explicit check:

-            if "login.cgi" in str(response.url):
+            if response.url.path.endswith("login.cgi"):
                 raise AirOSConnectionAuthenticationError("Session activation failed.")

344-355: Optional: Remove unnecessary return statement.

The return on line 354 is unnecessary since the function returns None implicitly and it's at the end of the function.

Simplify:

     async def login(self) -> None:
         """Login to AirOS device."""
         payload = {"username": self.username, "password": self.password}
         try:
             await self._request_json("POST", self._login_url, json_data=payload)
         except AirOSUrlNotFoundError:
             await self._login_v6()
         except AirOSConnectionSetupError as err:
             raise AirOSConnectionSetupError("Failed to login to AirOS device") from err
-        else:
-            return

Or if you prefer explicit returns, keep it but remove the else:

     async def login(self) -> None:
         """Login to AirOS device."""
         payload = {"username": self.username, "password": self.password}
         try:
             await self._request_json("POST", self._login_url, json_data=payload)
         except AirOSUrlNotFoundError:
             await self._login_v6()
         except AirOSConnectionSetupError as err:
             raise AirOSConnectionSetupError("Failed to login to AirOS device") from err
-        else:
-            return
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6feacb3 and 732fe25.

📒 Files selected for processing (1)
  • airos/base.py (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
airos/base.py (2)
tests/conftest.py (1)
  • base_url (18-20)
airos/exceptions.py (3)
  • AirOSDeviceConnectionError (24-25)
  • AirOSConnectionAuthenticationError (12-13)
  • AirOSUrlNotFoundError (44-45)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check commit
🔇 Additional comments (4)
airos/base.py (4)

58-59: LGTM: Sensible default for API version.

Setting api_version to 8 by default is appropriate, as it will be updated to 6 in the _login_v6() flow when needed.


95-95: LGTM: Clean centralization of login URLs.

The _login_urls list provides a clean way to check if a URL is a login endpoint, improving maintainability.


254-254: LGTM: Correct membership checks for login URLs.

The use of self._login_urls list for membership checks properly handles both v8 and v6 login endpoints.

Also applies to: 270-270


294-343: Good implementation of v6 XM login flow.

The three-step v6 login process is well-structured:

  1. Retrieve session cookie from login endpoint
  2. POST credentials with 302 redirect expectation
  3. Activate session via form URL

Error handling appropriately distinguishes connection errors from authentication failures, and the cookie handling is consistent with the v8 path.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
tests/test_stations6.py (1)

77-101: Consider adding explicit __aexit__ setup for async context managers.

While MagicMock may auto-mock the missing __aexit__, explicitly setting it makes the test more robust and clear:

Apply this pattern to each response mock:

 get_login_response = MagicMock()
 get_login_response.__aenter__.return_value = get_login_response
+get_login_response.__aexit__.return_value = AsyncMock(return_value=None)

Repeat for post_login_response and get_index_response.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 732fe25 and 0f52445.

📒 Files selected for processing (1)
  • tests/test_stations6.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/test_stations6.py (2)
airos/base.py (2)
  • status (356-380)
  • _login_v6 (294-342)
airos/airos6.py (1)
  • AirOS6 (17-82)
🪛 Gitleaks (8.28.0)
tests/test_stations6.py

[high] 116-116: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check commit
🔇 Additional comments (3)
tests/test_stations6.py (3)

7-7: LGTM!

The addition of MagicMock to the imports is appropriate for the new test function.


104-109: Verify host parameter format and use_ssl consistency.

The host parameter includes the scheme "http://192.168.1.3", but use_ssl defaults to True, which suggests HTTPS. Ensure this is intentional and that the base class correctly handles a host with an explicit scheme, or adjust the parameters for consistency:

 airos6_device = AirOS6(
-    host="http://192.168.1.3",
+    host="192.168.1.3",
     username="ubnt",
     password="ubnt",
     session=session,
+    use_ssl=False,
 )

111-117: LGTM! Test assertions align with the v6 login flow.

The test correctly validates:

  • Connection state after login
  • API version assignment
  • Auth cookie format
  • Expected number of HTTP requests

Note: The static analysis hint flagging line 116 is a false positive—this is a test assertion checking the cookie value, not an exposed secret.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
airos/base.py (1)

382-399: Content-Type/json mismatch in update_check(force=True).

You send JSON (json_data={"force": True}) but set form Content-Type (ct_form=True). Use ct_json=True for consistency and correctness.

-        if force:
-            return await self._request_json(
-                "POST",
-                self._update_check_url,
-                json_data={"force": True},
-                authenticated=True,
-                ct_form=True,
-            )
+        if force:
+            return await self._request_json(
+                "POST",
+                self._update_check_url,
+                json_data={"force": True},
+                authenticated=True,
+                ct_json=True,
+            )
♻️ Duplicate comments (3)
airos/base.py (3)

212-215: Cookie handling fix retained full cookie name.

Keeping the full AIROS_* cookie string in the header resolves the v6/v8 interoperability issue. LGTM.


222-230: Parsing Set-Cookie for AIROS_ and storing full pair is correct.*

This matches device behavior and prior feedback. LGTM.


82-85: Minor: endpoint URL consistency.

_v6_form_url is a path while others are absolute. It works (you prefix at use), but consider normalizing for consistency or add a clarifying comment.

🧹 Nitpick comments (4)
airos/model_map.py (1)

99-145: Good normalization; consider clarifying match semantics.

The strip() fix is spot on. The variable best_match_is_prefix is misleading since you’re using endswith(); consider renaming to avoid confusion and add a brief comment with an example (“devmodel is a suffix of the catalog name: 'LiteBeam 5AC' ⟶ 'airMAX LiteBeam 5AC'”).

tests/test_stations6.py (2)

67-71: Remove unused cookie setup.

This snippet isn’t used by assertions; drop it to reduce noise.

-    cookie = SimpleCookie()
-    cookie["session_id"] = "test-cookie"
-    cookie["AIROS_TOKEN"] = "abc123"

118-118: Gitleaks false positive: replace secret-looking test token.

The hardcoded "AIROS_ABC123=xyz789" can trigger secret scanners. The diff above switches to "AIROS_TEST=nonsecret". Alternatively, build the string from variables to avoid static patterns.

fixtures/airos_NanoStation_loco_M5_v6.3.16_XM_ap.json (1)

24-24: Consider adjusting cpuload to a more realistic baseline value.

The fixture specifies cpuload: 100.0, which represents 100% CPU utilization. For a typical test fixture representing normal device state, a lower baseline value (e.g., 15–40) would be more realistic and representative. If this represents a specific high-load test scenario, consider documenting it or creating a separate fixture variant.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0f52445 and 1cddc84.

📒 Files selected for processing (10)
  • CHANGELOG.md (1 hunks)
  • airos/base.py (9 hunks)
  • airos/model_map.py (3 hunks)
  • fixtures/airos_NanoStation_M5_sta_v6.3.16.json (1 hunks)
  • fixtures/airos_NanoStation_loco_M5_v6.3.16_XM_ap.json (1 hunks)
  • fixtures/airos_NanoStation_loco_M5_v6.3.16_XM_sta.json (1 hunks)
  • fixtures/userdata/NanoStation_loco_M5_v6.3.16_XM_ap.json (1 hunks)
  • fixtures/userdata/NanoStation_loco_M5_v6.3.16_XM_sta.json (1 hunks)
  • pyproject.toml (1 hunks)
  • tests/test_stations6.py (3 hunks)
✅ Files skipped from review due to trivial changes (4)
  • fixtures/airos_NanoStation_loco_M5_v6.3.16_XM_sta.json
  • fixtures/userdata/NanoStation_loco_M5_v6.3.16_XM_ap.json
  • fixtures/userdata/NanoStation_loco_M5_v6.3.16_XM_sta.json
  • CHANGELOG.md
🧰 Additional context used
🧬 Code graph analysis (2)
tests/test_stations6.py (2)
airos/base.py (2)
  • status (356-380)
  • _login_v6 (294-342)
airos/airos6.py (1)
  • AirOS6 (17-82)
airos/base.py (1)
airos/exceptions.py (3)
  • AirOSDeviceConnectionError (24-25)
  • AirOSConnectionAuthenticationError (12-13)
  • AirOSUrlNotFoundError (44-45)
🪛 Gitleaks (8.28.0)
tests/test_stations6.py

[high] 118-118: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check commit
🔇 Additional comments (4)
pyproject.toml (1)

7-7: Confirm pre-release intent and automation.

Version "0.5.7a9" is a pre-release (PEP 440). Ensure your publish/CI and consumers expect pre-releases; otherwise, pinning/upgrades may skip this version.

airos/model_map.py (1)

81-88: Public mappings look good.

Splitting SITE_MODELS and MANUAL_MODELS and merging into MODELS improves clarity and extensibility. No issues.

fixtures/airos_NanoStation_M5_sta_v6.3.16.json (1)

13-13: Fixture SKU correction matches mapper behavior.

"sku": "NSM5" aligns with the normalized "NanoStation M5 " -> NSM5 mapping. Good.

fixtures/airos_NanoStation_loco_M5_v6.3.16_XM_ap.json (1)

5-15: No action required - this is not a data inconsistency.

The fixture is correct. derived.mode represents the device's network topology configuration (point-to-point wireless link), while wireless.mode represents its operating mode (access point). These are two distinct concepts. Every similar fixture in the codebase (including ap-ptp, sta-ptp variants) consistently uses derived.mode: "point_to_point" with ptp: true, confirming this is the intended design pattern for devices operating in PTP topology.

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
airos/data.py (1)

607-609: Consider more robust frequency parsing.

The current parsing assumes a space between the number and "MHz" (e.g., "5180 MHz"). If the format varies (e.g., "5180MHz"), split()[0] might not work as expected.

Consider a more flexible approach:

 freq = d.get("frequency")
 if isinstance(freq, str) and "MHz" in freq:
-    d["frequency"] = int(freq.split()[0])
+    d["frequency"] = int(freq.replace("MHz", "").strip())

This handles both "5180 MHz" and "5180MHz" formats.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab562b6 and c705360.

📒 Files selected for processing (6)
  • CHANGELOG.md (1 hunks)
  • airos/data.py (5 hunks)
  • fixtures/airos_NanoStation_M5_sta_v6.3.16.json (4 hunks)
  • fixtures/airos_NanoStation_loco_M5_v6.3.16_XM_ap.json (1 hunks)
  • fixtures/airos_NanoStation_loco_M5_v6.3.16_XM_sta.json (1 hunks)
  • pyproject.toml (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • fixtures/airos_NanoStation_loco_M5_v6.3.16_XM_ap.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • pyproject.toml
  • fixtures/airos_NanoStation_loco_M5_v6.3.16_XM_sta.json
🧰 Additional context used
🪛 LanguageTool
CHANGELOG.md

[grammar] ~10-~10: Ensure spelling is correct
Context: ...XW already was successful) - Calculated cpuload on v6 if values available (to prevent r...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check commit
🔇 Additional comments (5)
fixtures/airos_NanoStation_M5_sta_v6.3.16.json (1)

1-165: LGTM! Fixture aligns with v6 data model.

The fixture data correctly reflects the v6 data structure changes:

  • CPU metrics (cpubusy, cputotal, cpuload) align with Host6 model
  • Numeric frequency (5180) and rates (216, 270) match Wireless6 type changes
  • Throughput object matches the derived field structure
airos/data.py (3)

249-263: LGTM! Safe cpuload calculation with proper guards.

The pre-deserialize hook correctly:

  • Validates both cpubusy and cputotal are numeric
  • Prevents division by zero with cputotal > 0 check
  • Computes percentage and rounds to 2 decimal places

574-596: LGTM! Type changes align with v6 numeric data format.

The field type updates (frequency, txrate, rxrate to int, and throughput addition) correctly reflect the v6 data structure. The pre-deserialize hook handles conversion from string formats.


611-618: LGTM! Throughput derivation handles type conversions safely.

The logic correctly:

  • Retrieves rxrate and txrate from the data
  • Uses int(float()) conversion to handle numeric strings and floats
  • Defaults to 0 when values are missing or falsy
  • Creates properly structured throughput dictionary
CHANGELOG.md (1)

5-12: LGTM! Changelog accurately documents v6 enhancements.

The version 0.5.7 entry clearly describes:

  • v6 XM model login support
  • CPU load calculation improvements
  • Frequency and rate handling for v6 firmware

Note: The static analysis warning about "cpuload" spelling is a false positive—it's valid technical terminology for CPU load metrics.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
airos/data.py (1)

43-103: MAC redaction can yield invalid MAC; redact last two octets explicitly.

Using the last 5 characters is brittle. Split by octets to retain “AA:BB”.

-                if isinstance(v, str) and (is_mac_address(v) or is_mac_address_mask(v)):
-                    # Redact only the last part of a MAC address to a dummy value
-                    redacted_d[k] = "00:11:22:33:" + v.replace("-", ":").upper()[-5:]
+                if isinstance(v, str) and (is_mac_address(v) or is_mac_address_mask(v)):
+                    # Keep last two octets; normalize separators
+                    norm = v.replace("-", ":").upper()
+                    parts = norm.split(":")
+                    tail = ":".join(parts[-2:]) if len(parts) >= 2 else "00:00"
+                    redacted_d[k] = f"00:11:22:33:{tail}"
🧹 Nitpick comments (4)
airos/helpers.py (2)

35-37: Docstring and naming drift.

Function returns fw_major, hostname, and mac — not just “major firmware version.” Update docstring for accuracy.

-    """Connect to a device and return the major firmware version."""
+    """Connect, authenticate, and return device info: fw_major, hostname, and mac."""

41-52: Log messages lump distinct failures.

AirOSDataMissingError isn’t auth; split messages for clarity.

-    except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
-        _LOGGER.exception("Authentication error connecting to device at %s", host)
+    except AirOSConnectionAuthenticationError:
+        _LOGGER.exception("Authentication error connecting to device at %s", host)
+        raise
+    except AirOSDataMissingError:
+        _LOGGER.exception("Device returned incomplete data at %s", host)
         raise
airos/base.py (2)

58-59: LGTM! Consider documenting this public attribute.

The api_version attribute provides useful version tracking, defaulting to 8 and updating to 6 after v6 XM login (line 372). Consider adding a docstring comment to clarify this is a public attribute that clients can inspect.


324-373: Three-step v6 XM login flow implemented correctly.

The login sequence (session cookie → POST credentials → activate session) follows the expected v6 XM authentication pattern. The 302 redirect check (line 358) and session activation verification (line 369) provide appropriate error detection.

Consider making the URL check at line 369 more robust:

-            if "login.cgi" in str(response.url):
+            if response.url.path and "login.cgi" in response.url.path:

This ensures you're checking the path component specifically rather than the entire URL string.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51d053e and 2e56fc8.

📒 Files selected for processing (21)
  • .github/workflows/verify.yml (1 hunks)
  • CHANGELOG.md (1 hunks)
  • airos/base.py (10 hunks)
  • airos/data.py (7 hunks)
  • airos/helpers.py (2 hunks)
  • fixtures/airos_LiteBeam5AC_ap-ptp_30mhz.json (1 hunks)
  • fixtures/airos_LiteBeam5AC_sta-ptp_30mhz.json (1 hunks)
  • fixtures/airos_NanoBeam_5AC_ap-ptmp_v8.7.18.json (1 hunks)
  • fixtures/airos_NanoStation_M5_sta_v6.3.16.json (5 hunks)
  • fixtures/airos_NanoStation_loco_M5_v6.3.16_XM_ap.json (1 hunks)
  • fixtures/airos_NanoStation_loco_M5_v6.3.16_XM_sta.json (1 hunks)
  • fixtures/airos_liteapgps_ap_ptmp_40mhz.json (1 hunks)
  • fixtures/airos_loco5ac_ap-ptp.json (1 hunks)
  • fixtures/airos_loco5ac_sta-ptp.json (1 hunks)
  • fixtures/airos_mocked_sta-ptmp.json (1 hunks)
  • fixtures/airos_nanobeam5ac_sta_ptmp_40mhz.json (1 hunks)
  • fixtures/airos_nanostation_ap-ptp_8718_missing_gps.json (1 hunks)
  • pyproject.toml (1 hunks)
  • tests/test_airos6.py (1 hunks)
  • tests/test_airos8.py (2 hunks)
  • tests/test_helpers.py (0 hunks)
💤 Files with no reviewable changes (1)
  • tests/test_helpers.py
✅ Files skipped from review due to trivial changes (2)
  • fixtures/airos_nanostation_ap-ptp_8718_missing_gps.json
  • tests/test_airos6.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • fixtures/airos_NanoStation_loco_M5_v6.3.16_XM_ap.json
  • pyproject.toml
🧰 Additional context used
🧬 Code graph analysis (2)
airos/base.py (2)
airos/airos6.py (1)
  • _derived_wireless_data (39-54)
airos/exceptions.py (5)
  • AirOSKeyDataMissingError (20-21)
  • AirOSDeviceConnectionError (24-25)
  • AirOSConnectionAuthenticationError (12-13)
  • AirOSUrlNotFoundError (44-45)
  • AirOSConnectionSetupError (8-9)
airos/helpers.py (3)
airos/airos8.py (1)
  • AirOS8 (11-30)
airos/exceptions.py (5)
  • AirOSConnectionAuthenticationError (12-13)
  • AirOSConnectionSetupError (8-9)
  • AirOSDataMissingError (16-17)
  • AirOSDeviceConnectionError (24-25)
  • AirOSKeyDataMissingError (20-21)
airos/base.py (4)
  • login (374-384)
  • raw_status (386-388)
  • get_fw_major (184-190)
  • get_mac (193-220)
🪛 LanguageTool
CHANGELOG.md

[grammar] ~10-~10: Ensure spelling is correct
Context: ...XW already was successful) - Calculated cpuload on v6 if values available (to prevent r...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🔇 Additional comments (29)
.github/workflows/verify.yml (2)

177-177: Verify the rationale for lowering the coverage threshold.

The required coverage threshold has been reduced from 85% to 80%. While the enriched summary indicates this aligns with PR activity (new tests/fixtures and removal of test_helpers.py), this represents a relaxation of code quality requirements.

Please clarify:

  • Is this change intentional because new v6 XM code cannot meet the 85% threshold?
  • Or is this a deliberate decision to accept a lower but still-adequate threshold?
  • Has overall coverage metrics been assessed with the new changes?

9-14: Workflow trigger configuration note.

The pull_request trigger is commented out (line 14), meaning this workflow won't automatically run on pull requests—only on schedule, manual dispatch, or direct pushes. If this PR is intended to undergo CI validation, consider whether the trigger should be enabled for pull request events.

fixtures/airos_NanoStation_M5_sta_v6.3.16.json (4)

147-147: All data model changes are properly supported—no breaking changes detected.

The verification confirms the fixture is fully compatible:

  • frequency (int): Data model defines it as int; parsing code explicitly handles both string format "5180 MHz" and numeric format 5180
  • throughput (structured object): Data model defines it as Throughput type; parsing code transforms rxrate/txrate strings into numeric rx/tx values
  • New fields (fw_major, ieeemode, etc.): All defined in data models and present in the fixture
  • The fixture deserializes correctly with expected types

The original concerns about type consistency and missing model support have been verified as non-issues—the codebase already handles these changes.


25-27: CPU metrics are properly defined and handled—no issues found.

The verification confirms that cpubusy, cpuload, and cputotal are all properly defined in the Host data model (airos/data.py, lines 249–251) with appropriate type annotations. These fields are also correctly handled during deserialization via the __pre_deserialize__ hook (lines 260–263), which validates the fields and calculates cpuload from cpubusy and cputotal when both are present. The fixture data structure aligns with the model definition.


14-14: SKU mapping verified as correct; no action required.

The fixture's devmodel "NanoStation M5 " will correctly resolve to SKU "NSM5" through the mapper's fuzzy matching logic (suffix matching after lowercase comparison). The model_map.py contains the proper entry "airMAX NanoStation M5": "NSM5", and LocoM5 remains correctly assigned to loco variants only. No dangling references to the old SKU were found.


7-7: No issues found - fw_major field is properly defined and used.

The fw_major field is defined in the Derived class (airos/data.py:733) as fw_major: int | None = None. It is correctly set during response processing in airos/base.py:177 via the get_fw_major() method, and the fixture data structure matches the model definition.

fixtures/airos_liteapgps_ap_ptmp_40mhz.json (1)

14-15: Consistent fw_major.

Value 8 matches host.fwversion=v8.7.18.

fixtures/airos_loco5ac_sta-ptp.json (1)

14-15: LGTM.

fw_major=8 aligns with v8.7.17.

fixtures/airos_NanoBeam_5AC_ap-ptmp_v8.7.18.json (1)

14-15: LGTM.

fw_major=8 matches host v8.7.18; remote v6.x doesn’t affect host-derived value.

fixtures/airos_loco5ac_ap-ptp.json (1)

14-15: LGTM.

fw_major=8 matches host major.

fixtures/airos_LiteBeam5AC_sta-ptp_30mhz.json (1)

14-15: LGTM.

Value 8 matches v8.7.11.

airos/data.py (2)

145-151: Enum additions look fine.

New IeeeMode members (_11ACVHT60, _11ACVHT50, _11NAHT40MINUS, _11NAHT40PLUS) align with observed device reports.


250-264: Nice: derive cpuload from cpubusy/cputotal.

This prevents bogus near-100% cpuloads on XM.

fixtures/airos_mocked_sta-ptmp.json (1)

14-15: Verification complete—all fixtures' derived.fw_major correctly aligns with host.fwversion major version.

Checked 13 fixtures containing both fields. All matches confirmed:

  • fixtures/airos_mocked_sta-ptmp.json: host.fwversion=v8.7.17 → derived.fw_major=8 ✓
  • All other fixtures similarly verified with correct alignment (v8.x.x→8, v6.x.x→6)

No mismatches found across the codebase.

fixtures/airos_nanobeam5ac_sta_ptmp_40mhz.json (1)

14-14: LGTM!

The fw_major field correctly reflects the major version from fwversion: "v8.7.18" at line 46.

fixtures/airos_LiteBeam5AC_ap-ptp_30mhz.json (1)

14-14: LGTM!

The fw_major field correctly reflects the major version from fwversion: "v8.7.18" at line 37.

tests/test_airos8.py (2)

126-131: LGTM!

The addition of fwversion is necessary to satisfy the new get_fw_major helper introduced in airos/base.py (lines 184-190), which is now called during derived_data processing.


260-260: LGTM!

The addition of fwversion ensures the test fixture includes required fields for derived data processing.

CHANGELOG.md (1)

5-16: LGTM!

The changelog accurately documents the v6 XM login support, derived data enhancements, and firmware detection improvements introduced in this PR.

fixtures/airos_NanoStation_loco_M5_v6.3.16_XM_sta.json (1)

1-154: LGTM!

The v6 XM fixture is well-structured and demonstrates the key v6 features documented in the changelog:

  • Correctly derived fw_major: 6 from fwversion: "v6.3.16"
  • antenna_gain: 13 properly extracted from antenna: "Built in - 13 dBi"
  • Both ieeemode and opmode fields present, enabling proper v6-to-v8 mapping
  • throughput object with rx/tx rates included
airos/base.py (9)

97-121: LGTM!

Renaming to _derived_wireless_data with an underscore prefix correctly signals this is an internal helper method. The @staticmethod decorator is appropriate as the method doesn't use instance state.


183-191: LGTM!

The get_fw_major helper correctly extracts the major version and handles edge cases appropriately with proper error handling.


192-220: LGTM!

The get_mac helper correctly prioritizes interfaces (br0 → eth0 → ath0) and safely falls back to the first interface. The early return at line 197 ensures interfaces[0] access at lines 217-218 is safe.


222-225: LGTM!

Converting derived_data to a classmethod enables proper polymorphism, allowing subclasses (like AirOS6) to override _derived_wireless_data while sharing the common _derived_data_helper logic.


242-245: LGTM!

Cookie handling correctly preserves the full cookie name/value pair (e.g., "AIROS_XXX=value") as noted in past review feedback.


374-385: LGTM!

The automatic fallback from v8 to v6 login (lines 379-380) provides seamless compatibility, allowing the same client code to work with both firmware versions without explicit version detection.


386-389: LGTM!

The raw_status method provides a useful way to retrieve unprocessed device data, which can be helpful for debugging or when derived data processing isn't needed.


284-286: LGTM!

Updating the connected check to use _login_urls list (line 284) correctly allows both v8 and v6 login endpoints to bypass the connection requirement.


165-178: LGTM!

Refactoring to use static helpers (get_mac and get_fw_major) improves code modularity and testability while preserving the original logic.

@CoMPaTech
Copy link
Owner Author

@coderabbitai please update the summary and your walkthrough based on all information available, note that this will be 0.6.0 after testing (and no longer 0.6.0) so 0.5.7a14 will become 0.6.0. As well as a full review of the PR

@coderabbitai
Copy link

coderabbitai bot commented Oct 22, 2025

@CoMPaTech, I'll update the summary and walkthrough to reflect that this will be version 0.6.0, and provide a comprehensive review of the PR.

Key Observations

This PR introduces significant enhancements to support v6 firmware XM models with a different login path, along with several architectural improvements:

Version Note: The version should be updated from 0.5.7a13 to 0.6.0 in:

  • pyproject.toml
  • CHANGELOG.md (change the 0.5.7 entry to 0.6.0)

Major Changes:

  1. Breaking Changes:

    • derived_data() converted from instance method to classmethod
    • New public attribute api_version on AirOS class
    • Cookie header format changed in authentication
  2. v6 XM Login Support:

    • New _login_v6() method for XM firmware login flow
    • Handles session cookie management, 302 redirects, and form activation
    • Fallback mechanism from v8 login to v6 login on URL not found
  3. Data Model Enhancements:

    • Extended IeeeMode enum with new values (_11ACVHT60, _11ACVHT50, _11NAHT40MINUS, _11NAHT40PLUS)
    • Host6 now includes cputotal and cpubusy for accurate CPU load calculation
    • Wireless6 expanded with throughput, ieeemode, and antenna_gain fields
    • Derived class includes fw_major field
  4. Model Mapping Refactor:

    • Split into SITE_MODELS and MANUAL_MODELS
    • Added "LiteAP GPS" and "NanoStation loco M5" manual entries
  5. Test Coverage:

    • New test for v6 login flow
    • Multiple XM device fixtures added
    • Coverage threshold adjusted from 85% to 80%

The scope of changes justifies a minor version bump to 0.6.0 given the breaking API changes.

Note: This will be version 0.6.0 after testing. The version in pyproject.toml should be updated from 0.5.7a13 to 0.6.0, and the CHANGELOG.md entry for 0.5.7 should be changed to 0.6.0.

✅ Actions performed

Summary regeneration triggered.

@sonarqubecloud
Copy link

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
CHANGELOG.md (2)

12-12: Fix awkward phrasing and grammar on line 12.

The phrase "XW already was successful" is grammatically awkward. Consider rephrasing to something like "XW was already supported" for improved clarity.

Apply this diff:

-Support for v6 firmware XM models using a different login path (XW already was successful)
+Support for v6 firmware XM models using a different login path (XW was already supported)

14-14: Clarify the throughput availability statement on line 14.

The phrasing states throughput is "available" but then "not yet available," which is contradictory. Clarify whether throughput is exposed in the current release or if it requires future work.

Consider rephrasing to something like:

-Added tx/rx rates for v6 as capacity (throughput is available in v6 web interface, but probably through counters, so not yet available)
+Added tx/rx rates for v6 as capacity (throughput visible in v6 web UI but not yet exposed via API)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a933cdf and af1eb08.

📒 Files selected for processing (2)
  • CHANGELOG.md (1 hunks)
  • pyproject.toml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pyproject.toml
🧰 Additional context used
🪛 LanguageTool
CHANGELOG.md

[grammar] ~12-~12: Ensure spelling is correct
Context: ...XW already was successful) - Calculated cpuload on v6 if values available (to prevent r...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check commit
🔇 Additional comments (1)
CHANGELOG.md (1)

5-17: Changelog properly documents the 0.6.0 release.

The changelog comprehensively covers the major features and improvements introduced in this PR: v6 XM login support, CPU load calculation, frequency handling, antenna derivation, and internal enhancements. The structure, acknowledgments, and organization align well with the PR objectives.

@CoMPaTech CoMPaTech merged commit ffbad03 into main Oct 23, 2025
13 checks passed
@CoMPaTech CoMPaTech deleted the v6loginfix3 branch October 23, 2025 11:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants