Skip to content

Gate charging-release switches on Modbus release mode - #1

Open
jensteblick wants to merge 1 commit into
mainfrom
release-mode-switch-gating
Open

Gate charging-release switches on Modbus release mode#1
jensteblick wants to merge 1 commit into
mainfrom
release-mode-switch-gating

Conversation

@jensteblick

Copy link
Copy Markdown
Contributor

Why

The Charging enabled (X300) and Available (X304) switches were exposed unconditionally. The CHARX controller only honours those registers when the charging release mode (X120) is Modbus (5) — and Veton chargers ship with release mode = OCPP (4), where OCPP owns authorisation/start/stop and the supported EMS control is the max-charging-current register X301.

So on a normal Veton charger both switches wrote to a register the controller silently ignored: a control that did nothing, and one that contradicted the public EMS integration guide, which asks third-party systems not to take over charging release.

Found while answering an external integrator who was pointed at both repos.

What changed

  • switch.py — shared base gates available on X120 == 5 (still deferring to super().available), exposes the release mode in entity attributes, and logs a warning once per entity — not on every 5 s refresh — naming the actual mode and pointing at the max-current number. Unique IDs are unchanged, so existing entities are not re-registered.
  • const.pyRELEASE_MODE_MODBUS constant instead of a bare 5.
  • dashboard.py — the Controls card now leads with Max Current; the two switch rows became conditional rows that hide while the entity is unavailable, instead of rendering permanently greyed out.
  • modbus_client.py — docstrings only, no behaviour change.
  • README.md — corrects the Controls bullet and the switch table row, adds a "Release mode — what you can actually control" section, and fixes the smart-charging section that told users to toggle Charging enabled from their automations.

The switches are kept, not removed: they are correct for a standalone, non-OCPP charger configured with release mode = Modbus.

Tests

42 passed (39 before). 18 new tests: gating across modes 0–5, data is None, last_update_success = False, attribute contents, warn-once across repeated checks, and the conditional dashboard rows.

Note

Tests are mock-based — this has not been exercised against a running Home Assistant or a real charger. Worth a spot check on a live unit before release. manifest.json is still at 1.1.0; no version bump, since that is a release decision.

🤖 Generated with Claude Code

The Charging enabled (X300) and Available (X304) switches were exposed
unconditionally, but the CHARX controller only honours those registers when
the charging release mode (X120) is Modbus (5). Veton chargers ship with
release mode = OCPP (4), where OCPP owns authorisation and start/stop and the
supported EMS control is the max-charging-current register X301.

On a normal charger both switches therefore wrote to a register the controller
silently ignored, presenting a control that did nothing and contradicting the
public EMS integration guide.

- switch.py: shared base gates `available` on X120 == 5, exposes the release
  mode in the entity attributes, and warns once per entity (not per 5 s
  refresh) explaining why the switch is inactive and what to use instead.
  Unique IDs unchanged, so existing entities are not re-registered.
- const.py: RELEASE_MODE_MODBUS constant instead of a bare literal.
- dashboard.py: Controls card leads with Max Current; the two switch rows
  become conditional rows that hide while the entity is unavailable.
- modbus_client.py: docstrings only — no behaviour change.
- README: correct the Controls bullet, the switch table row, and the smart
  charging section that told users to toggle Charging enabled from
  automations; add a section on what is actually controllable.
- tests: 18 new tests covering the gating, the warn-once behaviour and the
  conditional dashboard rows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Gate release switches by charger Modbus mode

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Marks charging-release switches unavailable unless the charger uses Modbus release mode.
• Prioritizes universally supported max-current control and hides inactive dashboard switches.
• Documents release-mode limitations and verifies gating, attributes, warnings, and dashboard
 behavior.
Diagram

