Skip to content

Enhance CT002 consumer discovery with ARP lookup and device branding#303

Merged
tomquist merged 3 commits into
developfrom
claude/update-device-discovery-Rn7iE
Apr 6, 2026
Merged

Enhance CT002 consumer discovery with ARP lookup and device branding#303
tomquist merged 3 commits into
developfrom
claude/update-device-discovery-Rn7iE

Conversation

@tomquist
Copy link
Copy Markdown
Owner

@tomquist tomquist commented Apr 6, 2026

Summary

This PR improves the MQTT Home Assistant discovery for CT002 consumers by adding network MAC address resolution via ARP lookup and updating device branding from "HAME Energy" to "AstraMeter" with Marstek as the manufacturer.

Key Changes

  • ARP Lookup Implementation: Added _arp_lookup() async function that queries /proc/net/arp to resolve IP addresses to MAC addresses. Gracefully handles missing files and invalid entries.

  • Device Branding Updates:

    • Changed manufacturer from "HAME Energy" to "Marstek"
    • Updated device name prefix from "HAME Energy" to "AstraMeter Consumer"
    • Changed device identifier format from hame_energy_* to astrameter_consumer_*
    • Renamed model_id field to model in device info
  • Connection Information:

    • Added Bluetooth connection entry when consumer_id is a valid 12-character hex MAC address (formatted as AA:BB:CC:DD:EE:FF)
    • Added optional MAC connection entry when network_mac parameter is provided (resolved via ARP lookup from battery_ip)
    • Connections are only included in device info when at least one connection is available
  • Enhanced Discovery Parameters:

    • build_ct002_consumer_discovery() now accepts optional network_mac parameter
    • _handle_ct002_event() performs ARP lookup on battery_ip field and passes result to discovery builder

Testing

Added comprehensive test coverage for:

  • Device discovery with and without device_type
  • MAC address validation and connection formatting
  • ARP lookup success, not found, and file missing scenarios

https://claude.ai/code/session_01RTxVw1HWeMwgaEV1C7Bb5w

Summary by CodeRabbit

  • New Features

    • Enhanced device connection metadata with Bluetooth, network MAC, and IP address information.
    • Improved network device discovery with automatic MAC address resolution capabilities.
  • Updates

    • Updated device branding and identification information for better clarity.

Rebrand device info: identifiers use astrameter_consumer_ prefix, manufacturer
is Marstek, name is AstraMeter Consumer, model field replaces model_id.
Add Bluetooth MAC connections (when consumer_id is a valid 12-char hex MAC)
and network MAC via best-effort ARP lookup from /proc/net/arp.

https://claude.ai/code/session_01RTxVw1HWeMwgaEV1C7Bb5w
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 6, 2026

Warning

Rate limit exceeded

@tomquist has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 15 minutes and 44 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 15 minutes and 44 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d9766157-750f-44e9-8e39-f696f3e4c371

📥 Commits

Reviewing files that changed from the base of the PR and between e98e2f1 and 79a65c1.

📒 Files selected for processing (1)
  • src/b2500_meter/mqtt_insights/service.py

Walkthrough

The changes modify the CT002 consumer Home Assistant MQTT discovery process to use updated branding (AstraMeter/Marstek), employ stable device identifiers, and augment device metadata with network connectivity information retrieved via ARP table lookup.

Changes

Cohort / File(s) Summary
Discovery Function Enhancement
src/b2500_meter/mqtt_insights/discovery.py
Added optional network_mac and battery_ip parameters to build_ct002_consumer_discovery. Updated device identification from array format to stable identifier astrameter_consumer_<mac_slug>. Changed branding from HAME Energy to AstraMeter Consumer and manufacturer to Marstek. Added conditional connections field for Bluetooth MAC and optional network connectivity.
Service Integration
src/b2500_meter/mqtt_insights/service.py
Added async helper function _arp_lookup to read /proc/net/arp and resolve IP addresses to MAC addresses. Modified _handle_ct002_event to extract battery_ip from event data, perform ARP lookup to obtain network_mac, and pass both to discovery function.
Test Coverage
src/b2500_meter/mqtt_insights/mqtt_insights_test.py
Extended discovery tests with model-specific assertions, device-type omission handling, and MAC validation. Added three new tests for _arp_lookup: successful lookup, IP not found, and file missing scenarios.

