Skip to content

Commit

Permalink
New charge rate service (#1050)
Browse files Browse the repository at this point in the history
* New charge rate service

* Fix schema ref

* Fix positive_int ref

* Fix data string

* Change back to dbl quotes

* Convert dict profile to string for service call

* Tidy up formatting

* Further fixes

* Fix linting

* Fix linting

* Add connector id to service

* Fix linting

* Improve service descriptions

* Fix typo

* Allow dict or str types

* fix linting
  • Loading branch information
drc38 authored Jan 27, 2024
1 parent c586f86 commit 62353b6
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 7 deletions.
60 changes: 57 additions & 3 deletions custom_components/ocpp/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import asyncio
from collections import defaultdict
from datetime import datetime, timedelta, timezone
import json
import logging
from math import sqrt
import ssl
Expand Down Expand Up @@ -142,6 +143,14 @@
vol.Optional("data"): cv.string,
}
)
CHRGR_SERVICE_DATA_SCHEMA = vol.Schema(
{
vol.Optional("limit_amps"): cv.positive_float,
vol.Optional("limit_watts"): cv.positive_int,
vol.Optional("conn_id"): cv.positive_int,
vol.Optional("custom_profile"): vol.Any(cv.string, dict),
}
)


class CentralSystem:
Expand Down Expand Up @@ -425,6 +434,25 @@ async def handle_data_transfer(call):
data = call.data.get("data", "")
await self.data_transfer(vendor, message, data)

async def handle_set_charge_rate(call):
"""Handle the data transfer service call."""
if self.status == STATE_UNAVAILABLE:
_LOGGER.warning("%s charger is currently unavailable", self.id)
return
amps = call.data.get("limit_amps", None)
watts = call.data.get("limit_watts", None)
id = call.data.get("conn_id", 0)
custom_profile = call.data.get("custom_profile", None)
if custom_profile is not None:
if type(custom_profile) is str:
custom_profile = custom_profile.replace("'", '"')
custom_profile = json.loads(custom_profile)
await self.set_charge_rate(profile=custom_profile, conn_id=id)
elif watts is not None:
await self.set_charge_rate(limit_watts=watts, conn_id=id)
elif amps is not None:
await self.set_charge_rate(limit_amps=amps, conn_id=id)

try:
self.status = STATE_OK
await asyncio.sleep(2)
Expand Down Expand Up @@ -493,6 +521,12 @@ async def handle_data_transfer(call):
self.hass.services.async_register(
DOMAIN, csvcs.service_clear_profile.value, handle_clear_profile
)
self.hass.services.async_register(
DOMAIN,
csvcs.service_set_charge_rate.value,
handle_set_charge_rate,
CHRGR_SERVICE_DATA_SCHEMA,
)
if prof.FW in self._attr_supported_features:
self.hass.services.async_register(
DOMAIN,
Expand Down Expand Up @@ -599,8 +633,28 @@ async def clear_profile(self):
)
return False

async def set_charge_rate(self, limit_amps: int = 32, limit_watts: int = 22000):
async def set_charge_rate(
self,
limit_amps: int = 32,
limit_watts: int = 22000,
conn_id: int = 0,
profile: dict | None = None,
):
"""Set a charging profile with defined limit."""
if profile is not None: # assumes advanced user and correct profile format
req = call.SetChargingProfilePayload(
connector_id=conn_id, cs_charging_profiles=profile
)
resp = await self.call(req)
if resp.status == ChargingProfileStatus.accepted:
return True
else:
_LOGGER.warning("Failed with response: %s", resp.status)
await self.notify_ha(
f"Warning: Set charging profile failed with response {resp.status}"
)
return False

if prof.SMART in self._attr_supported_features:
resp = await self.get_configuration(
ckey.charging_schedule_allowed_charging_rate_unit.value
Expand All @@ -621,7 +675,7 @@ async def set_charge_rate(self, limit_amps: int = 32, limit_watts: int = 22000):
)
stack_level = int(resp)
req = call.SetChargingProfilePayload(
connector_id=0,
connector_id=conn_id,
cs_charging_profiles={
om.charging_profile_id.value: 8,
om.stack_level.value: stack_level,
Expand All @@ -647,7 +701,7 @@ async def set_charge_rate(self, limit_amps: int = 32, limit_watts: int = 22000):
)
# try a lower stack level for chargers where level < maximum, not <=
req = call.SetChargingProfilePayload(
connector_id=0,
connector_id=conn_id,
cs_charging_profiles={
om.charging_profile_id.value: 8,
om.stack_level.value: stack_level - 1,
Expand Down
21 changes: 17 additions & 4 deletions custom_components/ocpp/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,36 @@ set_charge_rate:
# Field name as shown in UI
name: Limit (A)
# Description of the field
description: Maximum charge rate in Amps
description: Maximum charge rate in Amps (optional)
# Whether or not field is required (default = false)
required: false
# Advanced fields are only shown when the advanced mode is enabled for the user (default = false)
advanced: true
advanced: false
# Example value that can be passed for this field
example: 16
# The default field value
default: 32
limit_watts:
name: Limit (W)
description: Maximum charge rate in Watts
description: Maximum charge rate in Watts (optional)
required: false
advanced: true
example: 1500
default: 22000

conn_id:
name: Connector identifier
description: Optional, 0 = all connectors (default), 1 is first connector
required: false
advanced: true
example: 0
default: 0
custom_profile:
name: Custom profile
description: Used to send a custom charge profile to charger (for advanced users only use >- or '' to ensure profile is a string variable)
required: false
advanced: true
example: '{"chargingProfileId":8,"stackLevel":0,"chargingProfileKind":"Relative","chargingProfilePurpose":"ChargePointMaxProfile","chargingSchedule":{"chargingRateUnit":"A","chargingSchedulePeriod":[{"startPeriod":0,"limit":16}]}}'

clear_profile:
name: Clear charging profiles
description: Clears all charging profiles (limits) set (dependent on charger support)
Expand Down
28 changes: 28 additions & 0 deletions tests/test_charge_point.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ async def test_services(hass, socket_enabled):
csvcs.service_get_diagnostics,
csvcs.service_clear_profile,
csvcs.service_data_transfer,
csvcs.service_set_charge_rate,
]
for service in SERVICES:
data = {}
Expand All @@ -103,13 +104,40 @@ async def test_services(hass, socket_enabled):
data = {"upload_url": "https://webhook.site/abc"}
if service == csvcs.service_data_transfer:
data = {"vendor_id": "ABC"}
if service == csvcs.service_set_charge_rate:
data = {"limit_amps": 30}

await hass.services.async_call(
OCPP_DOMAIN,
service.value,
service_data=data,
blocking=True,
)
# test additional set charge rate options
await hass.services.async_call(
OCPP_DOMAIN,
csvcs.service_set_charge_rate,
service_data={"limit_watts": 3000},
blocking=True,
)
# test custom charge profile for advanced use
prof = {
"chargingProfileId": 8,
"stackLevel": 6,
"chargingProfileKind": "Relative",
"chargingProfilePurpose": "ChargePointMaxProfile",
"chargingSchedule": {
"chargingRateUnit": "A",
"chargingSchedulePeriod": [{"startPeriod": 0, "limit": 16.0}],
},
}
data = {"custom_profile": str(prof)}
await hass.services.async_call(
OCPP_DOMAIN,
csvcs.service_set_charge_rate,
service_data=data,
blocking=True,
)

for number in NUMBERS:
# test setting value of number slider
Expand Down

0 comments on commit 62353b6

Please sign in to comment.