graph TD
  A["Coordinator Poll"] --> B{"Modbus Mode?"} -->|Yes| C["Release Switches"] --> D["X300 / X304"]
  B -->|No| E["Unavailable State"] --> F["Conditional Rows"]
  E --> G["Mode Attributes"]
  E --> H["One-time Warning"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Remove release switches
  • ➕ Eliminates unsupported controls for standard OCPP chargers
  • ➕ Reduces integration surface area
  • ➖ Breaks valid standalone Modbus deployments
  • ➖ Removes existing entities and disrupts user automations
2. Register switches conditionally
  • ➕ Unsupported entities never appear on OCPP chargers
  • ➕ Avoids unavailable entities in the registry
  • ➖ Release-mode changes require dynamic entity lifecycle handling
  • ➖ Can create confusing entity disappearance and reappearance behavior
  • ➖ Complicates entity-registry continuity
3. Reject writes in the Modbus client
  • ➕ Provides defense in depth against direct or unexpected service calls
  • ➕ Centralizes enforcement near register writes
  • ➖ The client would need current release-mode state or an additional read
  • ➖ Couples a low-level transport API to controller policy
  • ➖ Does not by itself improve dashboard or entity presentation

Recommendation: The PR's availability-gating approach is the best compatibility-preserving option: it retains stable entity IDs for Modbus installations while accurately representing unsupported controls on OCPP chargers. Conditional dashboard rows and diagnostic attributes complement that behavior; a command-time guard could be considered later as defense in depth if unavailable entities can still receive service calls.

Files changed (7) +394 / -37

Bug fix (3) +143 / -31
const.pyDefine the Modbus release-mode constant +7/-1

Define the Modbus release-mode constant

• Adds a named constant for X120 value 5 and documents which CHARX control registers depend on it.

custom_components/veton/const.py

dashboard.pyPrioritize max current and hide inactive switches +19/-2

Prioritize max current and hide inactive switches

• Moves Max Current to the top of the Controls card. Wraps both release switches in conditional rows that disappear when their entities are unavailable.

custom_components/veton/dashboard.py

switch.pyGate release switches on X120 Modbus mode +117/-28

Gate release switches on X120 Modbus mode

• Introduces a shared release-mode switch base that preserves existing unique IDs while requiring successful coordinator data and X120 value 5. It exposes diagnostic attributes and logs one explanatory warning per entity outside Modbus mode.

custom_components/veton/switch.py

Tests (2) +215 / -0
test_dashboard.pyVerify conditional release controls in the dashboard +53/-0

Verify conditional release controls in the dashboard

• Tests that Max Current leads the Controls card and both switch rows depend on entity availability. Also verifies placeholder entity IDs remain consistent when switches are absent.

tests/test_dashboard.py

test_switch.pyCover release-mode switch gating and diagnostics +162/-0

Cover release-mode switch gating and diagnostics

• Adds coverage across release modes 0–5, missing data, failed updates, register state, diagnostic attributes, and warning suppression. Verifies warnings occur once per entity only for unsupported modes.

tests/test_switch.py

Documentation (2) +36 / -6
README.mdDocument release-mode control limitations +24/-4

Document release-mode control limitations

• Clarifies that max charging current works in every release mode while charging-release and availability switches require Modbus mode. Updates smart-charging guidance to recommend X301 for normal OCPP deployments.

README.md

modbus_client.pyClarify release-register write requirements +12/-2

Clarify release-register write requirements

• Expands X300 and X304 method docstrings to explain that the controller only honors writes in Modbus release mode. Runtime behavior is unchanged.

custom_components/veton/modbus_client.py

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Partial read disables switches 🐞 Bug ☼ Reliability
Description
If the X100–X120 config read fails, release_mode remains its default 0 while the coordinator
publishes the partial update as successful, so both switches become unavailable even when the
charger is still in Modbus mode. A persistent config-block failure disables both controls
indefinitely, while a transient failure also emits misleading warn-once messages.
Code

custom_components/veton/switch.py[R67-72]

+        mode = self._release_mode
+        if mode is None:
+            return False
+        if mode != RELEASE_MODE_MODBUS:
+            self._warn_release_mode(mode)
+            return False
Evidence
CharxConnectorData.release_mode defaults to 0, and read_connector_data catches a config-block
ModbusException locally before returning the newly created partial object. The coordinator
consequently treats the read as successful and replaces its snapshot; the added gate interprets that
default value as an actual non-Modbus mode and returns unavailable.

custom_components/veton/modbus_client.py[34-43]
custom_components/veton/modbus_client.py[192-207]
custom_components/veton/modbus_client.py[259-270]
custom_components/veton/coordinator.py[47-66]
custom_components/veton/switch.py[54-72]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A failed X100–X120 read leaves `release_mode` at the dataclass default `0`. Because the exception is swallowed and the coordinator publishes the partial snapshot, the new availability gate treats the charger as non-Modbus and disables both switches.

## Issue Context
The release mode now controls entity availability, so a missing register value must not be indistinguishable from the real Dashboard mode. Retain the last validated release mode, represent an unread value as `None`, or fail the coordinator update so its previous snapshot remains active. Add coverage for a config-only read failure while the previous mode is Modbus.

## Fix Focus Areas
- custom_components/veton/modbus_client.py[34-43]
- custom_components/veton/modbus_client.py[192-207]
- custom_components/veton/coordinator.py[47-66]
- custom_components/veton/switch.py[54-72]
- tests/test_switch.py[57-79]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Inactive reason misidentifies mode 🐞 Bug ◔ Observability
Description
inactive_reason always says OCPP owns charging even when X120 reports Dashboard, Local whitelist,
External control, Permanent release, or an unknown value. This exposes factually incorrect
diagnostic information for every non-Modbus mode except OCPP.
Code

custom_components/veton/switch.py[R27-30]

+_INACTIVE_NOTE = (
+    "Charging release is owned by OCPP on this charger; this switch only works "
+    "with release mode = Modbus (X120 = 5). Use the 'Max charging current' "
+    "number (X301) to steer charging from an EMS or automation."
Evidence
The constant map defines five distinct non-Modbus modes, but _INACTIVE_NOTE unconditionally
attributes ownership to OCPP and is attached whenever the mode is any value other than 5.

custom_components/veton/const.py[24-32]
custom_components/veton/switch.py[27-31]
custom_components/veton/switch.py[92-103]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The static inactive-reason text claims that OCPP owns charging for all non-Modbus release modes, although the integration recognizes several distinct modes.

## Issue Context
The availability warning already resolves the actual mode through `RELEASE_MODE`; the entity attribute should similarly use the current mode or use mode-neutral wording. Add attribute tests for modes 0–3 and unknown values, not only OCPP.

## Fix Focus Areas
- custom_components/veton/switch.py[27-30]
- custom_components/veton/switch.py[92-103]
- custom_components/veton/const.py[24-32]
- tests/test_switch.py[88-106]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Finding overflow, which tucks the rest behind 'View more'

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +67 to +72
mode = self._release_mode
if mode is None:
return False
if mode != RELEASE_MODE_MODBUS:
self._warn_release_mode(mode)
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Partial read disables switches 🐞 Bug ☼ Reliability

If the X100–X120 config read fails, release_mode remains its default 0 while the coordinator
publishes the partial update as successful, so both switches become unavailable even when the
charger is still in Modbus mode. A persistent config-block failure disables both controls
indefinitely, while a transient failure also emits misleading warn-once messages.
Agent Prompt
## Issue description
A failed X100–X120 read leaves `release_mode` at the dataclass default `0`. Because the exception is swallowed and the coordinator publishes the partial snapshot, the new availability gate treats the charger as non-Modbus and disables both switches.

## Issue Context
The release mode now controls entity availability, so a missing register value must not be indistinguishable from the real Dashboard mode. Retain the last validated release mode, represent an unread value as `None`, or fail the coordinator update so its previous snapshot remains active. Add coverage for a config-only read failure while the previous mode is Modbus.

## Fix Focus Areas
- custom_components/veton/modbus_client.py[34-43]
- custom_components/veton/modbus_client.py[192-207]
- custom_components/veton/coordinator.py[47-66]
- custom_components/veton/switch.py[54-72]
- tests/test_switch.py[57-79]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +27 to +30
_INACTIVE_NOTE = (
"Charging release is owned by OCPP on this charger; this switch only works "
"with release mode = Modbus (X120 = 5). Use the 'Max charging current' "
"number (X301) to steer charging from an EMS or automation."

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Inactive reason misidentifies mode 🐞 Bug ◔ Observability

inactive_reason always says OCPP owns charging even when X120 reports Dashboard, Local whitelist,
External control, Permanent release, or an unknown value. This exposes factually incorrect
diagnostic information for every non-Modbus mode except OCPP.
Agent Prompt
## Issue description
The static inactive-reason text claims that OCPP owns charging for all non-Modbus release modes, although the integration recognizes several distinct modes.

## Issue Context
The availability warning already resolves the actual mode through `RELEASE_MODE`; the entity attribute should similarly use the current mode or use mode-neutral wording. Add attribute tests for modes 0–3 and unknown values, not only OCPP.

## Fix Focus Areas
- custom_components/veton/switch.py[27-30]
- custom_components/veton/switch.py[92-103]
- custom_components/veton/const.py[24-32]
- tests/test_switch.py[88-106]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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.

1 participant