Skip to content

Commit 205dfe3

Browse files
authored
[bluelink] Support setting AC/DC charge limits (#20663)
* [bluelink] Support setting AC/DC charge limits Signed-off-by: Florian Hotze <dev@florianhotze.com>
1 parent 0124dc7 commit 205dfe3

7 files changed

Lines changed: 163 additions & 13 deletions

File tree

bundles/org.openhab.binding.bluelink/README.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,17 @@ The Force Refresh action can be used to refresh on demand.
6363

6464
Vehicle things support the following actions.
6565

66-
| Action | Parameters | Description |
67-
|-----------------|------------------------------------------------------------|------------------------------------------------------------|
68-
| forceRefresh() | - | Fetch up-to-date data from the vehicle and update channels |
69-
| climateStart() | temperature, heated features, defrost, engine run duration | Start climate control |
70-
| climateStop() | - | Stop climate control |
71-
| lock() | - | Lock the vehicle |
72-
| unlock() | - | Unlock the vehicle |
73-
| startCharging() | - | Start charging the vehicle |
74-
| stopCharging() | - | Stop charging the verhicle |
66+
| Action | Parameters | Description |
67+
|--------------------|------------------------------------------------------------|------------------------------------------------------------|
68+
| forceRefresh() | - | Fetch up-to-date data from the vehicle and update channels |
69+
| climateStart() | temperature, heated features, defrost, engine run duration | Start climate control |
70+
| climateStop() | - | Stop climate control |
71+
| lock() | - | Lock the vehicle |
72+
| unlock() | - | Unlock the vehicle |
73+
| startCharging() | - | Start charging the vehicle (EV only) |
74+
| stopCharging() | - | Stop charging the vehicle (EV only) |
75+
| setChargeLimitDC() | limit (integer) | Set the target DC charge limit (50-100%) (EV only) |
76+
| setChargeLimitAC() | limit (integer) | Set the target AC charge limit (50-100%) (EV only) |
7577

7678
## Channels
7779

@@ -147,8 +149,8 @@ This channel group is only available for electric and hybrid vehicles.
147149
| `soc` | Number:Dimensionless | R | Battery state of charge (%) |
148150
| `charging` | Switch | R | Charging status (ON to start) |
149151
| `plugged-in` | Switch | R | Charge cable plugged in |
150-
| `charge-limit-dc` | Number:Dimensionless | R | DC charge limit (%) |
151-
| `charge-limit-ac` | Number:Dimensionless | R | AC charge limit (%) |
152+
| `charge-limit-dc` | Number:Dimensionless | R/W | DC charge limit (%) |
153+
| `charge-limit-ac` | Number:Dimensionless | R/W | AC charge limit (%) |
152154
| `time-to-full-current` | Number:Time | R | Time to full (current charger) |
153155
| `time-to-full-fast` | Number:Time | R | Time to full (fast charger) |
154156
| `time-to-full-portable` | Number:Time | R | Time to full (portable charger) |

bundles/org.openhab.binding.bluelink/src/main/java/org/openhab/binding/bluelink/internal/handler/BluelinkAccountHandler.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,4 +261,14 @@ public boolean getVehicleStatus(final IVehicle vehicle, final boolean forceRefre
261261
final var api = this.api;
262262
return api != null && api.getVehicleStatus(vehicle, forceRefresh, cb);
263263
}
264+
265+
public boolean setChargeLimitDC(final IVehicle vehicle, final int limit) throws BluelinkApiException {
266+
final var api = this.api;
267+
return api != null && api.setChargeLimitDC(vehicle, limit);
268+
}
269+
270+
public boolean setChargeLimitAC(final IVehicle vehicle, final int limit) throws BluelinkApiException {
271+
final var api = this.api;
272+
return api != null && api.setChargeLimitAC(vehicle, limit);
273+
}
264274
}

