Skip to content

Add OCPP charger support (closes #44)#89

Open
kljakobsen wants to merge 2 commits into
dirkgroenen:mainfrom
kljakobsen:feature/ocpp-charger
Open

Add OCPP charger support (closes #44)#89
kljakobsen wants to merge 2 commits into
dirkgroenen:mainfrom
kljakobsen:feature/ocpp-charger

Conversation

@kljakobsen
Copy link
Copy Markdown

Implements a charger backend for OCPP-managed charge points via the lbbrhzn/ocpp integration. Tested end-to-end on a Garo Entity Pro using a Shelly 3EM as the meter source (as custom meter).

Design notes for anyone maintaining this:

  • OCPP unique-ids are dot-separated (e.g. number.ocpp..maximum_current), so the base HaDevice._get_entity_id_by_key helper's underscore-suffix match never works. OcppCharger ships its own dot-aware _get_ocpp_entity_id that matches by HA domain and by checking the key against the dot-separated unique-id parts.

  • IEC 61851 forbids charging below 6A. The upstream allocator can emit any value 0..max during overcurrent events. set_current_limit snaps any request in (0, 6) to 0A, which OCPP interprets as "pause", and the charger transitions to SuspendedEVSE.

  • The pause must not look like a manual override. The upstream power_allocator.detect_manual_override() compares the slider (0A after the snap) against last_applied_current (the pre-snap value like 4A), would see a mismatch and adopt 0A as the new ceiling -- permanently locking the charger off. OcppCharger tracks _last_requested and, while the charger is in SuspendedEVSE, get_current_limit reports that pre-snap value back so the override detector sees no change.

  • Profile replacement alone does not resume on most chargers. Once SuspendedEVSE, the contactor is de-energised and the firmware waits for a fresh RemoteStartTransaction. When set_current_limit is called with >=6A while paused, we push the new profile first, then toggle the charge_control switch off -> on to issue RemoteStop + RemoteStart and force a new transaction under the just-updated profile.

  • OCPP exposes one charge-point-wide current setting, so has_synced_phase_limits() is always True and set_current_limit uses the lowest of the per-phase values.

  • set_phase_mode is a validated no-op (OCPP 1.6 has no standardised single/three-phase toggle).

Validation:

  • All 33 new OcppCharger tests pass.
  • All 138 charger tests pass.
  • ruff check . clean; ruff format --check . clean.
  • Verified end-to-end on real hardware (Garo Entity Pro via lbbrhzn/ocpp and Shelly 3EM): multiple pause/resume cycles in a single session, no "Manual override detected" log lines during pauses, contactor reliably re-energised on resume.

Files changed:
custom_components/evse_load_balancer/chargers/ocpp_charger.py (new)
custom_components/evse_load_balancer/chargers/init.py (register)
custom_components/evse_load_balancer/config_flow.py (picker)
custom_components/evse_load_balancer/const.py (domain)
tests/chargers/test_ocpp_charger.py (new)
README.md (table)

OCPP Charger PR and others added 2 commits May 24, 2026 17:10
Implements a charger backend for OCPP-managed charge points via the
lbbrhzn/ocpp integration. Tested end-to-end on a Garo CS using a
Shelly 3EM as the meter source.

Design notes for anyone maintaining this:

* OCPP unique-ids are dot-separated
  (e.g. number.ocpp.<cpid>.maximum_current), so the base
  HaDevice._get_entity_id_by_key helper's underscore-suffix match
  never works. OcppCharger ships its own dot-aware
  _get_ocpp_entity_id that matches by HA domain and by checking the
  key against the dot-separated unique-id parts.

* IEC 61851 forbids charging below 6A. The upstream allocator can
  emit any value 0..max during overcurrent events. set_current_limit
  snaps any request in (0, 6) to 0A, which OCPP interprets as
  "pause", and the charger transitions to SuspendedEVSE.

* The pause must not look like a manual override. The upstream
  power_allocator.detect_manual_override() compares the slider
  (0A after the snap) against last_applied_current (the pre-snap
  value like 4A), would see a mismatch and adopt 0A as the new
  ceiling -- permanently locking the charger off. OcppCharger
  tracks _last_requested and, while the charger is in
  SuspendedEVSE, get_current_limit reports that pre-snap value
  back so the override detector sees no change.

* Profile replacement alone does not resume on most chargers.
  Once SuspendedEVSE, the contactor is de-energised and the
  firmware waits for a fresh RemoteStartTransaction. When
  set_current_limit is called with >=6A while paused, we push the
  new profile first, then toggle the charge_control switch
  off -> on to issue RemoteStop + RemoteStart and force a new
  transaction under the just-updated profile.

* OCPP exposes one charge-point-wide current setting, so
  has_synced_phase_limits() is always True and set_current_limit
  uses the lowest of the per-phase values.

* set_phase_mode is a validated no-op (OCPP 1.6 has no
  standardised single/three-phase toggle).

Validation:

* All 33 new OcppCharger tests pass.
* All 138 charger tests pass.
* ruff check . clean; ruff format --check . clean.
* Verified end-to-end on real hardware (Garo CS via lbbrhzn/ocpp
  + Shelly 3EM): multiple pause/resume cycles in a single session,
  no "Manual override detected" log lines during pauses,
  contactor reliably re-energised on resume.

Files changed:
  custom_components/evse_load_balancer/chargers/ocpp_charger.py  (new)
  custom_components/evse_load_balancer/chargers/__init__.py      (register)
  custom_components/evse_load_balancer/config_flow.py            (picker)
  custom_components/evse_load_balancer/const.py                  (domain)
  tests/chargers/test_ocpp_charger.py                            (new)
  README.md                                                      (table)
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