Skip to content

Conversation

@bouwew
Copy link
Contributor

@bouwew bouwew commented Dec 21, 2025

Changes:

  • Replace all xml .attrib[] by .get()
  • Optimize _get_groups() function
  • _collect_group_sensors(): correct to group_id

Summary by CodeRabbit

  • Bug Fixes
    • Improved resilience by safely handling missing IDs and preset fields, adding guards to prevent errors when attributes are absent.
  • Refactor
    • Streamlined gateway entity and group processing for more consistent discovery.
    • Consolidated attribute retrieval and selection logic; refined heater/central selection to prefer the first valid entry with actuators.
  • Chores
    • Updated changelog to include the latest PR reference.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 21, 2025

Warning

Rate limit exceeded

@bouwew has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 3 minutes and 39 seconds before requesting another review.

⌛ 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.

📥 Commits

Reviewing files that changed from the base of the PR and between b18d22c and 1f956c7.

📒 Files selected for processing (4)
  • plugwise/common.py (2 hunks)
  • plugwise/legacy/helper.py (5 hunks)
  • plugwise/legacy/smile.py (4 hunks)
  • plugwise/util.py (1 hunks)

Walkthrough

The PR replaces many direct XML attribute accesses (.attrib["id"]) with safe .get("id") calls across the codebase and refactors _get_groups in plugwise/common.py to return None while populating self.gw_entities in-place instead of returning a dict.

Changes

Cohort / File(s) Summary
Core method refactor
plugwise/common.py
_get_groups signature changed to return None; it now inserts group entries directly into self.gw_entities and no longer returns a groups mapping. Replaced .attrib["id"] with .get("id") in _get_groups and updated _get_module_data to use .get("id").
Helpers — safe attribute access
plugwise/helper.py, plugwise/.../helper.py
Replaced many XML attribute lookups from .attrib["id"] to .get("id") for appliances, locations, groups, rules, presets, thermostats, measurements and scheduling. Minor parameter rename entity_idgroup_id in some helpers and call sites.
Legacy smile / Smile integration
plugwise/legacy/smile.py, plugwise/smile.py
Call sites updated to no longer expect a dict return from _get_groups(); group merging logic adjusted. Multiple ID extractions changed to .get("id") in set_preset, set_schedule_state, set_number, set_switch_state, and _set_groupswitch_member_state.
Legacy helper updates
plugwise/legacy/helper.py
Safe .get("id") access applied to appliance, location, thermostat, preset and scheduling parsing; preset collection now validates icon and temperature via .get(...).
Utilities hardening
plugwise/util.py
check_heater_central reworked to collect candidate heater entries (with id/name guards and actuator checks), pick first valid with actuators, and return None early when none found.
Changelog
CHANGELOG.md
Added PR reference #839 to the code optimizations line.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Attention points:
    • Confirm all callers of _get_groups no longer rely on a returned dict and instead expect self.gw_entities to be populated.
    • Verify .get("id") usage is handled where None is possible (guards/early returns).
    • Check consistency of renamed helper parameters (group_id) across call sites and tests.

Possibly related PRs

Suggested reviewers

  • CoMPaTech

Poem

🐰 I hopped through XML fields in the night,
Swapped brittle attribs for get's gentle light.
Groups now nest where the gateway sings,
No KeyErrors fluttering their wings.
A tidy burrow of safer strings. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Optimize 3' is vague and non-descriptive; it provides no meaningful information about the specific changes (XML attribute access improvements, _get_groups refactoring, or group_id correction). Use a more descriptive title that conveys the main changes, such as 'Replace XML attribute access with .get() for safer error handling' or 'Refactor group handling and improve XML attribute access safety'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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 Dec 21, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (c005c19) to head (1f956c7).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #839   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           21        21           
  Lines         3435      3433    -2     
=========================================
- Hits          3435      3433    -2     

☔ 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
Contributor

@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

🧹 Nitpick comments (2)
plugwise/legacy/helper.py (1)

414-414: Consider adding a None check for rule_id.

While the .get("id") change is safer than direct attribute access, the rule_id is used in string formatting on line 424 and comparisons. If None, this could lead to unexpected behavior. Consider adding a None check similar to the pattern used in plugwise/legacy/smile.py line 196.

🔎 Suggested defensive check
         if (result := search.find("./rule[name='Thermostat schedule']")) is not None:
             name = "Thermostat schedule"
             rule_id = result.get("id")
+            if rule_id is None:
+                return available, selected
plugwise/smile.py (1)