bundles/org.openhab.binding.bluelink/src/main/java/org/openhab/binding/bluelink/internal/handler/BluelinkVehicleHandler.java

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,40 @@ public void handleCommand(final ChannelUID channelUID, final Command command) {
167167
if (command instanceof RefreshType) {
168168
// we do not force a refresh from the vehicle because of the low rate limit
169169
refreshVehicleStatus(false);
170+
return;
171+
}
172+
173+
final String channelId = channelUID.getIdWithoutGroup();
174+
try {
175+
if (CHANNEL_CHARGE_LIMIT_DC.equals(channelId)) {
176+
if (command instanceof QuantityType<?> qt) {
177+
qt = qt.toUnit(Units.PERCENT);
178+
if (qt != null) {
179+
setChargeLimitDC(qt.intValue());
180+
} else {
181+
logger.debug("Failed to convert QuantityType to PERCENT!");
182+
}
183+
} else if (command instanceof DecimalType decimal) {
184+
setChargeLimitDC(decimal.intValue());
185+
} else {
186+
logger.debug("Command has wrong type, QuantityType or DecimalType required!");
187+
}
188+
} else if (CHANNEL_CHARGE_LIMIT_AC.equals(channelId)) {
189+
if (command instanceof QuantityType<?> qt) {
190+
qt = qt.toUnit(Units.PERCENT);
191+
if (qt != null) {
192+
setChargeLimitAC(qt.intValue());
193+
} else {
194+
logger.debug("Failed to convert QuantityType to PERCENT!");
195+
}
196+
} else if (command instanceof DecimalType decimal) {
197+
setChargeLimitAC(decimal.intValue());
198+
} else {
199+
logger.debug("Command has wrong type, QuantityType or DecimalType required!");
200+
}
201+
}
202+
} catch (final BluelinkApiException e) {
203+
logger.debug("Failed to set charge limit: {}", e.getMessage());
170204
}
171205
}
172206

@@ -222,6 +256,40 @@ public boolean stopCharging() throws BluelinkApiException {
222256
return res;
223257
}
224258

259+
public boolean setChargeLimitDC(final int limit) throws BluelinkApiException {
260+
if (limit < 50 || limit > 100) {
261+
logger.debug("Invalid DC charge limit: {}. Must be between 50 and 100.", limit);
262+
return false;
263+
}
264+
final var bridgeHnd = getBridgeHandler();
265+
final var vehicle = this.vehicle;
266+
if (vehicle == null || bridgeHnd == null) {
267+
return false;
268+
}
269+
final boolean res = bridgeHnd.setChargeLimitDC(vehicle, limit);
270+
if (res) {
271+
scheduleForceRefresh();
272+
}
273+
return res;
274+
}
275+
276+
public boolean setChargeLimitAC(final int limit) throws BluelinkApiException {
277+
if (limit < 50 || limit > 100) {
278+
logger.debug("Invalid AC charge limit: {}. Must be between 50 and 100.", limit);
279+
return false;
280+
}
281+
final var bridgeHnd = getBridgeHandler();
282+
final var vehicle = this.vehicle;
283+
if (vehicle == null || bridgeHnd == null) {
284+
return false;
285+
}
286+
final boolean res = bridgeHnd.setChargeLimitAC(vehicle, limit);
287+
if (res) {
288+
scheduleForceRefresh();
289+
}
290+
return res;
291+
}
292+
225293
public boolean climateStart(final QuantityType<Temperature> temperature, final boolean heat, final boolean defrost,
226294
final @Nullable Integer igniOnDuration) throws BluelinkApiException {
227295
final var bridgeHnd = getBridgeHandler();

bundles/org.openhab.binding.bluelink/src/main/java/org/openhab/binding/bluelink/internal/handler/VehicleActions.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,30 @@ public boolean stopCharging() {
100100
}
101101
}
102102

103+
@RuleAction(label = "@text/action.set-charge-limit-dc.label")
104+
@ActionOutput(type = "boolean")
105+
public boolean setChargeLimitDC(
106+
final @ActionInput(name = "limit", type = "int", required = true, label = "@text/action.set-charge-limit-dc.input.limit.label") int limit) {
107+
final BluelinkVehicleHandler hnd = handler;
108+
try {
109+
return hnd != null && hnd.setChargeLimitDC(limit);
110+
} catch (final BluelinkApiException e) {
111+
return false;
112+
}
113+
}
114+
115+
@RuleAction(label = "@text/action.set-charge-limit-ac.label")
116+
@ActionOutput(type = "boolean")
117+
public boolean setChargeLimitAC(
118+
final @ActionInput(name = "limit", type = "int", required = true, label = "@text/action.set-charge-limit-ac.input.limit.label") int limit) {
119+
final BluelinkVehicleHandler hnd = handler;
120+
try {
121+
return hnd != null && hnd.setChargeLimitAC(limit);
122+
} catch (final BluelinkApiException e) {
123+
return false;
124+
}
125+
}
126+
103127
@RuleAction(label = "@text/action.climate-start.label")
104128
@ActionOutput(type = "boolean")
105129
public boolean climateStart(
@@ -182,4 +206,20 @@ public static void stopCharging(final @Nullable ThingActions actions) {
182206
throw new IllegalArgumentException("expected VehicleActions");
183207
}
184208
}
209+
210+
public static void setChargeLimitDC(final @Nullable ThingActions actions, final int limit) {
211+
if (actions instanceof VehicleActions va) {
212+
va.setChargeLimitDC(limit);
213+
} else {
214+
throw new IllegalArgumentException("expected VehicleActions");
215+
}
216+
}
217+
218+
public static void setChargeLimitAC(final @Nullable ThingActions actions, final int limit) {
219+
if (actions instanceof VehicleActions va) {
220+
va.setChargeLimitAC(limit);
221+
} else {
222+
throw new IllegalArgumentException("expected VehicleActions");
223+
}
224+
}
185225
}