Sequence Diagram

sequenceDiagram
    participant Event as CT002 Event
    participant Service as Service
    participant ARP as ARP Lookup
    participant Discovery as Discovery Builder
    participant HA as Home Assistant MQTT

    Event->>Service: CT002 consumer event with battery_ip
    Service->>Service: Extract battery_ip from event
    Service->>ARP: _arp_lookup(battery_ip)
    ARP->>ARP: Read /proc/net/arp
    ARP-->>Service: Return MAC address
    Service->>Discovery: build_ct002_consumer_discovery(network_mac, battery_ip, ...)
    Discovery->>Discovery: Build device metadata with connections
    Discovery-->>Service: Discovery payload
    Service->>HA: Publish MQTT discovery
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 accurately describes the main changes: enhancing CT002 consumer discovery with ARP lookup functionality and device branding updates (AstraMeter/Marstek rebranding).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/update-device-discovery-Rn7iE

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.

Keep model_id instead of model for device type. Add battery_ip as an
"ip" connection type in the device discovery payload.

https://claude.ai/code/session_01RTxVw1HWeMwgaEV1C7Bb5w
@tomquist tomquist marked this pull request as ready for review April 6, 2026 19:24
@tomquist
Copy link
Copy Markdown
Owner Author

tomquist commented Apr 6, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 6, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@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

🧹 Nitpick comments (1)
src/b2500_meter/mqtt_insights/service.py (1)

349-360: Retry the MAC backfill after first-discovery misses.

battery_ip is emitted on every CT002 event in src/b2500_meter/ct002/ct002.py:585-625, but _arp_lookup() only runs inside the first-discovery branch. If that first lookup returns "", this consumer never gets its network_mac connection until reconnect/remove. Consider retrying until a valid MAC is found and republishing discovery once it is.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/b2500_meter/mqtt_insights/service.py` around lines 349 - 360, The code
only calls _arp_lookup when handling the first-discovery path, so if that lookup
returns an empty network_mac the consumer never gets updated later; modify the
consumer processing (where build_ct002_consumer_discovery is used) to retry
_arp_lookup whenever battery_ip is present and network_mac is empty (e.g., on
subsequent CT002 events or on a short retry/backoff) and, once a valid MAC is
found, republish discovery via build_ct002_consumer_discovery with the new
network_mac so the consumer gets updated; reference the _arp_lookup function for
the lookup and build_ct002_consumer_discovery for republishing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/b2500_meter/mqtt_insights/discovery.py`:
- Around line 189-203: The code currently normalizes consumer_id into mac_slug
(via _sanitize_id(...).lower().replace("-", "").replace("_", "")) which
collapses distinct non-MAC IDs; change the logic so you only strip
hyphens/underscores when the value is a verified 12-hex MAC: compute a sanitized
value (e.g., raw_slug = _sanitize_id(consumer_id).lower()), then if
re.fullmatch(r"[0-9a-f]{12}", raw_slug.replace("-", "").replace("_", "")) treat
it as a MAC (build mac_slug by removing -/_ and use it for
identifiers/connections and BT MAC conversion), otherwise use the original
node_id (or raw_slug without stripping) as the unique identifier in
device_info["identifiers"] and name to avoid merging different non-MAC consumer
IDs; update references to mac_slug, device_info, and BT connection logic
accordingly.

---

Nitpick comments:
In `@src/b2500_meter/mqtt_insights/service.py`:
- Around line 349-360: The code only calls _arp_lookup when handling the
first-discovery path, so if that lookup returns an empty network_mac the
consumer never gets updated later; modify the consumer processing (where
build_ct002_consumer_discovery is used) to retry _arp_lookup whenever battery_ip
is present and network_mac is empty (e.g., on subsequent CT002 events or on a
short retry/backoff) and, once a valid MAC is found, republish discovery via
build_ct002_consumer_discovery with the new network_mac so the consumer gets
updated; reference the _arp_lookup function for the lookup and
build_ct002_consumer_discovery for republishing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e45e33e8-be1d-4dab-baaf-4932dff48f5c