428-432: Consider adding a None check for switch_id.

The switch_id obtained from .get("id") is used in URI construction on line 433. While line 431 appears to always execute for single-item actuators, if item.get("id") returns None, the URI would be malformed. Consider adding a None check before line 433.

🔎 Suggested defensive check
             else:  # actuators with a single item like relay_functionality
                 switch_id = item.get("id")
+                if switch_id is None:
+                    return current_state  # or raise appropriate error
 
         uri = f"{APPLIANCES};id={appl_id}/{switch.device};id={switch_id}"
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c005c19 and a73ff8e.

📒 Files selected for processing (6)
  • plugwise/common.py (2 hunks)
  • plugwise/helper.py (11 hunks)
  • plugwise/legacy/helper.py (5 hunks)
  • plugwise/legacy/smile.py (4 hunks)
  • plugwise/smile.py (5 hunks)
  • plugwise/util.py (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2024-12-22T09:37:24.648Z
Learnt from: bouwew
Repo: plugwise/python-plugwise PR: 678
File: plugwise/legacy/helper.py:69-69
Timestamp: 2024-12-22T09:37:24.648Z
Learning: The code in plugwise/legacy/helper.py is fully separated from plugwise/helper.py. The user indicates that the problem signaled about the mismatch of the variable name (_home_location vs _home_loc_id) does not actually apply.

Applied to files:

  • plugwise/helper.py
  • plugwise/legacy/helper.py
📚 Learning: 2025-10-11T14:05:29.022Z
Learnt from: bouwew
Repo: plugwise/python-plugwise PR: 797
File: fixtures/adam_anna_new_2/data.json:1-304
Timestamp: 2025-10-11T14:05:29.022Z
Learning: In Plugwise fixture data (data.json files), location IDs referenced in device entries don't always need corresponding top-level location objects. Only climate zones (ThermoZone entries with dev_class "climate") require full top-level definitions with thermostats, schedules, and presets. Simple organizational locations that just group devices (like single plugs) can be referenced without top-level entries.

Applied to files:

  • plugwise/helper.py
📚 Learning: 2025-12-21T10:56:14.461Z
Learnt from: bouwew
Repo: plugwise/python-plugwise PR: 838
File: plugwise/helper.py:210-211
Timestamp: 2025-12-21T10:56:14.461Z
Learning: In plugwise/helper.py, when parsing location XML elements, the `type` element is always present for a location, so null checks are not required when accessing `location.find("type").text`.

Applied to files:

  • plugwise/legacy/helper.py
🧬 Code graph analysis (2)
plugwise/smile.py (1)
plugwise/common.py (1)
  • _get_groups (191-219)
plugwise/legacy/smile.py (1)
plugwise/common.py (1)
  • _get_groups (191-219)
🔇 Additional comments (18)
plugwise/legacy/smile.py (3)

85-85: LGTM! Correctly aligns with the new _get_groups() signature.

The change to call _get_groups() without using its return value is consistent with the refactoring in plugwise/common.py, where _get_groups() now returns None and populates self.gw_entities directly.


193-199: Good defensive programming with None check.

The code properly checks if schedule_rule_id is None before using it, which handles the case where the rule element might not have an "id" attribute. This is the correct pattern when using .get("id").


206-206: Verify that template elements always have an "id" attribute.

Similar to line 161, if the template element doesn't have an "id" attribute, template_id will be None and the XML construction on line 212 will include id='None'. Consider adding a None check or ensuring the attribute is always present.

plugwise/legacy/helper.py (3)

151-162: Verify that location elements always have an "id" attribute.

The loc_id from .get("id") is used as a dictionary key in self._loc_data on line 162 and as self._home_loc_id on line 160. If None, this could cause unexpected behavior in location lookups and home location identification.


435-435: Verify that appliance elements always have an "id" attribute.

The appliance_id from .get("id") is used in the URI construction on line 436. If None, the URI would be malformed as {APPLIANCES};id=None/thermostat.


110-110: Add validation to ensure appliance elements have the "id" attribute before using as entity_id.

While all test fixtures consistently include id attributes on appliances, the code should not silently accept None for entity_id. Since line 110 uses appliance.get("id") which returns None if missing, and entity_id is subsequently used as a dictionary key in self._create_gw_entities() on line 137 and compared against self.heater_id on line 134, missing or None values would cause issues. Add explicit validation rather than relying on defensive .get() alone.

plugwise/helper.py (6)

120-122: Verify that appliance elements always have an "id" attribute.

The entity_id is a critical identifier used throughout the entity lifecycle. If .get("id") returns None, it would cause issues when:

  • Creating gateway entities
  • Using as dictionary keys in self.gw_entities
  • Performing entity lookups

272-272: Verify that gateway appliance elements always have an "id" attribute.

The self._gateway_id is a fundamental identifier for the gateway entity. If None, it would break gateway identification and various gateway-related operations throughout the codebase.


393-409: Good parameter rename for clarity.

Renaming the parameter from entity_id to group_id on line 396 improves code readability by making it clear that this function operates on group entities specifically.


505-508: Verify that notification elements always have an "id" attribute.

If .get("id") returns None, it will be used as a dictionary key in self._notifications on line 508. While this won't crash, it could lead to confusing notification tracking.


1005-1005: Verify that thermostat_functionality elements always have an "id" attribute.

The thermostat_functionality_id from .get("id") is used in URI construction on line 1007. If None, the resulting URI would be malformed: {LOCATIONS};id={loc_id}/thermostat;id=None.


906-909: The review comment describes code that does not match the actual repository.

The actual code at lines 906-909 uses directive.attrib["preset"], not directive.get("preset"). The concern about None being used as a dictionary key does not apply to the current implementation, as .attrib["preset"] will raise a KeyError if the attribute is missing, providing fail-fast behavior rather than silent dictionary key pollution.

Likely an incorrect or invalid review comment.

plugwise/common.py (2)

266-266: Verify that service elements always have an "id" attribute.

The link_id from .get("id") is used in XPath construction on line 267 for module lookup. If None, the XPath would be malformed and the module lookup would fail.


191-219: Consider adding explicit validation for group id attributes.

The concern about missing "id" attributes is theoretically valid. While all examined fixtures (40+ files) consistently include id attributes on group and appliance elements, the code lacks explicit safeguards. On line 208, item.get("id") in self.gw_entities implicitly prevents None members from being added since None won't exist as a key. However, on line 202, group_id = group.get("id") could return None and be used as a dictionary key on line 212 if a group lacked an id attribute.

To make this more robust, consider explicitly validating that both group_id and appliance ids are not None before processing, rather than relying on implicit safeguards.

plugwise/smile.py (4)

116-116: LGTM! Correctly aligns with the refactored _get_groups() method.

The change to call _get_groups() without capturing or using its return value is consistent with the method signature change in plugwise/common.py, where the method now returns None and populates self.gw_entities directly.


179-182: Verify that thermostat_functionality elements always have an "id" attribute.

The thermostat_id from .get("id") is checked for None on line 181 (good defensive programming!), but verify that under normal circumstances, thermostat_functionality elements should always have an "id" attribute. If they don't, the error message on line 182 will be raised, which might not accurately reflect the root cause.


359-360: Verify that template elements always have an "id" attribute.

The template_id from .get("id") is used directly in XML construction on line 360. If None, the template XML would contain id="None", which might not be the intended behavior for legacy Anna devices.


457-457: Verify that functionality elements always have an "id" attribute.

The switch_id from .get("id") is used in URI construction on line 458. If None, the URI would be malformed. Consider adding a None check or verifying that the XML structure guarantees the presence of this attribute.

Copy link
Contributor

@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 (1)
plugwise/legacy/smile.py (1)

161-163: Critical: Still crashes with AttributeError if find() returns None.

If find(locator) returns None, calling .get("id") will raise AttributeError before the None check executes. The walrus operator assigns the result of .get("id"), but the method call happens on the potentially-None return value. Follow the pattern from set_schedule_state() (lines 192-196): validate find() returns an element before accessing attributes.

🔎 Proposed fix
-    locator = f'rule/directives/when/then[@icon="{preset}"].../.../...'
-    if (rule_id := self._domain_objects.find(locator).get("id")) is None:
-        raise PlugwiseError("Plugwise: no preset id found.")  # pragma: no cover
+    locator = f'rule/directives/when/then[@icon="{preset}"].../.../...'
+    if (elem := self._domain_objects.find(locator)) is None or (rule_id := elem.get("id")) is None:
+        raise PlugwiseError("Plugwise: no preset id found.")  # pragma: no cover
🧹 Nitpick comments (1)
plugwise/util.py (1)

88-89: Optional: Redundant None-check for element from findall().

findall() never yields None elements, so the check at lines 88-89 is redundant. You can safely remove this defensive guard.

🔎 Proposed simplification
     for heater_central in xml.findall(locator):
-        if not heater_central:
-            continue  # pragma: no cover
-
         heater_central_count += 1
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ae27f8d and 74428bf.

📒 Files selected for processing (3)
  • plugwise/legacy/helper.py (5 hunks)
  • plugwise/legacy/smile.py (4 hunks)
  • plugwise/util.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugwise/legacy/helper.py
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2024-10-06T16:03:27.250Z
Learnt from: bouwew
Repo: plugwise/python-plugwise PR: 630
File: tests/data/adam/adam_g_o.json:0-0
Timestamp: 2024-10-06T16:03:27.250Z
Learning: The minimal information for the `heater_central` device is due to a known bug still present in the code.

Applied to files:

  • plugwise/util.py
📚 Learning: 2025-12-21T10:56:14.461Z
Learnt from: bouwew
Repo: plugwise/python-plugwise PR: 838
File: plugwise/helper.py:210-211
Timestamp: 2025-12-21T10:56:14.461Z
Learning: In plugwise/helper.py, when parsing location XML elements, the `type` element is always present for a location, so null checks are not required when accessing `location.find("type").text`.

Applied to files:

  • plugwise/util.py
🧬 Code graph analysis (2)
plugwise/legacy/smile.py (4)
plugwise/common.py (1)
  • _get_groups (191-219)
plugwise/legacy/helper.py (1)
  • _presets (392-407)
plugwise/helper.py (1)
  • _presets (893-911)
plugwise/exceptions.py (1)
  • PlugwiseError (24-25)
plugwise/util.py (2)
plugwise/common.py (1)
  • heater_id (63-65)
plugwise/__init__.py (1)
  • heater_id (101-103)
🔇 Additional comments (5)
plugwise/legacy/smile.py (2)

85-85: LGTM! Aligns with _get_groups() refactor.

The removal of the conditional merge correctly reflects that _get_groups() now populates self.gw_entities directly rather than returning a dictionary.


192-196: LGTM! Safe attribute access with proper validation.

The change from .attrib["id"] to .get("id") with subsequent None-checking (line 198) correctly handles missing id attributes.

plugwise/util.py (3)

92-93: LGTM! Robust None-handling for missing id attribute.

The guard correctly prevents None from being used as a dictionary key (addressed the concern from previous review). By continuing when id is None, the code ensures only valid IDs reach line 101.


106-112: LGTM! Sound selection logic for heater_id.

The logic correctly defaults to the first valid heater_central and upgrades to one with actuators when multiple candidates exist. The control flow ensures heater_id is always a valid string when returned.


100-101: The filtering behavior is correct and aligns with the PR objective.

The code intentionally restricts heater_central collection to devices named "Central heating boiler" to exclude orphaned or misconfigured appliances (such as SmartPlug Floor devices) that are incorrectly marked as heater_central type. This filtering addresses Pw-Beta Issue #739 regarding Circle/Stealth device handling and is consistent with the known heater_central issues documented in the codebase.

Copy link
Contributor

@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 (2)
plugwise/legacy/helper.py (2)

418-418: Consider the impact of None in the search path.

If result.get("id") returns None, line 428's f-string becomes './rule[@id="None"]...', which would fail to match any rule. While this may be acceptable (since directives would be False and the function returns appropriate defaults), explicitly checking for None and keeping rule_id = NONE would make the intent clearer.

🔎 Suggested refinement
-        rule_id = result.get("id")
+        if (rule_id_val := result.get("id")) is not None:
+            rule_id = rule_id_val

439-439: Add defensive checks for thermostat appliance lookup.

The function assumes .find(locator) returns a valid element and that it has an id attribute. If either assumption fails, you'd get AttributeError or construct an invalid URI. Consider adding explicit checks or documenting that this method requires valid thermostat XML structure.

🔎 Proposed defensive pattern
-        appliance_id = self._appliances.find(locator).get("id")
-        return f"{APPLIANCES};id={appliance_id}/thermostat"
+        if (thermostat := self._appliances.find(locator)) is None:
+            return ""  # or raise appropriate error
+        if (appliance_id := thermostat.get("id")) is None:
+            return ""  # or raise appropriate error
+        return f"{APPLIANCES};id={appliance_id}/thermostat"
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 74428bf and 2cf61b4.

📒 Files selected for processing (3)
  • plugwise/legacy/helper.py (5 hunks)
  • plugwise/legacy/smile.py (4 hunks)
  • plugwise/util.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugwise/legacy/smile.py
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2024-10-06T16:03:27.250Z
Learnt from: bouwew
Repo: plugwise/python-plugwise PR: 630
File: tests/data/adam/adam_g_o.json:0-0
Timestamp: 2024-10-06T16:03:27.250Z
Learning: The minimal information for the `heater_central` device is due to a known bug still present in the code.

Applied to files:

  • plugwise/util.py
📚 Learning: 2024-12-22T09:37:24.648Z
Learnt from: bouwew
Repo: plugwise/python-plugwise PR: 678
File: plugwise/legacy/helper.py:69-69
Timestamp: 2024-12-22T09:37:24.648Z
Learning: The code in plugwise/legacy/helper.py is fully separated from plugwise/helper.py. The user indicates that the problem signaled about the mismatch of the variable name (_home_location vs _home_loc_id) does not actually apply.

Applied to files:

  • plugwise/util.py
  • plugwise/legacy/helper.py
📚 Learning: 2025-12-21T10:56:14.461Z
Learnt from: bouwew
Repo: plugwise/python-plugwise PR: 838
File: plugwise/helper.py:210-211
Timestamp: 2025-12-21T10:56:14.461Z
Learning: In plugwise/helper.py, when parsing location XML elements, the `type` element is always present for a location, so null checks are not required when accessing `location.find("type").text`.

Applied to files:

  • plugwise/util.py
  • plugwise/legacy/helper.py
🧬 Code graph analysis (1)
plugwise/util.py (2)
plugwise/common.py (1)
  • heater_id (63-65)
plugwise/__init__.py (1)
  • heater_id (101-103)
🪛 GitHub Check: Run pytest using Python 3.13
plugwise/util.py

[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.

🪛 GitHub Check: Run pytest using Python 3.14
plugwise/util.py

[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.


[warning] 88-88:
Testing an element's truth value will always return True in future versions. Use specific 'len(elem)' or 'elem is not None' test instead.

⏰ 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: Process test coverage
🔇 Additional comments (5)
plugwise/legacy/helper.py (3)

396-405: Good defensive validation of directive attributes.

The enhanced checks ensure both icon and temperature attributes are present before constructing preset entries, avoiding potential TypeError from float(None). The migration to .get() improves robustness.


151-151: No action needed. The code at line 151 correctly uses location.get("id"), which is already the defensive approach. Test fixtures confirm all location elements have id attributes, and the XML schema guarantees this requirement.


110-110: The change to .get("id") improves defensive coding, and test fixtures confirm appliances always have id attributes.

The shift from appliance.attrib["id"] to appliance.get("id") prevents KeyError, which is good practice. However, examination of XML test fixtures (legacy_anna, legacy_anna_2, etc.) shows that all appliance elements consistently include the id attribute, making the None risk minimal in practice. That said, explicit None-checking after line 110 would strengthen robustness if malformed XML ever occurs.

plugwise/util.py (2)

91-101: Robust None-checking and filtering logic.

The explicit checks for heater_central_id and heater_central_name being None prevent potential errors and make the code more defensive. The filtering by name "Central heating boiler" correctly implements the requirement from Pw-Beta Issue #739.


103-114: Clear selection logic for multiple heater_central devices.

The refactored logic correctly handles the Core Issue #104433 scenario by selecting the first heater_central with actuators when multiple devices exist. The fallback to the first entry is sensible and the empty-list guard prevents errors.

Copy link
Contributor

@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: 2

♻️ Duplicate comments (1)
plugwise/legacy/smile.py (1)

210-210: Critical: Missing None-check before calling .get() on find() result.

If find(locator) returns None, calling .get("id") will raise AttributeError. This issue was previously flagged and marked as addressed, but the problematic code is still present.

🔎 Proposed fix
     locator = f'.//*[@id="{schedule_rule_id}"]/template'
-    template_id = self._domain_objects.find(locator).get("id")
+    if (template_elem := self._domain_objects.find(locator)) is None or (template_id := template_elem.get("id")) is None:
+        raise PlugwiseError(
+            "Plugwise: template not found for schedule."
+        )  # pragma: no cover
🧹 Nitpick comments (2)
plugwise/util.py (2)

94-99: Consider additional guard for Element.text being None.

While the check at lines 94-95 ensures the name element exists, Element.text can theoretically be None. If robustness is a priority, consider adding:

if (heater_central_name := heater_central.find("name")) is None or heater_central_name.text is None:
    continue  # pragma: no cover

However, in this domain context, name elements should always have text content, so this is optional defensive coding.


86-114: Simplify data structure for better readability.

The current implementation uses a list of single-key dictionaries (list[dict[str, bool]]), which makes extraction at lines 106 and 109 unnecessarily complex:

  • Line 106: list(heater_central_list[0].keys())[0]
  • Line 109: next(iter(item.items()))

Consider using a simpler structure such as a list of tuples:

🔎 Proposed refactor using tuples
     heater_central_count = 0
-    heater_central_list: list[dict[str, bool]] = []
+    heater_central_list: list[tuple[str, bool]] = []
     if not (result := xml.findall(locator)):
-        return   # pragma: no cover
+        return NONE  # pragma: no cover
 
     for heater_central in result:
         if (heater_central_id := heater_central.get("id")) is None:
             continue  # pragma: no cover
 
         if (heater_central_name := heater_central.find("name")) is None:
             continue  # pragma: no cover
 
         has_actuators = heater_central.find("actuator_functionalities/") is not None
         # Filter for Plug/Circle/Stealth heater_central -- Pw-Beta Issue #739
         if heater_central_name.text == "Central heating boiler":
-            heater_central_list.append({heater_central_id: has_actuators})
+            heater_central_list.append((heater_central_id, has_actuators))
             heater_central_count += 1
 
     if not heater_central_list:
         return NONE  # pragma: no cover
 
-    heater_id = list(heater_central_list[0].keys())[0]
+    heater_id = heater_central_list[0][0]
     if heater_central_count > 1:
         for item in heater_central_list:
-            heater_central_id, has_actuators = next(iter(item.items()))
+            heater_central_id, has_actuators = item
             if has_actuators:
                 heater_id = heater_central_id
                 break
 
     return heater_id
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2cf61b4 and b18d22c.

📒 Files selected for processing (3)
  • plugwise/legacy/helper.py (5 hunks)
  • plugwise/legacy/smile.py (4 hunks)
  • plugwise/util.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • plugwise/legacy/helper.py
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2024-10-06T16:03:27.250Z
Learnt from: bouwew
Repo: plugwise/python-plugwise PR: 630
File: tests/data/adam/adam_g_o.json:0-0
Timestamp: 2024-10-06T16:03:27.250Z
Learning: The minimal information for the `heater_central` device is due to a known bug still present in the code.

Applied to files:

  • plugwise/util.py
📚 Learning: 2024-12-22T09:37:24.648Z
Learnt from: bouwew
Repo: plugwise/python-plugwise PR: 678
File: plugwise/legacy/helper.py:69-69
Timestamp: 2024-12-22T09:37:24.648Z
Learning: The code in plugwise/legacy/helper.py is fully separated from plugwise/helper.py. The user indicates that the problem signaled about the mismatch of the variable name (_home_location vs _home_loc_id) does not actually apply.

Applied to files:

  • plugwise/util.py
📚 Learning: 2025-12-21T10:56:14.461Z
Learnt from: bouwew
Repo: plugwise/python-plugwise PR: 838
File: plugwise/helper.py:210-211
Timestamp: 2025-12-21T10:56:14.461Z
Learning: In plugwise/helper.py, when parsing location XML elements, the `type` element is always present for a location, so null checks are not required when accessing `location.find("type").text`.

Applied to files:

  • plugwise/util.py
🧬 Code graph analysis (2)
plugwise/util.py (2)
plugwise/common.py (1)
  • heater_id (63-65)
plugwise/__init__.py (1)
  • heater_id (101-103)
plugwise/legacy/smile.py (4)
plugwise/common.py (1)
  • _get_groups (191-219)
plugwise/legacy/helper.py (1)
  • _presets (392-407)
plugwise/helper.py (1)
  • _presets (893-911)
plugwise/exceptions.py (1)
  • PlugwiseError (24-25)
🪛 GitHub Actions: Latest commit
plugwise/util.py

[error] 88-88: mypy: Return value expected [return-value]. Found 1 error in 1 file (checked 12 source files).

🔇 Additional comments (1)
plugwise/legacy/smile.py (1)

85-85: LGTM: Aligned with refactored _get_groups().

The direct call without capturing a return value is correct. According to the relevant code from plugwise/common.py, _get_groups() now returns None and populates self.gw_entities in-place.

@bouwew bouwew marked this pull request as ready for review December 21, 2025 13:57
@bouwew bouwew requested a review from a team as a code owner December 21, 2025 13:57
@sonarqubecloud
Copy link

@bouwew bouwew merged commit fa40d6d into main Dec 21, 2025
18 checks passed
@bouwew bouwew deleted the xml-get branch December 21, 2025 14:20
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.

3 participants