2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.growatt.internal.cloud;
15 import java.lang.reflect.Type;
16 import java.net.HttpCookie;
17 import java.security.MessageDigest;
18 import java.security.NoSuchAlgorithmException;
19 import java.time.DateTimeException;
20 import java.time.Duration;
21 import java.time.LocalTime;
22 import java.util.ArrayList;
23 import java.util.LinkedHashMap;
24 import java.util.List;
26 import java.util.Objects;
27 import java.util.Optional;
28 import java.util.concurrent.ConcurrentHashMap;
29 import java.util.concurrent.ExecutionException;
30 import java.util.concurrent.TimeUnit;
31 import java.util.concurrent.TimeoutException;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.eclipse.jetty.client.HttpClient;
36 import org.eclipse.jetty.client.api.ContentResponse;
37 import org.eclipse.jetty.client.api.Request;
38 import org.eclipse.jetty.client.util.FormContentProvider;
39 import org.eclipse.jetty.http.HttpMethod;
40 import org.eclipse.jetty.http.HttpStatus;
41 import org.eclipse.jetty.util.Fields;
42 import org.openhab.binding.growatt.internal.GrowattBindingConstants;
43 import org.openhab.binding.growatt.internal.config.GrowattBridgeConfiguration;
44 import org.openhab.binding.growatt.internal.dto.GrowattDevice;
45 import org.openhab.binding.growatt.internal.dto.GrowattPlant;
46 import org.openhab.binding.growatt.internal.dto.GrowattPlantList;
47 import org.openhab.binding.growatt.internal.dto.GrowattUser;
48 import org.openhab.core.io.net.http.HttpClientFactory;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
52 import com.google.gson.Gson;
53 import com.google.gson.JsonArray;
54 import com.google.gson.JsonElement;
55 import com.google.gson.JsonObject;
56 import com.google.gson.JsonParser;
57 import com.google.gson.JsonPrimitive;
58 import com.google.gson.JsonSyntaxException;
59 import com.google.gson.reflect.TypeToken;
62 * The {@link GrowattCloud} class allows the binding to access the inverter state and settings via HTTP calls to the
63 * remote Growatt cloud API server (instead of receiving the data from the local Grott proxy server).
65 * This class is necessary since the Grott proxy server does not (yet) support easy access to some inverter register
66 * settings, such as the settings for the battery charging and discharging programs.
68 * @author Andrew Fiddian-Green - Initial contribution
71 public class GrowattCloud implements AutoCloseable {
73 // JSON field names for the battery charging program
74 public static final String CHARGE_PROGRAM_POWER = "chargePowerCommand";
75 public static final String CHARGE_PROGRAM_TARGET_SOC = "wchargeSOCLowLimit2";
76 public static final String CHARGE_PROGRAM_ALLOW_AC_CHARGING = "acChargeEnable";
77 public static final String CHARGE_PROGRAM_START_TIME = "forcedChargeTimeStart1";
78 public static final String CHARGE_PROGRAM_STOP_TIME = "forcedChargeTimeStop1";
79 public static final String CHARGE_PROGRAM_ENABLE = "forcedChargeStopSwitch1";
81 // JSON field names for the battery discharging program
82 public static final String DISCHARGE_PROGRAM_POWER = "disChargePowerCommand";
83 public static final String DISCHARGE_PROGRAM_TARGET_SOC = "wdisChargeSOCLowLimit2";
84 public static final String DISCHARGE_PROGRAM_START_TIME = "forcedDischargeTimeStart1";
85 public static final String DISCHARGE_PROGRAM_STOP_TIME = "forcedDischargeTimeStop1";
86 public static final String DISCHARGE_PROGRAM_ENABLE = "forcedDischargeStopSwitch1";
89 private static final String SERVER_URL = "https://server-api.growatt.com/";
92 private static final String LOGIN_API_ENDPOINT = "newTwoLoginAPI.do";
93 private static final String PLANT_LIST_API_ENDPOINT = "PlantListAPI.do";
94 private static final String PLANT_INFO_API_ENDPOINT = "newTwoPlantAPI.do";
95 private static final String NEW_TCP_SET_API_ENDPOINT = "newTcpsetAPI.do";
97 private static final String FMT_NEW_DEVICE_TYPE_API_DO = "new%sApi.do";
100 private static final String OP_GET_ALL_DEVICE_LIST = "getAllDeviceList";
102 // enum of device types
103 private static enum DeviceType {
113 * Map of device types vs. field parameters for GET requests to FMT_NEW_DEVICE_TYPE_API_DO end-points.
114 * Note: some values are guesses which have not yet been confirmed by users
116 private static final Map<DeviceType, String> SUPPORTED_TYPES_GET_PARAM = Map.of(
118 DeviceType.MIX, "getMixSetParams",
119 DeviceType.MAX, "getMaxSetData",
120 DeviceType.MIN, "getMinSetData",
121 DeviceType.SPA, "getSpaSetData",
122 DeviceType.SPH, "getSphSetData",
123 DeviceType.TLX, "getTlxSetData"
128 * Map of device types vs. field parameters for POST commands to NEW_TCP_SET_API_ENDPOINT.
129 * Note: some values are guesses which have not yet been confirmed by users
131 private static final Map<DeviceType, String> SUPPORTED_TYPE_POST_PARAM = Map.of(
133 DeviceType.MIX, "mixSetApiNew", // was "mixSetApi"
134 DeviceType.MAX, "maxSetApi",
135 DeviceType.MIN, "minSetApi",
136 DeviceType.SPA, "spaSetApi",
137 DeviceType.SPH, "sphSet",
138 DeviceType.TLX, "tlxSet"
142 // enum to select charge resp. discharge program
143 private static enum ProgramType {
148 // enum of program modes
149 public static enum ProgramMode {
156 private static final Type DEVICE_LIST_TYPE = new TypeToken<List<GrowattDevice>>() {}.getType();
159 // HTTP headers (user agent is spoofed to mimic the Growatt Android Shine app)
160 private static final String USER_AGENT = "Dalvik/2.1.0 (Linux; U; Android 12; https://www.openhab.org)";
161 private static final String FORM_CONTENT = "application/x-www-form-urlencoded";
163 private static final Duration HTTP_TIMEOUT = Duration.ofSeconds(10);
165 private final Logger logger = LoggerFactory.getLogger(GrowattCloud.class);
166 private final HttpClient httpClient;
167 private final GrowattBridgeConfiguration configuration;
168 private final Gson gson = new Gson();
169 private final List<String> plantIds = new ArrayList<>();
170 private final Map<String, DeviceType> deviceIdTypeMap = new ConcurrentHashMap<>();
172 private String userId = "";
177 * @param configuration the bridge configuration parameters.
178 * @param httpClientFactory the OH core {@link HttpClientFactory} instance.
179 * @throws Exception if anything goes wrong.
181 public GrowattCloud(GrowattBridgeConfiguration configuration, HttpClientFactory httpClientFactory)
183 this.configuration = configuration;
184 this.httpClient = httpClientFactory.createHttpClient(GrowattBindingConstants.BINDING_ID);
185 this.httpClient.start();
189 public void close() throws Exception {
194 * Create a hash of the given password using normal MD5, except add 'c' if a byte of the digest is less than 10
196 * @param password the plain text password
197 * @return the hash of the password
198 * @throws GrowattApiException if MD5 algorithm is not supported
200 private static String createHash(String password) throws GrowattApiException {
203 md = MessageDigest.getInstance("MD5");
204 } catch (NoSuchAlgorithmException e) {
205 throw new GrowattApiException("Hash algorithm error", e);
207 byte[] bytes = md.digest(password.getBytes());
208 StringBuilder result = new StringBuilder();
209 for (byte b : bytes) {
210 result.append(String.format("%02x", b));
212 for (int i = 0; i < result.length(); i += 2) {
213 if (result.charAt(i) == '0') {
214 result.replace(i, i + 1, "c");
217 return result.toString();
221 * Refresh the login cookies.
223 * @throws GrowattApiException if any error occurs.
225 private void refreshCookies() throws GrowattApiException {
226 List<HttpCookie> cookies = httpClient.getCookieStore().getCookies();
227 if (cookies.isEmpty() || cookies.stream().anyMatch(HttpCookie::hasExpired)) {
228 postLoginCredentials();
233 * Login to the server (if necessary) and then execute an HTTP request using the given HTTP method, to the given end
234 * point, and with the given request URL parameters and/or request form fields. If the cookies are not valid first
235 * login to the server before making the actual HTTP request.
237 * @param method the HTTP method to use.
238 * @param endPoint the API end point.
239 * @param params the request URL parameters (may be null).
240 * @param fields the request form fields (may be null).
241 * @return a Map of JSON elements containing the server response.
242 * @throws GrowattApiException if any error occurs.
244 private Map<String, JsonElement> doHttpRequest(HttpMethod method, String endPoint,
245 @Nullable Map<String, String> params, @Nullable Fields fields) throws GrowattApiException {
247 return doHttpRequestInner(method, endPoint, params, fields);
251 * Inner method to execute an HTTP request using the given HTTP method, to the given end point, and with the given
252 * request URL parameters and/or request form fields.
254 * @param method the HTTP method to use.
255 * @param endPoint the API end point.
256 * @param params the request URL parameters (may be null).
257 * @param fields the request form fields (may be null).
258 * @return a Map of JSON elements containing the server response.
259 * @throws GrowattApiException if any error occurs.
261 private Map<String, JsonElement> doHttpRequestInner(HttpMethod method, String endPoint,
262 @Nullable Map<String, String> params, @Nullable Fields fields) throws GrowattApiException {
264 Request request = httpClient.newRequest(SERVER_URL + endPoint).method(method).agent(USER_AGENT)
265 .timeout(HTTP_TIMEOUT.getSeconds(), TimeUnit.SECONDS);
267 if (params != null) {
268 params.entrySet().forEach(p -> request.param(p.getKey(), p.getValue()));
271 if (fields != null) {
272 request.content(new FormContentProvider(fields), FORM_CONTENT);
275 if (logger.isTraceEnabled()) {
276 logger.trace("{} {}{} {} {}", method, request.getPath(), params == null ? "" : "?" + request.getQuery(),
277 request.getVersion(), fields == null ? "" : "? " + FormContentProvider.convert(fields));
280 ContentResponse response;
282 response = request.send();
283 } catch (InterruptedException | TimeoutException | ExecutionException e) {
284 throw new GrowattApiException("HTTP I/O Exception", e);
287 int status = response.getStatus();
288 String content = response.getContentAsString();
290 logger.trace("HTTP {} {} {}", status, HttpStatus.getMessage(status), content);
292 if (status != HttpStatus.OK_200) {
293 throw new GrowattApiException(String.format("HTTP %d %s", status, HttpStatus.getMessage(status)));
296 if (content == null || content.isBlank()) {
297 throw new GrowattApiException("Response is " + (content == null ? "null" : "blank"));
300 if (content.contains("<html>")) {
301 logger.warn("HTTP {} {} {}", status, HttpStatus.getMessage(status), content);
302 throw new GrowattApiException("Response is HTML");
306 JsonElement jsonObject = JsonParser.parseString(content).getAsJsonObject();
307 if (jsonObject instanceof JsonObject jsonElement) {
308 return jsonElement.asMap();
310 throw new GrowattApiException("Response JSON invalid");
311 } catch (JsonSyntaxException | IllegalStateException e) {
312 throw new GrowattApiException("Response JSON syntax exception", e);
317 * Get the deviceType for the given deviceId. If the deviceIdTypeMap is empty then download it freshly.
319 * @param the deviceId to get.
320 * @return the deviceType.
321 * @throws GrowattApiException if any error occurs.
323 private DeviceType getDeviceTypeChecked(String deviceId) throws GrowattApiException {
324 if (deviceIdTypeMap.isEmpty()) {
325 if (plantIds.isEmpty()) {
328 for (String plantId : plantIds) {
329 for (GrowattDevice device : getPlantInfo(plantId)) {
331 deviceIdTypeMap.put(device.getId(), DeviceType.valueOf(device.getType().toUpperCase()));
332 } catch (IllegalArgumentException e) {
333 // just ignore unsupported device types
337 logger.debug("Downloaded deviceTypes:{}", deviceIdTypeMap);
339 if (deviceId.isBlank()) {
340 throw new GrowattApiException("Device id is blank");
342 DeviceType deviceType = deviceIdTypeMap.get(deviceId);
343 if (deviceType != null) {
346 throw new GrowattApiException("Unsupported device:" + deviceId);
350 * Get the inverter device settings.
352 * @param the deviceId to get.
353 * @return a Map of JSON elements containing the server response.
354 * @throws GrowattApiException if any error occurs.
356 public Map<String, JsonElement> getDeviceSettings(String deviceId) throws GrowattApiException {
357 DeviceType deviceType = getDeviceTypeChecked(deviceId);
358 String dt = deviceType.name().toLowerCase();
360 String endPoint = String.format(FMT_NEW_DEVICE_TYPE_API_DO, dt.substring(0, 1).toUpperCase() + dt.substring(1));
362 Map<String, String> params = new LinkedHashMap<>(); // keep params in order
363 params.put("op", Objects.requireNonNull(SUPPORTED_TYPES_GET_PARAM.get(deviceType)));
364 params.put("serialNum", deviceId);
365 params.put("kind", "0");
367 Map<String, JsonElement> result = doHttpRequest(HttpMethod.GET, endPoint, params, null);
369 JsonElement obj = result.get("obj");
370 if (obj instanceof JsonObject object) {
371 Map<String, JsonElement> map = object.asMap();
372 Optional<String> key = map.keySet().stream().filter(k -> k.toLowerCase().endsWith("bean")).findFirst();
373 if (key.isPresent()) {
374 JsonElement beanJson = map.get(key.get());
375 if (beanJson instanceof JsonObject bean) {
380 throw new GrowattApiException("Invalid JSON response");
384 * Get the plant information.
386 * @param the plantId to get.
387 * @return a list of {@link GrowattDevice} containing the server response.
388 * @throws GrowattApiException if any error occurs.
390 public List<GrowattDevice> getPlantInfo(String plantId) throws GrowattApiException {
391 Map<String, String> params = new LinkedHashMap<>(); // keep params in order
392 params.put("op", OP_GET_ALL_DEVICE_LIST);
393 params.put("plantId", plantId);
394 params.put("pageNum", "1");
395 params.put("pageSize", "1");
397 Map<String, JsonElement> result = doHttpRequest(HttpMethod.GET, PLANT_INFO_API_ENDPOINT, params, null);
399 JsonElement deviceList = result.get("deviceList");
400 if (deviceList instanceof JsonArray deviceArray) {
402 List<GrowattDevice> devices = gson.fromJson(deviceArray, DEVICE_LIST_TYPE);
403 if (devices != null) {
406 } catch (JsonSyntaxException e) {
410 throw new GrowattApiException("Invalid JSON response");
414 * Get the plant list.
416 * @param the userId to get from.
417 * @return a {@link GrowattPlantList} containing the server response.
418 * @throws GrowattApiException if any error occurs.
420 public GrowattPlantList getPlantList(String userId) throws GrowattApiException {
421 Map<String, String> params = new LinkedHashMap<>(); // keep params in order
422 params.put("userId", userId);
424 Map<String, JsonElement> result = doHttpRequest(HttpMethod.GET, PLANT_LIST_API_ENDPOINT, params, null);
426 JsonElement back = result.get("back");
427 if (back instanceof JsonObject backObject) {
429 GrowattPlantList plantList = gson.fromJson(backObject, GrowattPlantList.class);
430 if (plantList != null && plantList.getSuccess()) {
433 } catch (JsonSyntaxException e) {
437 throw new GrowattApiException("Invalid JSON response");
441 * Attempt to login to the remote server by posting the given user credentials.
443 * @throws GrowattApiException if any error occurs.
445 private void postLoginCredentials() throws GrowattApiException {
446 String userName = configuration.userName;
447 if (userName == null || userName.isBlank()) {
448 throw new GrowattApiException("User name missing");
450 String password = configuration.password;
451 if (password == null || password.isBlank()) {
452 throw new GrowattApiException("Password missing");
455 Fields fields = new Fields();
456 fields.put("userName", userName);
457 fields.put("password", createHash(password));
459 Map<String, JsonElement> result = doHttpRequestInner(HttpMethod.POST, LOGIN_API_ENDPOINT, null, fields);
461 JsonElement back = result.get("back");
462 if (back instanceof JsonObject backObject) {
464 GrowattPlantList plantList = gson.fromJson(backObject, GrowattPlantList.class);
465 if (plantList != null && plantList.getSuccess()) {
466 GrowattUser user = plantList.getUserId();
467 userId = user != null ? user.getId() : userId;
469 plantIds.addAll(plantList.getPlants().stream().map(GrowattPlant::getId).toList());
470 logger.debug("Logged in userId:{}, plantIds:{}", userId, plantIds);
473 } catch (JsonSyntaxException e) {
477 throw new GrowattApiException("Login failed");
481 * Post a command to setup the inverter battery charging program.
483 * @param the deviceId to set up
484 * @param programModeInt index of the type of program Load First (0) / Battery First (1) / Grid First (2)
485 * @param powerLevel the rate of charging / discharging
486 * @param stopSOC the SOC at which to stop charging / discharging
487 * @param enableAcCharging allow charging from AC power
488 * @param startTime the start time of the charging / discharging program
489 * @param stopTime the stop time of the charging / discharging program
490 * @param enableProgram charging / discharging program shall be enabled
492 * @throws GrowattApiException if any error occurs
494 public void setupBatteryProgram(String deviceId, int programModeInt, @Nullable Integer powerLevel,
495 @Nullable Integer stopSOC, @Nullable Boolean enableAcCharging, @Nullable String startTime,
496 @Nullable String stopTime, @Nullable Boolean enableProgram) throws GrowattApiException {
498 if (deviceId.isBlank()) {
499 throw new GrowattApiException("Device id is blank");
502 ProgramMode programMode;
504 programMode = ProgramMode.values()[programModeInt];
505 } catch (IndexOutOfBoundsException e) {
506 throw new GrowattApiException("Program mode is out of range (0..2)");
509 DeviceType deviceType = getDeviceTypeChecked(deviceId);
510 switch (deviceType) {
514 setTimeProgram(deviceId, deviceType,
515 programMode == ProgramMode.BATTERY_FIRST ? ProgramType.CHARGE : ProgramType.DISCHARGE,
516 powerLevel, stopSOC, enableAcCharging, startTime, stopTime, enableProgram);
520 if (enableAcCharging != null) {
521 setEnableAcCharging(deviceId, deviceType, enableAcCharging);
523 if (powerLevel != null) {
524 setPowerLevel(deviceId, deviceType, programMode, powerLevel);
526 if (stopSOC != null) {
527 setStopSOC(deviceId, deviceType, programMode, stopSOC);
529 if (startTime != null || stopTime != null || enableProgram != null) {
530 setTimeSegment(deviceId, deviceType, programMode, startTime, stopTime, enableProgram);
536 throw new GrowattApiException("Unsupported device type:" + deviceType.name());
540 * Look for an entry in the given Map, and return its value as a boolean.
542 * @param map the source map.
543 * @param key the key to search for in the map.
544 * @return the boolean value.
545 * @throws GrowattApiException if any error occurs.
547 public static boolean mapGetBoolean(Map<String, JsonElement> map, String key) throws GrowattApiException {
548 JsonElement element = map.get(key);
549 if (element instanceof JsonPrimitive primitive) {
550 if (primitive.isBoolean()) {
551 return primitive.getAsBoolean();
552 } else if (primitive.isNumber() || primitive.isString()) {
554 switch (primitive.getAsInt()) {
560 } catch (NumberFormatException e) {
561 throw new GrowattApiException("Boolean bad value", e);
565 throw new GrowattApiException("Boolean missing or bad value");
569 * Look for an entry in the given Map, and return its value as an integer.
571 * @param map the source map.
572 * @param key the key to search for in the map.
573 * @return the integer value.
574 * @throws GrowattApiException if any error occurs.
576 public static int mapGetInteger(Map<String, JsonElement> map, String key) throws GrowattApiException {
577 JsonElement element = map.get(key);
578 if (element instanceof JsonPrimitive primitive) {
580 return primitive.getAsInt();
581 } catch (NumberFormatException e) {
582 throw new GrowattApiException("Integer bad value", e);
585 throw new GrowattApiException("Integer missing or bad value");
589 * Look for an entry in the given Map, and return its value as a LocalTime.
591 * @param source the source map.
592 * @param key the key to search for in the map.
593 * @return the LocalTime.
594 * @throws GrowattApiException if any error occurs.
596 public static LocalTime mapGetLocalTime(Map<String, JsonElement> source, String key) throws GrowattApiException {
597 JsonElement element = source.get(key);
598 if ((element instanceof JsonPrimitive primitive) && primitive.isString()) {
600 return localTimeOf(primitive.getAsString());
601 } catch (DateTimeException e) {
602 throw new GrowattApiException("LocalTime bad value", e);
605 throw new GrowattApiException("LocalTime missing or bad value");
609 * Parse a time formatted string into a LocalTime entity.
611 * Note: unlike the standard LocalTime.parse() method, this method accepts hour and minute fields from the Growatt
612 * server that are without leading zeros e.g. "1:1" and it accepts the conventional "01:01" format too.
614 * @param localTime a time formatted string e.g. "12:34"
615 * @return a corresponding LocalTime entity.
616 * @throws DateTimeException if any error occurs.
618 public static LocalTime localTimeOf(String localTime) throws DateTimeException {
619 String splitParts[] = localTime.split(":");
620 if (splitParts.length < 2) {
621 throw new DateTimeException("LocalTime bad value");
624 return LocalTime.of(Integer.valueOf(splitParts[0]), Integer.valueOf(splitParts[1]));
625 } catch (NumberFormatException | DateTimeException e) {
626 throw new DateTimeException("LocalTime bad value", e);
631 * Post a command to set up the inverter battery charging / discharging program.
633 * @param the deviceId to set up
634 * @param the deviceType to set up
635 * @param programType selects whether the program is for charge or discharge
636 * @param powerLevel the rate of charging / discharging 1%..100%
637 * @param stopSOC the SOC at which to stop the program 5%..100%
638 * @param enableAcCharging allow charging from AC power (only applies to hybrid/mix inverters)
639 * @param startTime the start time of the program
640 * @param stopTime the stop time of the program
641 * @param enableProgram the program shall be enabled
643 * @throws GrowattApiException if any error occurs
645 private void setTimeProgram(String deviceId, DeviceType deviceType, ProgramType programType,
646 @Nullable Integer powerLevel, @Nullable Integer stopSOC, @Nullable Boolean enableAcCharging,
647 @Nullable String startTime, @Nullable String stopTime, @Nullable Boolean enableProgram)
648 throws GrowattApiException {
650 if (powerLevel == null || powerLevel < 1 || powerLevel > 100) {
651 throw new GrowattApiException("Power level parameter is null or out of range (1%..100%)");
653 if (stopSOC == null || stopSOC < 5 || stopSOC > 100) {
654 throw new GrowattApiException("Target SOC parameter is null out of range (5%..100%)");
656 if (startTime == null) {
657 throw new GrowattApiException("Start time parameter is null");
659 if (stopTime == null) {
660 throw new GrowattApiException("Stop time parameter is null");
662 if (enableProgram == null) {
663 throw new GrowattApiException("Program enable parameter is null");
665 boolean isMixChargeCommand = deviceType == DeviceType.MIX && programType == ProgramType.CHARGE;
666 if (isMixChargeCommand && enableAcCharging == null) {
667 throw new GrowattApiException("Allow ac charging parameter is null");
669 LocalTime localStartTime;
671 localStartTime = GrowattCloud.localTimeOf(startTime);
672 } catch (DateTimeException e) {
673 throw new GrowattApiException("Start time is invalid");
675 LocalTime localStopTime;
677 localStopTime = GrowattCloud.localTimeOf(stopTime);
678 } catch (DateTimeException e) {
679 throw new GrowattApiException("Stop time is invalid");
682 Fields fields = new Fields();
684 fields.put("op", Objects.requireNonNull(SUPPORTED_TYPE_POST_PARAM.get(deviceType)));
685 fields.put("serialNum", deviceId);
686 fields.put("type", String.format("%s_ac_%s_time_period", deviceType.name().toLowerCase(),
687 programType.name().toLowerCase()));
691 paramId = addParam(fields, paramId, String.format("%d", powerLevel));
692 paramId = addParam(fields, paramId, String.format("%d", stopSOC));
693 if (isMixChargeCommand) {
694 paramId = addParam(fields, paramId, enableAcCharging ? "1" : "0");
696 paramId = addParam(fields, paramId, String.format("%02d", localStartTime.getHour()));
697 paramId = addParam(fields, paramId, String.format("%02d", localStartTime.getMinute()));
698 paramId = addParam(fields, paramId, String.format("%02d", localStopTime.getHour()));
699 paramId = addParam(fields, paramId, String.format("%02d", localStopTime.getMinute()));
700 paramId = addParam(fields, paramId, enableProgram ? "1" : "0");
701 paramId = addParam(fields, paramId, "00");
702 paramId = addParam(fields, paramId, "00");
703 paramId = addParam(fields, paramId, "00");
704 paramId = addParam(fields, paramId, "00");
705 paramId = addParam(fields, paramId, "0");
706 paramId = addParam(fields, paramId, "00");
707 paramId = addParam(fields, paramId, "00");
708 paramId = addParam(fields, paramId, "00");
709 paramId = addParam(fields, paramId, "00");
710 paramId = addParam(fields, paramId, "0");
712 postSetCommandForm(fields);
716 * Add a new entry in the given {@link Fields} map in the form "paramN" = paramValue where N is the parameter index.
718 * @param fields the map to be added to.
719 * @param parameterIndex the parameter index.
720 * @param parameterValue the parameter value.
722 * @return the next parameter index.
724 private int addParam(Fields fields, int parameterIndex, String parameterValue) {
725 fields.put(String.format("param%d", parameterIndex), parameterValue);
726 return parameterIndex + 1;
730 * Inner method to execute a POST setup command using the given form fields.
732 * @param fields the form fields to be posted.
734 * @throws GrowattApiException if any error occurs
736 private void postSetCommandForm(Fields fields) throws GrowattApiException {
737 Map<String, JsonElement> result = doHttpRequest(HttpMethod.POST, NEW_TCP_SET_API_ENDPOINT, null, fields);
738 JsonElement success = result.get("success");
739 if (success instanceof JsonPrimitive sucessPrimitive) {
740 if (sucessPrimitive.getAsBoolean()) {
744 throw new GrowattApiException("Command failed");
748 * Post a command to enable / disable ac charging.
750 * @param the deviceId to set up
751 * @param the deviceType to set up
752 * @param enableAcCharging enable or disable the function
754 * @throws GrowattApiException if any error occurs
756 private void setEnableAcCharging(String deviceId, DeviceType deviceType, boolean enableAcCharging)
757 throws GrowattApiException {
759 Fields fields = new Fields();
761 fields.put("op", Objects.requireNonNull(SUPPORTED_TYPE_POST_PARAM.get(deviceType)));
762 fields.put("serialNum", deviceId);
763 fields.put("type", "ac_charge");
764 fields.put("param1", enableAcCharging ? "1" : "0");
766 postSetCommandForm(fields);
770 * Post a command to set up a program charge / discharge power level.
772 * @param the deviceId to set up
773 * @param the deviceType to set up
774 * @param programMode the program mode that the setting shall apply to
775 * @param powerLevel the rate of charging / discharging 1%..100%
777 * @throws GrowattApiException if any error occurs
779 private void setPowerLevel(String deviceId, DeviceType deviceType, ProgramMode programMode, int powerLevel)
780 throws GrowattApiException {
782 if (powerLevel < 1 || powerLevel > 100) {
783 throw new GrowattApiException("Power level out of range (1%..100%)");
787 switch (programMode) {
789 typeParam = "charge_power";
793 typeParam = "discharge_power";
796 throw new GrowattApiException("Unexpected exception");
799 Fields fields = new Fields();
801 fields.put("op", Objects.requireNonNull(SUPPORTED_TYPE_POST_PARAM.get(deviceType)));
802 fields.put("serialNum", deviceId);
803 fields.put("type", typeParam);
804 fields.put("param1", String.format("%d", powerLevel));
806 postSetCommandForm(fields);
810 * Post a command to set up a program target (stop) SOC level.
812 * @param the deviceId to set up
813 * @param the deviceType to set up
814 * @param programMode the program mode that the setting shall apply to
815 * @param stopSOC the SOC at which to stop the program 11%..100%
817 * @throws GrowattApiException if any error occurs
819 private void setStopSOC(String deviceId, DeviceType deviceType, ProgramMode programMode, int stopSOC)
820 throws GrowattApiException {
822 if (stopSOC < 11 || stopSOC > 100) {
823 throw new GrowattApiException("Target SOC out of range (11%..100%)");
827 switch (programMode) {
829 typeParam = "charge_stop_soc";
832 typeParam = "on_grid_discharge_stop_soc";
835 typeParam = "discharge_stop_soc";
838 throw new GrowattApiException("Unexpected exception");
841 Fields fields = new Fields();
843 fields.put("op", Objects.requireNonNull(SUPPORTED_TYPE_POST_PARAM.get(deviceType)));
844 fields.put("serialNum", deviceId);
845 fields.put("type", typeParam);
846 fields.put("param1", String.format("%d", stopSOC));
848 postSetCommandForm(fields);
852 * Post a command to set up a time segment program.
853 * Note: uses separate dedicated time segments for Load First, Battery First, Grid First modes.
855 * @param the deviceId to set up
856 * @param the deviceType to set up
857 * @param programMode the program mode for the time segment
858 * @param startTime the start time of the program
859 * @param stopTime the stop time of the program
860 * @param enableProgram the program shall be enabled
862 * @throws GrowattApiException if any error occurs
864 private void setTimeSegment(String deviceId, DeviceType deviceType, ProgramMode programMode,
865 @Nullable String startTime, @Nullable String stopTime, @Nullable Boolean enableProgram)
866 throws GrowattApiException {
868 if (startTime == null) {
869 throw new GrowattApiException("Start time parameter is null");
871 if (stopTime == null) {
872 throw new GrowattApiException("Stop time parameter is null");
874 if (enableProgram == null) {
875 throw new GrowattApiException("Program enable parameter is null");
877 LocalTime localStartTime;
879 localStartTime = GrowattCloud.localTimeOf(startTime);
880 } catch (DateTimeException e) {
881 throw new GrowattApiException("Start time is invalid");
883 LocalTime localStopTime;
885 localStopTime = GrowattCloud.localTimeOf(stopTime);
886 } catch (DateTimeException e) {
887 throw new GrowattApiException("Stop time is invalid");
890 Fields fields = new Fields();
892 fields.put("op", Objects.requireNonNull(SUPPORTED_TYPE_POST_PARAM.get(deviceType)));
893 fields.put("serialNum", deviceId);
894 fields.put("type", String.format("time_segment%d", programMode.ordinal() + 1));
895 fields.put("param1", String.format("%d", programMode.ordinal()));
896 fields.put("param2", String.format("%02d", localStartTime.getHour()));
897 fields.put("param3", String.format("%02d", localStartTime.getMinute()));
898 fields.put("param4", String.format("%02d", localStopTime.getHour()));
899 fields.put("param5", String.format("%02d", localStopTime.getMinute()));
900 fields.put("param6", enableProgram ? "1" : "0");
902 postSetCommandForm(fields);