Skip to content

Conversation

jthun
Copy link
Contributor

@jthun jthun commented Sep 8, 2025

Update EAIR continuously (Fixes #1707). Handle clear profile exceptions when changing max current (Fixes #1706 and fixes #1703). Use proper connector id for extra attr (phase data) (Fixes #1704). Clear IdTag and reset transaction id to 0 on stop/unplug (Fixes #1702). Add more tests. Test coverage settings.

Summary by CodeRabbit

  • New Features
    • Optimistic UI for adjusting current limit (immediate slider response).
    • Improved multi-connector handling, including per-connector phase attributes and energy accounting.
  • Bug Fixes
    • Empty units/attributes now treated as missing, enabling correct fallbacks.
    • More robust post-connect, availability, and charging profile flows with safer error handling.
    • Clearer behavior when connector ID is omitted or invalid.
  • Tests
    • Extensive new coverage for availability, charge-rate, and phase-handling scenarios.
  • Chores
    • Added VS Code Pytest configuration for streamlined testing.

…xceptions when changing max current (Fixes lbbrhzn#1706 and lbbrhzn#1703). Use proper connector id for extra attr (phase data) (Fixes lbbrhzn#1704). Clear IdTag and reset transaction id to 0 on stop/unplug (Fixes lbbrhzn#1702). Add more tests.
@jthun jthun temporarily deployed to continuous-integration September 8, 2025 07:46 — with GitHub Actions Inactive
Copy link
Contributor

coderabbitai bot commented Sep 8, 2025

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • Failed to retrieve linked issues from the platform client.
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

Copy link

codecov bot commented Sep 8, 2025

Codecov Report

❌ Patch coverage is 87.66234% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.25%. Comparing base (00e5fd1) to head (fbc017a).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
custom_components/ocpp/chargepoint.py 60.71% 11 Missing ⚠️
custom_components/ocpp/api.py 77.77% 4 Missing ⚠️
custom_components/ocpp/number.py 81.81% 2 Missing ⚠️
custom_components/ocpp/ocppv16.py 97.93% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1708      +/-   ##
==========================================
+ Coverage   94.16%   94.25%   +0.09%     
==========================================
  Files          12       12              
  Lines        2673     2751      +78     
==========================================
+ Hits         2517     2593      +76     
- Misses        156      158       +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.

@jthun
Copy link
Contributor Author

jthun commented Sep 8, 2025

Please test with one-connector chargers to make sure set_charge_rate works, and that phase attributes land on the sensor.

@jthun jthun marked this pull request as ready for review September 8, 2025 07:53
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

Caution

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

⚠️ Outside diff range comments (1)
tests/test_charge_point_v16.py (1)

2622-2829: Bug: using cp_id instead of cpid in CentralSystem lookups can break on custom CPIDs

CentralSystem.get_extra_attr expects the HA device id (cpid), not the websocket cp_id. Several assertions here pass cp_id, which will fail when CONF_CPID != cp_id.

@@ async def test_current_import_phase_extra_attrs_single_and_multi_connector(
-            srv_cp: ServerCP = cs.charge_points[cp_id]
+            srv_cp: ServerCP = cs.charge_points[cp_id]
+            cpid = srv_cp.settings.cpid
@@
-                attrs = cs.get_extra_attr(cp_id, "Current.Import", connector_id=None)
+                attrs = cs.get_extra_attr(cpid, "Current.Import", connector_id=None)
@@
-                attrs1 = cs.get_extra_attr(cp_id, "Current.Import", connector_id=1)
+                attrs1 = cs.get_extra_attr(cpid, "Current.Import", connector_id=1)
@@
-                attrs1 = cs.get_extra_attr(cp_id, "Current.Import", connector_id=1)
-                attrs2 = cs.get_extra_attr(cp_id, "Current.Import", connector_id=2)
+                attrs1 = cs.get_extra_attr(cpid, "Current.Import", connector_id=1)
+                attrs2 = cs.get_extra_attr(cpid, "Current.Import", connector_id=2)
🧹 Nitpick comments (9)
custom_components/ocpp/api.py (1)

370-374: Good normalization of empty values to enable fallbacks.

Treating blank unit strings and empty extra_attr dicts as None is correct and aligns with multi-connector fallback logic. Consider a tiny DRY helper (e.g., _none_if_blank) to avoid repetition across get_unit/get_ha_unit/get_extra_attr.

Also applies to: 386-389, 412-416, 428-431, 454-457, 470-472

custom_components/ocpp/number.py (1)

35-37: Avoid setting logger level in module code.

Hard-coding the domain logger to INFO can override user HA logger settings. Recommend removing the level set and letting HA config control verbosity.

-_LOGGER: logging.Logger = logging.getLogger(__package__)
-logging.getLogger(DOMAIN).setLevel(logging.INFO)
+_LOGGER: logging.Logger = logging.getLogger(__package__)
custom_components/ocpp/ocppv16.py (3)

328-356: Deterministic profile IDs: nice; tighten type safety to avoid typos.

Passing purpose as a free-form string risks mismatches. Consider typing purpose as ChargingProfilePurposeType and deriving the code/map from the Enum to prevent string drift.

-    def _profile_ids_for(
-        self, conn_id: int, purpose: str, tx_id: int | None = None
+    def _profile_ids_for(
+        self,
+        conn_id: int,
+        purpose: ChargingProfilePurposeType,
+        tx_id: int | None = None,
     ) -> tuple[int, int]:
@@
-        PURPOSE_CODE = {
-            "ChargePointMaxProfile": 1,
-            "TxDefaultProfile": 2,
-            "TxProfile": 3,
-        }
-        if purpose == "ChargePointMaxProfile":
+        PURPOSE_CODE = {
+            ChargingProfilePurposeType.charge_point_max_profile: 1,
+            ChargingProfilePurposeType.tx_default_profile: 2,
+            ChargingProfilePurposeType.tx_profile: 3,
+        }
+        if purpose == ChargingProfilePurposeType.charge_point_max_profile:
@@
-        if purpose == "TxProfile" and tx_id is not None:
+        if purpose == ChargingProfilePurposeType.tx_profile and tx_id is not None:

And adapt call sites accordingly when constructing profiles.


394-415: Profile application order and pre-clearing by id are solid; improve unit detection.

Attempt sequence (CPMax → TxDefault → TxProfile) and targeted Clear by id reduce conflicts. One small robustness tweak: parse the allowed units token-wise and case-insensitively to avoid substring matches (e.g., "Power" containing "ow").

Would you like a follow-up patch to tokenize resp_units and select amps/watts reliably?

Also applies to: 416-435, 437-475


480-496: Availability errors are swallowed; consider notifying HA like other flows.

Other service paths notify HA on failure. Mirror that here to surface issues to users.

-        except TimeoutError as ex:
+        except TimeoutError as ex:
             _LOGGER.debug("ChangeAvailability timed out (conn=%s): %s", conn, ex)
-            return False
+            await self.notify_ha(f"ChangeAvailability timed out (conn={conn})")
+            return False
         except Exception as ex:
             _LOGGER.debug("ChangeAvailability failed (conn=%s): %s", conn, ex)
-            return False
+            await self.notify_ha(f"ChangeAvailability failed (conn={conn}): {ex}")
+            return False
@@
-            return status in (
+            ok = status in (
                 AvailabilityStatus.accepted,
                 AvailabilityStatus.scheduled,
             )
+            if not ok:
+                await self.notify_ha(
+                    f"Set availability not accepted (conn={conn}, status={status})"
+                )
+            return ok

Also applies to: 497-503

custom_components/ocpp/chargepoint.py (2)

649-660: Use the canonical self.num_connectors for target connector resolution

Relying on getattr(self, CONF_NUM_CONNECTORS, …) risks drift if the constant ever changes or isn’t set as an attribute on this instance. You already maintain self.num_connectors; use that directly.

-        n_connectors = getattr(self, CONF_NUM_CONNECTORS, DEFAULT_NUM_CONNECTORS) or 1
+        n_connectors = int(getattr(self, "num_connectors", DEFAULT_NUM_CONNECTORS) or 1)

691-733: Minor readability nit: direct equality for voltage measurand check

No functional change, but this reads simpler.

-            if metric in [Measurand.voltage.value]:
+            if metric == Measurand.voltage.value:
tests/test_charge_point_v16.py (2)

2857-2888: Nit: keep comments consistent and in English

Small language mix in the assertion comment.

-            assert ok is False  # timeout-grenen ska returnera False
+            assert ok is False  # the timeout branch should return False

2708-2714: Optional: avoid reusing TCP ports across parametrized tests to reduce flakiness under parallel runs

Several tests share ports used elsewhere (e.g., 9077). If these ever run in parallel (xdist), consider allocating unique ports per test or deriving from an offset.

Would you like a quick helper to scan for duplicate port literals across tests?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 00e5fd1 and fbc017a.

📒 Files selected for processing (6)
  • .vscode/settings.json (1 hunks)
  • custom_components/ocpp/api.py (6 hunks)
  • custom_components/ocpp/chargepoint.py (5 hunks)
  • custom_components/ocpp/number.py (3 hunks)
  • custom_components/ocpp/ocppv16.py (6 hunks)
  • tests/test_charge_point_v16.py (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-03-02T06:05:56.424Z
Learnt from: drc38
PR: lbbrhzn/ocpp#1527
File: custom_components/ocpp/chargepoint.py:391-391
Timestamp: 2025-03-02T06:05:56.424Z
Learning: In the OCPP component, post_connect initialization has been refactored to only happen in response to boot notifications rather than automatically on startup, preventing duplicate initialization if a boot notification occurs close to the initial connection.

Applied to files:

  • custom_components/ocpp/chargepoint.py
🧬 Code graph analysis (5)
custom_components/ocpp/number.py (1)
custom_components/ocpp/api.py (1)
  • set_max_charge_rate_amps (539-550)
custom_components/ocpp/api.py (2)
custom_components/ocpp/chargepoint.py (5)
  • unit (94-96)
  • unit (99-101)
  • ha_unit (104-106)
  • extra_attr (109-111)
  • extra_attr (114-116)
tests/test_api_paths.py (1)
  • test_get_units_and_attrs_fallbacks (196-221)
custom_components/ocpp/ocppv16.py (2)
custom_components/ocpp/chargepoint.py (10)
  • value (84-86)
  • value (89-91)
  • clear_profile (387-389)
  • notify_ha (865-876)
  • set_availability (401-403)
  • get_energy_kwh (735-739)
  • unit (94-96)
  • unit (99-101)
  • extra_attr (109-111)
  • extra_attr (114-116)
custom_components/ocpp/ocppv201.py (2)
  • clear_profile (335-343)
  • set_availability (422-440)
custom_components/ocpp/chargepoint.py (3)
custom_components/ocpp/ocppv16.py (4)
  • get_number_of_connectors (108-147)
  • set_availability (478-508)
  • trigger_boot_notification (253-263)
  • trigger_status_notification (265-289)
custom_components/ocpp/ocppv201.py (4)
  • get_number_of_connectors (249-252)
  • set_availability (422-440)
  • trigger_status_notification (321-333)
  • ChargePoint (77-881)
tests/test_api_paths.py (1)
  • set_availability (43-48)
tests/test_charge_point_v16.py (5)
custom_components/ocpp/api.py (2)
  • CentralSystem (98-670)
  • get_extra_attr (445-485)
custom_components/ocpp/chargepoint.py (6)
  • ChargePoint (225-876)
  • value (84-86)
  • value (89-91)
  • get (172-175)
  • set_availability (401-403)
  • set_charge_rate (391-399)
custom_components/ocpp/ocppv16.py (3)
  • ChargePoint (83-1106)
  • set_availability (478-508)
  • set_charge_rate (358-476)
custom_components/ocpp/ocppv201.py (3)
  • ChargePoint (77-881)
  • set_availability (422-440)
  • set_charge_rate (345-420)
tests/charge_point_test.py (1)
  • wait_ready (91-94)
🔇 Additional comments (8)
custom_components/ocpp/number.py (1)

220-241: Revert UI on failure in async_set_native_value
Capture the old _attr_native_value before updating, then if set_max_charge_rate_amps returns false or throws, restore the previous value and call async_write_ha_state() to keep the UI in sync.

custom_components/ocpp/ocppv16.py (4)

314-327: clear_profile now robust to exceptions and treats Unknown as success — good.


366-378: Custom profile path handled with guarded SetChargingProfile — good.

Good defensive logging and early return on accepted.


821-842: Per-connector EAIR updates during active transactions — good improvement.


1036-1038: Clearing id_tag and transaction_id on stop — good and consistent with #1702.

custom_components/ocpp/chargepoint.py (1)

330-335: post_connect hardening: initialization order and defensive availability handling look good

Fetching number of connectors first, initializing connector slots, then best‑effort availability (with CancelledError passthrough) is sensible and reduces brittle failures. Debug‑level logging for non‑fatal aborts is appropriate.

Also applies to: 357-363, 369-371

tests/test_charge_point_v16.py (2)

46-46: Added imports for AvailabilityType and Phase are correct

These are used by new tests; imports look right.

Also applies to: 56-56


2997-3031: SetAvailability and SetChargingProfile tests cover key branches well

Good coverage of timeout/exception paths, happy paths, and unit/attempt fallbacks for set_charge_rate. Assertions validate both payload shape and outcomes.

Also applies to: 3041-3075, 3085-3153, 3164-3226, 3236-3292, 3302-3343

Comment on lines +13 to +20
},
"python.testing.pytestArgs": [
"tests",
"--cov=custom_components",
"--cov-report=term-missing",
"--cov-report=xml:coverage.xml",
"--timeout=30"
],
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Pytest args look fine; verify plugins and consider centralizing config.

  • The added flags require pytest-cov and pytest-timeout; ensure they are present in dev requirements or pyproject.
  • Prefer pytest.ini/pyproject.toml to make these defaults across all environments, not just VS Code.

Run to verify presence of plugins and centralized config:


🏁 Script executed:

#!/bin/bash
# Check for pytest config and plugins
rg -nP '^\s*\[tool.pytest.ini_options\]|\[pytest\]' -g 'pyproject.toml|pytest.ini|setup.cfg' -n
rg -nP 'pytest-(cov|timeout)\b' -g 'requirements*.txt|dev-requirements*.txt|pyproject.toml|setup.cfg' -n -C2

Length of output: 297


🏁 Script executed:

#!/bin/bash
set -e

echo "=== Locating pytest config files ==="
find . -maxdepth 3 -type f \( -name "pyproject.toml" -o -name "pytest.ini" -o -name "setup.cfg" \)

for cfg in pyproject.toml pytest.ini setup.cfg; do
  if [ -f "$cfg" ]; then
    echo
    echo "=== Contents of $cfg matching pytest config ==="
    grep -nE '^\s*\[tool\.pytest\.ini_options\]|\[pytest\]' "$cfg" || echo "No pytest config block in $cfg"
  fi
done

echo
echo "=== Checking for pytest-cov and pytest-timeout in dependency files ==="
find . -type f -name "*requirements*.txt" -print -exec grep -nE 'pytest-(cov|timeout)' {} \; || echo "No matches in requirements files"
grep -R -nE 'pytest-(cov|timeout)' pyproject.toml setup.cfg || echo "No plugin deps in pyproject.toml/setup.cfg"

Length of output: 344


Add missing pytest plugins and centralize configuration

  • Add pytest-cov and pytest-timeout to your development dependencies (e.g. a requirements-dev.txt or in pyproject.toml) so the VS Code pytest args work correctly.
  • Move these pytest flags into a central pytest config (create pytest.ini or add a [tool.pytest.ini_options] block in pyproject.toml or under [pytest] in setup.cfg) to apply them across all environments.
🤖 Prompt for AI Agents
In .vscode/settings.json around lines 13 to 20 the VS Code pytest args reference
plugins and flags that should be managed centrally and installed as dev
dependencies; add pytest-cov and pytest-timeout to your development dependencies
(e.g. requirements-dev.txt, dev-dependencies in pyproject.toml, or setup.cfg)
and remove plugin-specific flags from the editor settings; instead create a
central pytest config (pytest.ini or a [tool.pytest.ini_options] block in
pyproject.toml or [pytest] in setup.cfg) and move the flags
--cov=custom_components, --cov-report=term-missing,
--cov-report=xml:coverage.xml, and --timeout=30 into that config so they apply
in all environments and the VS Code settings simply reference the test discovery
path.

Comment on lines +680 to 685
self._metrics[(target_cid, measurand)].unit = unit
self._metrics[(target_cid, measurand)].extra_attr[om.unit.value] = unit
self._metrics[(target_cid, measurand)].extra_attr[phase] = value
self._metrics[(target_cid, measurand)].extra_attr[om.context.value] = (
context
)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Guard extra_attr against None to avoid TypeError and use a local alias

Metric.extra_attr can be None (tests simulate this). Ensure a dict before item assignment and reuse a local alias for clarity.

-                self._metrics[(target_cid, measurand)].unit = unit
-                self._metrics[(target_cid, measurand)].extra_attr[om.unit.value] = unit
-                self._metrics[(target_cid, measurand)].extra_attr[phase] = value
-                self._metrics[(target_cid, measurand)].extra_attr[om.context.value] = (
-                    context
-                )
+                m = self._metrics[(target_cid, measurand)]
+                m.unit = unit
+                if not isinstance(m.extra_attr, dict):
+                    m.extra_attr = {}
+                m.extra_attr[om.unit.value] = unit
+                m.extra_attr[phase] = value
+                m.extra_attr[om.context.value] = context
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
self._metrics[(target_cid, measurand)].unit = unit
self._metrics[(target_cid, measurand)].extra_attr[om.unit.value] = unit
self._metrics[(target_cid, measurand)].extra_attr[phase] = value
self._metrics[(target_cid, measurand)].extra_attr[om.context.value] = (
context
)
m = self._metrics[(target_cid, measurand)]
m.unit = unit
if not isinstance(m.extra_attr, dict):
m.extra_attr = {}
m.extra_attr[om.unit.value] = unit
m.extra_attr[phase] = value
m.extra_attr[om.context.value] = context
🤖 Prompt for AI Agents
In custom_components/ocpp/chargepoint.py around lines 680 to 685,
Metric.extra_attr may be None which causes a TypeError when assigning keys;
create or ensure a dict before assigning and use a local alias for the Metric
instance or its extra_attr to avoid repeated lookups. Concretely, retrieve the
metric into a local variable, if metric.extra_attr is None set it to {} (or
assign a new dict to a local extra_attr and assign back), then set metric.unit
and assign unit, phase and context entries into that dict, and finally reassign
metric.extra_attr if you used a local dict.

Comment on lines +928 to +931
if status == ChargePointStatus.available:
self._metrics[(connector_id or 1, cstat.id_tag.value)].value = ""
self._metrics[(connector_id or 1, csess.transaction_id.value)].value = 0

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Don't clear connector 1 on station-level (connectorId=0) status.

Using (connector_id or 1) clears connector 1 when a station-level Available arrives. Limit clearing to real connector ids > 0 (or intentionally clear all).

-        if status == ChargePointStatus.available:
-            self._metrics[(connector_id or 1, cstat.id_tag.value)].value = ""
-            self._metrics[(connector_id or 1, csess.transaction_id.value)].value = 0
+        if status == ChargePointStatus.available and connector_id not in (None, 0):
+            self._metrics[(connector_id, cstat.id_tag.value)].value = ""
+            self._metrics[(connector_id, csess.transaction_id.value)].value = 0
+        # Optionally, to clear all on station-level Available:
+        # elif connector_id in (None, 0):
+        #     for cid in list(self._active_tx.keys()):
+        #         self._metrics[(cid, cstat.id_tag.value)].value = ""
+        #         self._metrics[(cid, csess.transaction_id.value)].value = 0
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if status == ChargePointStatus.available:
self._metrics[(connector_id or 1, cstat.id_tag.value)].value = ""
self._metrics[(connector_id or 1, csess.transaction_id.value)].value = 0
if status == ChargePointStatus.available and connector_id not in (None, 0):
self._metrics[(connector_id, cstat.id_tag.value)].value = ""
self._metrics[(connector_id, csess.transaction_id.value)].value = 0
# Optionally, to clear all on station-level Available:
# elif connector_id in (None, 0):
# for cid in list(self._active_tx.keys()):
# self._metrics[(cid, cstat.id_tag.value)].value = ""
# self._metrics[(cid, csess.transaction_id.value)].value = 0
🤖 Prompt for AI Agents
In custom_components/ocpp/ocppv16.py around lines 928-931, the current logic
uses (connector_id or 1) which causes station-level messages (connector_id == 0
or None) to clear connector 1 metrics; change the code to only clear metrics
when connector_id is a real connector (> 0). Concretely, check if connector_id
is truthy and > 0 before touching self._metrics for that connector id (skip
clearing when connector_id is 0 or None); do not fallback to 1 for station-level
messages unless you intentionally want to clear all connectors.

Copy link
Collaborator

@drc38 drc38 left a comment

Choose a reason for hiding this comment

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

Hi @jthun, looks like import register is still not updating

Image

Ignore, it is updating just by very small increments as I'm not charging

@drc38 drc38 merged commit 9bf50dd into lbbrhzn:main Sep 8, 2025
8 of 9 checks passed
@jthun jthun deleted the bugfix_mc branch September 8, 2025 14:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment