2 * Copyright (c) 2010-2023 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.gree.internal.handler;
15 import static org.openhab.binding.gree.internal.GreeBindingConstants.*;
17 import java.io.IOException;
18 import java.net.DatagramPacket;
19 import java.net.DatagramSocket;
20 import java.net.InetAddress;
21 import java.nio.charset.StandardCharsets;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.HashMap;
25 import java.util.List;
27 import java.util.Objects;
28 import java.util.Optional;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.openhab.binding.gree.internal.GreeCryptoUtil;
32 import org.openhab.binding.gree.internal.GreeException;
33 import org.openhab.binding.gree.internal.gson.GreeBindRequestPackDTO;
34 import org.openhab.binding.gree.internal.gson.GreeBindResponseDTO;
35 import org.openhab.binding.gree.internal.gson.GreeBindResponsePackDTO;
36 import org.openhab.binding.gree.internal.gson.GreeExecResponseDTO;
37 import org.openhab.binding.gree.internal.gson.GreeExecResponsePackDTO;
38 import org.openhab.binding.gree.internal.gson.GreeExecuteCommandPackDTO;
39 import org.openhab.binding.gree.internal.gson.GreeReqStatusPackDTO;
40 import org.openhab.binding.gree.internal.gson.GreeRequestDTO;
41 import org.openhab.binding.gree.internal.gson.GreeScanResponseDTO;
42 import org.openhab.binding.gree.internal.gson.GreeStatusResponseDTO;
43 import org.openhab.binding.gree.internal.gson.GreeStatusResponsePackDTO;
44 import org.openhab.core.library.types.QuantityType;
45 import org.openhab.core.library.unit.SIUnits;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
49 import com.google.gson.Gson;
50 import com.google.gson.JsonSyntaxException;
53 * The GreeDevice object repesents a Gree Airconditioner and provides
54 * device specific attributes as well a the functionality for the Air Conditioner
56 * @author John Cunha - Initial contribution
57 * @author Markus Michels - Refactoring, adapted to OH 2.5x
60 public class GreeAirDevice {
61 private final Logger logger = LoggerFactory.getLogger(GreeAirDevice.class);
62 private static final Gson GSON = new Gson();
63 private boolean isBound = false;
64 private final InetAddress ipAddress;
66 private String encKey = "";
67 private Optional<GreeScanResponseDTO> scanResponseGson = Optional.empty();
68 private Optional<GreeStatusResponseDTO> statusResponseGson = Optional.empty();
69 private Optional<GreeStatusResponsePackDTO> prevStatusResponsePackGson = Optional.empty();
71 public GreeAirDevice() {
72 ipAddress = InetAddress.getLoopbackAddress();
75 public GreeAirDevice(InetAddress ipAddress, int port, GreeScanResponseDTO scanResponse) {
76 this.ipAddress = ipAddress;
78 this.scanResponseGson = Optional.of(scanResponse);
81 public void getDeviceStatus(DatagramSocket clientSocket) throws GreeException {
83 throw new GreeException("Device not bound");
86 // Set the values in the HashMap
87 ArrayList<String> columns = new ArrayList<>();
88 columns.add(GREE_PROP_POWER);
89 columns.add(GREE_PROP_MODE);
90 columns.add(GREE_PROP_SETTEMP);
91 columns.add(GREE_PROP_WINDSPEED);
92 columns.add(GREE_PROP_AIR);
93 columns.add(GREE_PROP_DRY);
94 columns.add(GREE_PROP_HEALTH);
95 columns.add(GREE_PROP_SLEEP);
96 columns.add(GREE_PROP_LIGHT);
97 columns.add(GREE_PROP_SWINGLEFTRIGHT);
98 columns.add(GREE_PROP_SWINGUPDOWN);
99 columns.add(GREE_PROP_QUIET);
100 columns.add(GREE_PROP_TURBO);
101 columns.add(GREE_PROP_TEMPUNIT);
102 columns.add(GREE_PROP_HEAT);
103 columns.add(GREE_PROP_HEATCOOL);
104 columns.add(GREE_PROP_TEMPREC);
105 columns.add(GREE_PROP_PWR_SAVING);
106 columns.add(GREE_PROP_NOISESET);
107 columns.add(GREE_PROP_CURRENT_TEMP_SENSOR);
109 // Convert the parameter map values to arrays
110 String[] colArray = columns.toArray(new String[0]);
112 // Prep the Command Request pack
113 GreeReqStatusPackDTO reqStatusPackGson = new GreeReqStatusPackDTO();
114 reqStatusPackGson.t = GREE_CMDT_STATUS;
115 reqStatusPackGson.cols = colArray;
116 reqStatusPackGson.mac = getId();
117 String reqStatusPackStr = GSON.toJson(reqStatusPackGson);
119 // Encrypt and send the Status Request pack
120 String encryptedStatusReqPacket = GreeCryptoUtil.encryptPack(getKey(), reqStatusPackStr);
121 DatagramPacket sendPacket = createPackRequest(0,
122 new String(encryptedStatusReqPacket.getBytes(), StandardCharsets.UTF_8));
123 clientSocket.send(sendPacket);
125 // Keep a copy of the old response to be used to check if values have changed
126 // If first time running, there will not be a previous GreeStatusResponsePack4Gson
127 if (statusResponseGson.isPresent() && statusResponseGson.get().packJson != null) {
128 prevStatusResponsePackGson = Optional
129 .of(new GreeStatusResponsePackDTO(statusResponseGson.get().packJson));
132 // Read the response, create the JSON to hold the response values
133 GreeStatusResponseDTO resp = receiveResponse(clientSocket, GreeStatusResponseDTO.class);
134 resp.decryptedPack = GreeCryptoUtil.decryptPack(getKey(), resp.pack);
135 logger.debug("Response from device: {}", resp.decryptedPack);
136 resp.packJson = GSON.fromJson(resp.decryptedPack, GreeStatusResponsePackDTO.class);
139 statusResponseGson = Optional.of(resp);
141 } catch (IOException | JsonSyntaxException e) {
142 throw new GreeException("I/O exception while updating status", e);
143 } catch (RuntimeException e) {
144 logger.debug("Exception", e);
145 String json = statusResponseGson.map(r -> r.packJson.toString()).orElse("n/a");
146 throw new GreeException("Exception while updating status, JSON=" + json, e);
150 public void bindWithDevice(DatagramSocket clientSocket) throws GreeException {
152 // Prep the Binding Request pack
153 GreeBindRequestPackDTO bindReqPackGson = new GreeBindRequestPackDTO();
154 bindReqPackGson.mac = getId();
155 bindReqPackGson.t = GREE_CMDT_BIND;
156 bindReqPackGson.uid = 0;
157 String bindReqPackStr = GSON.toJson(bindReqPackGson);
159 // Encrypt and send the Binding Request pack
160 String encryptedBindReqPacket = GreeCryptoUtil.encryptPack(GreeCryptoUtil.getAESGeneralKeyByteArray(),
162 DatagramPacket sendPacket = createPackRequest(1, encryptedBindReqPacket);
163 clientSocket.send(sendPacket);
165 // Recieve a response, create the JSON to hold the response values
166 GreeBindResponseDTO resp = receiveResponse(clientSocket, GreeBindResponseDTO.class);
167 resp.decryptedPack = GreeCryptoUtil.decryptPack(GreeCryptoUtil.getAESGeneralKeyByteArray(), resp.pack);
168 resp.packJson = GSON.fromJson(resp.decryptedPack, GreeBindResponsePackDTO.class);
170 // Now set the key and flag to indicate the bind was successful
171 encKey = resp.packJson.key;
175 } catch (IOException | JsonSyntaxException e) {
176 throw new GreeException("Unable to bind to device", e);
180 public void setDevicePower(DatagramSocket clientSocket, int value) throws GreeException {
181 setCommandValue(clientSocket, GREE_PROP_POWER, value);
184 public void setDeviceMode(DatagramSocket clientSocket, int value) throws GreeException {
185 if ((value < 0 || value > 4)) {
186 throw new GreeException("Device mode out of range!");
188 setCommandValue(clientSocket, GREE_PROP_MODE, value);
192 * SwUpDn: controls the swing mode of the vertical air blades
195 * 1: swing in full range
196 * 2: fixed in the upmost position (1/5)
197 * 3: fixed in the middle-up position (2/5)
198 * 4: fixed in the middle position (3/5)
199 * 5: fixed in the middle-low position (4/5)
200 * 6: fixed in the lowest position (5/5)
201 * 7: swing in the downmost region (5/5)
202 * 8: swing in the middle-low region (4/5)
203 * 9: swing in the middle region (3/5)
204 * 10: swing in the middle-up region (2/5)
205 * 11: swing in the upmost region (1/5)
207 public void setDeviceSwingUpDown(DatagramSocket clientSocket, int value) throws GreeException {
208 if (value < 0 || value > 11) {
209 throw new GreeException("SwingUpDown value is out of range!");
211 setCommandValue(clientSocket, GREE_PROP_SWINGUPDOWN, value);
215 * SwingLfRig: controls the swing mode of the horizontal air blades (available on limited number of devices, e.g.
216 * some Cooper & Hunter units - thanks to mvmn)
220 * 2-6: fixed position from leftmost to rightmost
221 * Full swing, like for SwUpDn is not supported
223 public void setDeviceSwingLeftRight(DatagramSocket clientSocket, int value) throws GreeException {
224 if (value < 0 || value > 6) {
225 throw new GreeException("SwingLeftRight value is out of range!");
227 setCommandValue(clientSocket, GREE_PROP_SWINGLEFTRIGHT, value, 0, 6);
231 * Only allow this to happen if this device has been bound and values are valid
232 * Possible values are :
240 public void setDeviceWindspeed(DatagramSocket clientSocket, int value) throws GreeException {
241 if (value < 0 || value > 5) {
242 throw new GreeException("Value out of range!");
245 HashMap<String, Integer> parameters = new HashMap<>();
246 parameters.put(GREE_PROP_WINDSPEED, value);
247 parameters.put(GREE_PROP_QUIET, 0);
248 parameters.put(GREE_PROP_TURBO, 0);
249 parameters.put(GREE_PROP_NOISE, 0);
250 executeCommand(clientSocket, parameters);
254 * Tur: sets fan speed to the maximum. Fan speed cannot be changed while active and only available in Dry and Cool
260 public void setDeviceTurbo(DatagramSocket clientSocket, int value) throws GreeException {
261 setCommandValue(clientSocket, GREE_PROP_TURBO, value, 0, 1);
264 public void setQuietMode(DatagramSocket clientSocket, int value) throws GreeException {
265 setCommandValue(clientSocket, GREE_PROP_QUIET, value, 0, 2);
268 public void setDeviceLight(DatagramSocket clientSocket, int value) throws GreeException {
269 setCommandValue(clientSocket, GREE_PROP_LIGHT, value);
273 * @param temp set temperature in degrees Celsius or Fahrenheit
275 public void setDeviceTempSet(DatagramSocket clientSocket, QuantityType<?> temp) throws GreeException {
276 // If commanding Fahrenheit set halfStep to 1 or 0 to tell the A/C which F integer
277 // temperature to use as celsius alone is ambigious
278 double newVal = temp.doubleValue();
279 int celsiusOrFahrenheit = SIUnits.CELSIUS.equals(temp.getUnit()) ? TEMP_UNIT_CELSIUS : TEMP_UNIT_FAHRENHEIT; // 0=Celsius,
281 if (((celsiusOrFahrenheit == TEMP_UNIT_CELSIUS) && (newVal < TEMP_MIN_C || newVal > TEMP_MAX_C))
282 || ((celsiusOrFahrenheit == TEMP_UNIT_FAHRENHEIT) && (newVal < TEMP_MIN_F || newVal > TEMP_MAX_F))) {
283 throw new IllegalArgumentException("Temp Value out of Range");
286 // Default for Celsius
287 int outVal = (int) newVal;
288 int halfStep = TEMP_HALFSTEP_NO; // for whatever reason halfStep is not supported for Celsius
290 // If value argument is degrees F, convert Fahrenheit to Celsius,
291 // SetTem input to A/C always in Celsius despite passing in 1 to TemUn
292 // ******************TempRec TemSet Mapping for setting Fahrenheit****************************
294 // C = [20.0, 20.5, 21.1, 21.6, 22.2, 22.7, 23.3, 23.8, 24.4, 25.0, 25.5, 26.1, 26.6, 27.2, 27.7, 28.3,
297 // TemSet = [20..30] or [68..86]
298 // TemRec = value - (value) > 0 ? 1 : 1 -> when xx.5 is request xx will become TemSet and halfStep the indicator
299 // for "half on top of TemSet"
300 // ******************TempRec TemSet Mapping for setting Fahrenheit****************************
301 // subtract the float version - the int version to get the fractional difference
302 // if the difference is positive set halfStep to 1, negative to 0
303 if (celsiusOrFahrenheit == TEMP_UNIT_FAHRENHEIT) { // If Fahrenheit,
304 halfStep = newVal - outVal > 0 ? TEMP_HALFSTEP_YES : TEMP_HALFSTEP_NO;
306 logger.debug("Converted temp from {}{} to temp={}, halfStep={}, unit={})", newVal, temp.getUnit(), outVal,
307 halfStep, celsiusOrFahrenheit == TEMP_UNIT_CELSIUS ? "C" : "F");
309 // Set the values in the HashMap
310 HashMap<String, Integer> parameters = new HashMap<>();
311 parameters.put(GREE_PROP_TEMPUNIT, celsiusOrFahrenheit);
312 parameters.put(GREE_PROP_SETTEMP, outVal);
313 parameters.put(GREE_PROP_TEMPREC, halfStep);
314 executeCommand(clientSocket, parameters);
317 public void setDeviceAir(DatagramSocket clientSocket, int value) throws GreeException {
318 setCommandValue(clientSocket, GREE_PROP_AIR, value);
321 public void setDeviceDry(DatagramSocket clientSocket, int value) throws GreeException {
322 setCommandValue(clientSocket, GREE_PROP_DRY, value);
325 public void setDeviceHealth(DatagramSocket clientSocket, int value) throws GreeException {
326 setCommandValue(clientSocket, GREE_PROP_HEALTH, value);
329 public void setDevicePwrSaving(DatagramSocket clientSocket, int value) throws GreeException {
330 // Set the values in the HashMap
331 HashMap<String, Integer> parameters = new HashMap<>();
332 parameters.put(GREE_PROP_PWR_SAVING, value);
333 parameters.put(GREE_PROP_WINDSPEED, 0);
334 parameters.put(GREE_PROP_QUIET, 0);
335 parameters.put(GREE_PROP_TURBO, 0);
336 parameters.put(GREE_PROP_SLEEP, 0);
337 parameters.put(GREE_PROP_SLEEPMODE, 0);
338 executeCommand(clientSocket, parameters);
341 public int getIntStatusVal(String valueName) {
343 * Note : Values can be:
344 * "Pow": Power (0 or 1)
345 * "Mod": Mode: Auto: 0, Cool: 1, Dry: 2, Fan: 3, Heat: 4
346 * "SetTem": Requested Temperature
347 * "WdSpd": Fan Speed : Low:1, Medium Low:2, Medium :3, Medium High :4, High :5
348 * "Air": Air Mode Enabled
354 * "SwingLfRig": Swing Left Right
355 * "SwUpDn": Swing Up Down: // Ceiling:0, Upwards : 10, Downwards : 11, Full range : 1
356 * "Quiet": Quiet mode
359 * "TemUn": Temperature unit, 0 for Celsius, 1 for Fahrenheit
361 * "TemRec": (0 or 1), Send with SetTem, when TemUn==1, distinguishes between upper and lower integer Fahrenheit
363 * "SvSt": Power Saving
365 // Find the valueName in the Returned Status object
366 if (isStatusAvailable()) {
367 List<String> colList = Arrays.asList(statusResponseGson.get().packJson.cols);
368 List<Integer> valList = Arrays.asList(statusResponseGson.get().packJson.dat);
369 int valueArrayposition = colList.indexOf(valueName);
370 if (valueArrayposition != -1) {
371 // get the Corresponding value
372 return valList.get(valueArrayposition);
379 public boolean isStatusAvailable() {
380 return statusResponseGson.isPresent() && (statusResponseGson.get().packJson.cols != null)
381 && (statusResponseGson.get().packJson.dat != null);
384 public boolean hasStatusValChanged(String valueName) throws GreeException {
385 if (prevStatusResponsePackGson.isEmpty()) {
386 return true; // update value if there is no previous one
388 // Find the valueName in the Current Status object
389 List<String> currcolList = Arrays.asList(statusResponseGson.get().packJson.cols);
390 List<Integer> currvalList = Arrays.asList(statusResponseGson.get().packJson.dat);
391 int currvalueArrayposition = currcolList.indexOf(valueName);
392 if (currvalueArrayposition == -1) {
393 throw new GreeException("Unable to decode device status");
396 // Find the valueName in the Previous Status object
397 List<String> prevcolList = Arrays.asList(prevStatusResponsePackGson.get().cols);
398 List<Integer> prevvalList = Arrays.asList(prevStatusResponsePackGson.get().dat);
399 int prevvalueArrayposition = prevcolList.indexOf(valueName);
400 if (prevvalueArrayposition == -1) {
401 throw new GreeException("Unable to get status value");
404 // Finally Compare the values
405 return !Objects.equals(currvalList.get(currvalueArrayposition), prevvalList.get(prevvalueArrayposition));
408 protected void executeCommand(DatagramSocket clientSocket, Map<String, Integer> parameters) throws GreeException {
409 // Only allow this to happen if this device has been bound
411 throw new GreeException("Device is not bound!");
415 // Convert the parameter map values to arrays
416 String[] keyArray = parameters.keySet().toArray(new String[0]);
417 Integer[] valueArray = parameters.values().toArray(new Integer[0]);
419 // Prep the Command Request pack
420 GreeExecuteCommandPackDTO execCmdPackGson = new GreeExecuteCommandPackDTO();
421 execCmdPackGson.opt = keyArray;
422 execCmdPackGson.p = valueArray;
423 execCmdPackGson.t = GREE_CMDT_CMD;
424 String execCmdPackStr = GSON.toJson(execCmdPackGson);
426 // Now encrypt and send the Command Request pack
427 String encryptedCommandReqPacket = GreeCryptoUtil.encryptPack(getKey(), execCmdPackStr);
428 DatagramPacket sendPacket = createPackRequest(0, encryptedCommandReqPacket);
429 clientSocket.send(sendPacket);
431 // Receive and decode result
432 GreeExecResponseDTO execResponseGson = receiveResponse(clientSocket, GreeExecResponseDTO.class);
433 execResponseGson.decryptedPack = GreeCryptoUtil.decryptPack(getKey(), execResponseGson.pack);
435 // Create the JSON to hold the response values
436 execResponseGson.packJson = GSON.fromJson(execResponseGson.decryptedPack, GreeExecResponsePackDTO.class);
437 } catch (IOException | JsonSyntaxException e) {
438 throw new GreeException("Exception on command execution", e);
442 private void setCommandValue(DatagramSocket clientSocket, String command, int value) throws GreeException {
443 executeCommand(clientSocket, Map.of(command, value));
446 private void setCommandValue(DatagramSocket clientSocket, String command, int value, int min, int max)
447 throws GreeException {
448 if ((value < min) || (value > max)) {
449 throw new GreeException("Command value out of range!");
451 executeCommand(clientSocket, Map.of(command, value));
454 private DatagramPacket createPackRequest(int i, String pack) {
455 GreeRequestDTO request = new GreeRequestDTO();
456 request.cid = GREE_CID;
458 request.t = GREE_CMDT_PACK;
460 request.tcid = getId();
462 byte[] sendData = GSON.toJson(request).getBytes(StandardCharsets.UTF_8);
463 return new DatagramPacket(sendData, sendData.length, ipAddress, port);
466 private <T> T receiveResponse(DatagramSocket clientSocket, Class<T> classOfT)
467 throws IOException, JsonSyntaxException {
468 byte[] receiveData = new byte[1024];
469 DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
470 clientSocket.receive(receivePacket);
471 String json = new String(receivePacket.getData(), StandardCharsets.UTF_8).replace("\\u0000", "").trim();
472 return GSON.fromJson(json, classOfT);
475 private void updateTempFtoC() {
476 // Status message back from A/C always reports degrees C
477 // If using Fahrenheit, us SetTem, TemUn and TemRec to reconstruct the Fahrenheit temperature
478 // Get Celsius or Fahrenheit from status message
479 int celsiusOrFahrenheit = getIntStatusVal(GREE_PROP_TEMPUNIT);
480 int newVal = getIntStatusVal(GREE_PROP_SETTEMP);
481 int halfStep = getIntStatusVal(GREE_PROP_TEMPREC);
483 if ((celsiusOrFahrenheit == -1) || (newVal == -1) || (halfStep == -1)) {
484 throw new IllegalArgumentException("SetTem,TemUn or TemRec is invalid, not performing conversion");
485 } else if (celsiusOrFahrenheit == 1) { // convert SetTem to Fahrenheit
486 // Find the valueName in the Returned Status object
487 String[] columns = statusResponseGson.get().packJson.cols;
488 Integer[] values = statusResponseGson.get().packJson.dat;
489 List<String> colList = Arrays.asList(columns);
490 int valueArrayposition = colList.indexOf(GREE_PROP_SETTEMP);
491 if (valueArrayposition != -1) {
492 // convert Celsius to Fahrenheit,
493 // SetTem status returns degrees C regardless of TempUn setting
495 // Perform the float Celsius to Fahrenheit conversion add or subtract 0.5 based on the value of TemRec
496 // (0 = -0.5, 1 = +0.5). Pass into a rounding function, this yeild the correct Fahrenheit Temperature to
498 newVal = (int) (Math.round(((newVal * 9.0 / 5.0) + 32.0) + halfStep - 0.5));
500 // Update the status array with F temp, assume this is updating the array in situ
501 values[valueArrayposition] = newVal;
506 public InetAddress getAddress() {
510 public boolean getIsBound() {
514 public byte[] getKey() {
515 return encKey.getBytes(StandardCharsets.UTF_8);
518 public String getId() {
519 return scanResponseGson.isPresent() ? scanResponseGson.get().packJson.mac : "";
522 public String getName() {
523 return scanResponseGson.isPresent() ? scanResponseGson.get().packJson.name : "";
526 public String getVendor() {
527 return scanResponseGson.isPresent()
528 ? scanResponseGson.get().packJson.brand + " " + scanResponseGson.get().packJson.vender
532 public String getModel() {
533 return scanResponseGson.isPresent()
534 ? scanResponseGson.get().packJson.series + " " + scanResponseGson.get().packJson.model
538 public void setScanResponseGson(GreeScanResponseDTO gson) {
539 scanResponseGson = Optional.of(gson);