bundles/org.openhab.binding.bluelink/src/main/resources/OH-INF/i18n/bluelink.properties

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,10 @@ action.lock.label = Lock Vehicle
171171
action.unlock.label = Unlock Vehicle
172172
action.start-charging.label = Start Charging
173173
action.stop-charging.label = Stop Charging
174+
action.set-charge-limit-dc.label = Set DC Charge Limit
175+
action.set-charge-limit-dc.input.limit.label = DC Charge Limit (%)
176+
action.set-charge-limit-ac.label = Set AC Charge Limit
177+
action.set-charge-limit-ac.input.limit.label = AC Charge Limit (%)
174178
action.climate-start.label = Climate Start
175179
action.climate-start.input.temperature.label = Cabin Temperature
176180
action.climate-start.input.heating.label = Heated Features

bundles/org.openhab.binding.bluelink/src/main/resources/OH-INF/thing/vehicle.xml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,8 @@
401401
<tag>Status</tag>
402402
<tag>Energy</tag>
403403
</tags>
404-
<state pattern="%.0f %%" readOnly="true"/>
404+
<state pattern="%.0f %%" readOnly="false" min="50" max="100" step="10"/>
405+
<autoUpdatePolicy>veto</autoUpdatePolicy>
405406
</channel-type>
406407
<channel-type id="charge-limit-ac">
407408
<item-type>Number:Dimensionless</item-type>
@@ -412,7 +413,8 @@
412413
<tag>Status</tag>
413414
<tag>Energy</tag>
414415
</tags>
415-
<state pattern="%.0f %%" readOnly="true"/>
416+
<state pattern="%.0f %%" readOnly="false" min="50" max="100" step="10"/>
417+
<autoUpdatePolicy>veto</autoUpdatePolicy>
416418
</channel-type>
417419

418420
<channel-type id="charge-time-current">

bundles/org.openhab.binding.bluelink/src/test/java/org/openhab/binding/bluelink/internal/handler/BluelinkAccountHandlerTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,5 +201,29 @@ void testLoginAndGetVehicles() throws BluelinkApiException {
201201
final var secondEu = (Vehicle) second;
202202
assertTrue(secondEu.ccs2ProtocolSupport());
203203
}
204+
205+
@Test
206+
void testSetChargeLimitDC() throws BluelinkApiException {
207+
final var vehicleId = "aa6c0ca6-48eb-430c-9eea-902bb6cd281c";
208+
stubFor(post(urlEqualTo("/api/v1/spa/vehicles/" + vehicleId + "/charge/target"))
209+
.withRequestBody(equalToJson("{\"targetSOClist\":[{\"plugType\":0,\"targetSOClevel\":80}]}"))
210+
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
211+
.withBody("{\"retCode\":\"S\"}")));
212+
213+
final var vehicle = handler.getVehicles().getFirst();
214+
assertTrue(handler.setChargeLimitDC(vehicle, 80));
215+
}
216+
217+
@Test
218+
void testSetChargeLimitAC() throws BluelinkApiException {
219+
final var vehicleId = "aa6c0ca6-48eb-430c-9eea-902bb6cd281c";
220+
stubFor(post(urlEqualTo("/api/v1/spa/vehicles/" + vehicleId + "/charge/target"))
221+
.withRequestBody(equalToJson("{\"targetSOClist\":[{\"plugType\":1,\"targetSOClevel\":90}]}"))
222+
.willReturn(aResponse().withStatus(200).withHeader("Content-Type", "application/json")
223+
.withBody("{\"retCode\":\"S\"}")));
224+
225+
final var vehicle = handler.getVehicles().getFirst();
226+
assertTrue(handler.setChargeLimitAC(vehicle, 90));
227+
}
204228
}
205229
}

0 commit comments

Comments
 (0)