Skip to content

Commit 2462a21

Browse files
jthunJan Thunqvist
andauthored
Remove clear profiles from set_charge_rate
Co-authored-by: Jan Thunqvist <[email protected]>
1 parent f43b1d2 commit 2462a21

File tree

4 files changed

+301
-365
lines changed

4 files changed

+301
-365
lines changed

custom_components/ocpp/ocppv16.py

Lines changed: 79 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -377,36 +377,6 @@ async def clear_profile(
377377
_LOGGER.debug("ClearChargingProfile raised %s (ignored)", ex)
378378
return False
379379

380-
def _profile_ids_for(
381-
self, conn_id: int, purpose: str, tx_id: int | None = None
382-
) -> tuple[int, int]:
383-
"""Return (chargingProfileId, stackLevel) unique per (purpose, connector).
384-
385-
- Keeps IDs small and stable across restarts.
386-
- For TxProfile you may include tx_id to avoid clashes if multiple are alive.
387-
"""
388-
PURPOSE_CODE = {
389-
"ChargePointMaxProfile": 1,
390-
"TxDefaultProfile": 2,
391-
"TxProfile": 3,
392-
}
393-
if purpose == "ChargePointMaxProfile":
394-
conn_seg = 0
395-
else:
396-
try:
397-
conn_seg = max(1, int(conn_id or 1))
398-
except Exception:
399-
conn_seg = 1
400-
401-
base = 1000
402-
pid = base + PURPOSE_CODE[purpose] + conn_seg * 10
403-
404-
if purpose == "TxProfile" and tx_id is not None:
405-
pid = pid * 1000 + (int(tx_id) % 1000)
406-
407-
stack_level = 1
408-
return pid, stack_level
409-
410380
async def set_charge_rate(
411381
self,
412382
limit_amps: int = 32,
@@ -417,118 +387,108 @@ async def set_charge_rate(
417387
"""Set charge rate."""
418388
if profile is not None:
419389
try:
420-
resp = await self.call(
421-
call.SetChargingProfile(
422-
connector_id=int(conn_id), cs_charging_profiles=profile
423-
)
390+
req = call.SetChargingProfile(
391+
connector_id=int(conn_id), cs_charging_profiles=profile
424392
)
393+
resp = await self.call(req)
425394
if resp.status == ChargingProfileStatus.accepted:
426395
return True
427396
_LOGGER.warning("Custom SetChargingProfile rejected: %s", resp.status)
428397
except Exception as ex:
429398
_LOGGER.warning("Custom SetChargingProfile failed: %s", ex)
399+
await self.notify_ha(
400+
"Warning: Set charging profile failed with response Exception"
401+
)
402+
return False
403+
404+
if prof.SMART not in self._attr_supported_features:
405+
_LOGGER.info("Smart charging is not supported by this charger")
406+
return False
430407

431-
resp_units = await self.get_configuration(
408+
# Determine allowed unit (default to Amps if not reported)
409+
units_resp = await self.get_configuration(
432410
ckey.charging_schedule_allowed_charging_rate_unit.value
433411
)
434-
if resp_units is None:
435-
_LOGGER.warning("Failed to query charging rate unit, assuming Amps")
436-
resp_units = om.current.value
412+
if not units_resp:
413+
_LOGGER.debug("Charging rate unit not reported; assuming Amps")
414+
units_resp = om.current.value
437415

438-
use_amps = om.current.value in resp_units
439-
limit_val = float(limit_amps if use_amps else limit_watts)
440-
unit_val = (
416+
use_amps = om.current.value in units_resp
417+
limit_value = float(limit_amps if use_amps else limit_watts)
418+
units_value = (
441419
ChargingRateUnitType.amps.value
442420
if use_amps
443421
else ChargingRateUnitType.watts.value
444422
)
445423

446-
# Build attempt order (CPMax -> TxDefault -> TxProfile if active)
447-
attempts: list[tuple[int, str]] = []
448-
attempts.append((0, "ChargePointMaxProfile"))
449-
if conn_id and conn_id > 0:
450-
attempts.append((conn_id, "TxDefaultProfile"))
451-
452-
has_active = bool(getattr(self, "active_transaction_id", 0))
453-
if has_active:
454-
tx_conn = next(
455-
(c for c, tx in getattr(self, "_active_tx", {}).items() if tx),
456-
conn_id or 1,
457-
)
458-
attempts.append((tx_conn, "TxProfile"))
459-
460-
await self.clear_profile(
461-
None, ChargingProfilePurposeType.charge_point_max_profile
462-
)
463-
if conn_id and conn_id > 0:
464-
await self.clear_profile(
465-
conn_id, ChargingProfilePurposeType.tx_default_profile
424+
# Read max stack level (default to 1 on parse errors)
425+
try:
426+
stack_level_resp = await self.get_configuration(
427+
ckey.charge_profile_max_stack_level.value
466428
)
429+
stack_level = int(stack_level_resp)
430+
except Exception:
431+
stack_level = 1
467432

468-
def _mk_profile(purpose: str, cid: int) -> dict:
469-
tx_id = (
470-
self.active_transaction_id
471-
if (purpose == "TxProfile" and has_active)
472-
else None
473-
)
474-
pid, stack = self._profile_ids_for(cid, purpose, tx_id=tx_id)
433+
# Helper to build a simple relative schedule with one period
434+
def _mk_schedule(_units: str, _limit: float) -> dict:
475435
return {
476-
om.charging_profile_id.value: pid,
477-
om.stack_level.value: stack,
478-
om.charging_profile_kind.value: ChargingProfileKindType.relative.value,
479-
om.charging_profile_purpose.value: purpose,
480-
om.charging_schedule.value: {
481-
om.charging_rate_unit.value: unit_val,
482-
om.charging_schedule_period.value: [
483-
{om.start_period.value: 0, om.limit.value: limit_val}
484-
],
485-
},
436+
om.charging_rate_unit.value: _units,
437+
om.charging_schedule_period.value: [
438+
{om.start_period.value: 0, om.limit.value: _limit}
439+
],
486440
}
487441

488-
# Try each purpose/connector in order; optionally clear-by-id before setting
489-
last_status = None
490-
for cid, purpose in attempts:
491-
try:
492-
try:
493-
tx_id = (
494-
self.active_transaction_id
495-
if (purpose == "TxProfile" and has_active)
496-
else None
497-
)
498-
pid, _ = self._profile_ids_for(cid, purpose, tx_id=tx_id)
499-
await self.call(call.ClearChargingProfile(id=pid))
500-
except Exception:
501-
pass
502-
503-
req = call.SetChargingProfile(
504-
connector_id=cid, cs_charging_profiles=_mk_profile(purpose, cid)
505-
)
506-
resp = await self.call(req)
507-
last_status = resp.status
508-
if resp.status == ChargingProfileStatus.accepted:
509-
_LOGGER.debug(
510-
"SetChargingProfile accepted with purpose=%s connectorId=%s",
511-
purpose,
512-
cid,
513-
)
514-
return True
515-
_LOGGER.debug(
516-
"SetChargingProfile %s on connector %s -> %s",
517-
purpose,
518-
cid,
519-
resp.status,
520-
)
521-
except Exception as ex:
522-
_LOGGER.debug(
523-
"SetChargingProfile %s on connector %s raised %s", purpose, cid, ex
524-
)
442+
# Try ChargePointMaxProfile (connectorId = 0)
443+
try:
444+
req = call.SetChargingProfile(
445+
connector_id=0,
446+
cs_charging_profiles={
447+
om.charging_profile_id.value: 8,
448+
om.stack_level.value: stack_level,
449+
om.charging_profile_kind.value: ChargingProfileKindType.relative.value,
450+
om.charging_profile_purpose.value: ChargingProfilePurposeType.charge_point_max_profile.value,
451+
om.charging_schedule.value: _mk_schedule(units_value, limit_value),
452+
},
453+
)
454+
resp = await self.call(req)
455+
if resp.status == ChargingProfileStatus.accepted:
456+
return True
457+
_LOGGER.debug(
458+
"ChargePointMaxProfile not accepted (%s); will try TxDefaultProfile.",
459+
resp.status,
460+
)
461+
except Exception as ex:
462+
_LOGGER.debug("ChargePointMaxProfile call raised: %s", ex)
525463

526-
if last_status is not None:
527-
_LOGGER.warning("SetChargingProfile failed (last status=%s).", last_status)
464+
# Fallback: TxDefaultProfile on target connector
465+
# If no connector given, prefer 1 as a reasonable default.
466+
target_cid = int(conn_id) if conn_id and int(conn_id) > 0 else 1
467+
try:
468+
# Some chargers are picky: try a slightly lower stack level if possible.
469+
tx_stack = max(1, stack_level - 1)
470+
req = call.SetChargingProfile(
471+
connector_id=target_cid,
472+
cs_charging_profiles={
473+
om.charging_profile_id.value: 8,
474+
om.stack_level.value: tx_stack,
475+
om.charging_profile_kind.value: ChargingProfileKindType.relative.value,
476+
om.charging_profile_purpose.value: ChargingProfilePurposeType.tx_default_profile.value,
477+
om.charging_schedule.value: _mk_schedule(units_value, limit_value),
478+
},
479+
)
480+
resp = await self.call(req)
481+
if resp.status == ChargingProfileStatus.accepted:
482+
return True
483+
_LOGGER.warning("Set TxDefaultProfile rejected: %s", resp.status)
528484
await self.notify_ha(
529-
f"SetChargingProfile failed (last status={last_status})."
485+
f"Warning: Set charging profile failed with response {resp.status}"
530486
)
531-
return False
487+
return False
488+
except Exception as ex:
489+
_LOGGER.warning("Set TxDefaultProfile failed: %s", ex)
490+
await self.notify_ha(f"Warning: Set charging profile failed: {ex}")
491+
return False
532492

533493
async def set_availability(self, state: bool = True, connector_id: int | None = 0):
534494
"""Change availability."""

tests/test_additional_charge_point_v16.py

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -414,38 +414,6 @@ async def test_trigger_custom_message_unsupported_name(
414414
await ws.close()
415415

416416

417-
@pytest.mark.timeout(5)
418-
@pytest.mark.parametrize(
419-
"setup_config_entry",
420-
[{"port": 9318, "cp_id": "CP_cov_profile_ids", "cms": "cms_services"}],
421-
indirect=True,
422-
)
423-
@pytest.mark.parametrize("cp_id", ["CP_cov_profile_ids"])
424-
@pytest.mark.parametrize("port", [9318])
425-
async def test_profile_ids_for_bad_conn_id_cast(
426-
hass, socket_enabled, cp_id, port, setup_config_entry
427-
):
428-
"""Test profile ids path when conn_id cast fails and conn_seg defaults to 1."""
429-
cs = setup_config_entry
430-
async with websockets.connect(
431-
f"ws://127.0.0.1:{port}/{cp_id}", subprotocols=["ocpp1.6"]
432-
) as ws:
433-
cp = ChargePoint(f"{cp_id}_client", ws)
434-
task = asyncio.create_task(cp.start())
435-
try:
436-
await cp.send_boot_notification()
437-
await wait_ready(cs.charge_points[cp_id])
438-
srv = cs.charge_points[cp_id]
439-
pid, level = srv._profile_ids_for(conn_id="X", purpose="TxDefaultProfile")
440-
# conn_seg should fall back to 1 -> pid = 1000 + 2 + (1*10) = 1012
441-
assert (pid, level) == (1012, 1)
442-
finally:
443-
task.cancel()
444-
with contextlib.suppress(asyncio.CancelledError):
445-
await task
446-
await ws.close()
447-
448-
449417
@pytest.mark.timeout(10)
450418
@pytest.mark.parametrize(
451419
"setup_config_entry",

0 commit comments

Comments
 (0)