📥 Commits

Reviewing files that changed from the base of the PR and between 1e3730f and e98e2f1.

📒 Files selected for processing (3)
  • src/b2500_meter/mqtt_insights/discovery.py
  • src/b2500_meter/mqtt_insights/mqtt_insights_test.py
  • src/b2500_meter/mqtt_insights/service.py

Comment on lines 189 to +203
mac_slug = _sanitize_id(consumer_id).lower().replace("-", "").replace("_", "")
identifiers = [node_id, f"hame_energy_{mac_slug}"]

device_info: dict = {
"identifiers": identifiers,
"name": f"HAME Energy {device_type} {mac_slug}"
"identifiers": [f"astrameter_consumer_{mac_slug}"],
"name": f"AstraMeter Consumer {device_type} {mac_slug}"
if device_type
else f"HAME Energy {mac_slug}",
"manufacturer": "HAME Energy",
else f"AstraMeter Consumer {mac_slug}",
"manufacturer": "Marstek",
}
connections: list[list[str]] = []
if re.fullmatch(r"[0-9a-f]{12}", mac_slug):
bt_mac = ":".join(
mac_slug[i : i + 2] for i in range(0, len(mac_slug), 2)
).upper()
connections.append(["bluetooth", bt_mac])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve uniqueness for non-MAC consumer IDs.

mac_slug becomes the only device identifier here, but the normalization strips - and _ even when consumer_id is not actually a MAC. Distinct IDs like abc-def and abc_def now collapse to the same astrameter_consumer_abcdef, so Home Assistant will merge different consumers into one device. Keep the stripped form only for verified MACs, and fall back to node_id otherwise.

Suggested fix
-    mac_slug = _sanitize_id(consumer_id).lower().replace("-", "").replace("_", "")
+    safe_consumer_id = _sanitize_id(consumer_id).lower()
+    mac_slug = safe_consumer_id.replace("-", "").replace("_", "")
+    is_mac = re.fullmatch(r"[0-9a-f]{12}", mac_slug) is not None
+    consumer_suffix = mac_slug if is_mac else safe_consumer_id

     device_info: dict = {
-        "identifiers": [f"astrameter_consumer_{mac_slug}"],
-        "name": f"AstraMeter Consumer {device_type} {mac_slug}"
+        "identifiers": (
+            [f"astrameter_consumer_{mac_slug}", node_id] if is_mac else [node_id]
+        ),
+        "name": f"AstraMeter Consumer {device_type} {consumer_suffix}"
         if device_type
-        else f"AstraMeter Consumer {mac_slug}",
+        else f"AstraMeter Consumer {consumer_suffix}",
         "manufacturer": "Marstek",
     }
     connections: list[list[str]] = []
-    if re.fullmatch(r"[0-9a-f]{12}", mac_slug):
+    if is_mac:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/b2500_meter/mqtt_insights/discovery.py` around lines 189 - 203, The code
currently normalizes consumer_id into mac_slug (via
_sanitize_id(...).lower().replace("-", "").replace("_", "")) which collapses
distinct non-MAC IDs; change the logic so you only strip hyphens/underscores
when the value is a verified 12-hex MAC: compute a sanitized value (e.g.,
raw_slug = _sanitize_id(consumer_id).lower()), then if
re.fullmatch(r"[0-9a-f]{12}", raw_slug.replace("-", "").replace("_", "")) treat
it as a MAC (build mac_slug by removing -/_ and use it for
identifiers/connections and BT MAC conversion), otherwise use the original
node_id (or raw_slug without stripping) as the unique identifier in
device_info["identifiers"] and name to avoid merging different non-MAC consumer
IDs; update references to mac_slug, device_info, and BT connection logic
accordingly.

Track consumers with missing network MAC in _pending_arp set. On
subsequent CT002 events, retry the ARP lookup and republish discovery
once a valid MAC is resolved.

https://claude.ai/code/session_01RTxVw1HWeMwgaEV1C7Bb5w
@tomquist tomquist merged commit a6aa6c9 into develop Apr 6, 2026
13 checks passed
@tomquist tomquist deleted the claude/update-device-discovery-Rn7iE branch April 6, 2026 19:50
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