diff --git a/CODEOWNERS b/CODEOWNERS
index 9b9bf59650c17..bfee361649f3a 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -67,6 +67,7 @@
/bundles/org.openhab.binding.gardena/ @gerrieg
/bundles/org.openhab.binding.globalcache/ @mhilbush
/bundles/org.openhab.binding.gpstracker/ @gbicskei
+/bundles/org.openhab.binding.gree/ @markus7017
/bundles/org.openhab.binding.groheondus/ @FlorianSW
/bundles/org.openhab.binding.harmonyhub/ @digitaldan
/bundles/org.openhab.binding.hdanywhere/ @kgoderis
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 15e92b1609b7c..0e828ecae50b5 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -331,6 +331,11 @@
org.openhab.binding.gpstracker${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.gree
+ ${project.version}
+ org.openhab.addons.bundlesorg.openhab.binding.groheondus
diff --git a/bundles/org.openhab.binding.gree/.classpath b/bundles/org.openhab.binding.gree/.classpath
new file mode 100644
index 0000000000000..a5d95095ccaaf
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/.classpath
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.openhab.binding.gree/.project b/bundles/org.openhab.binding.gree/.project
new file mode 100644
index 0000000000000..5e843714895b6
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/.project
@@ -0,0 +1,23 @@
+
+
+ org.openhab.binding.gree
+
+
+
+
+
+ org.eclipse.jdt.core.javabuilder
+
+
+
+
+ org.eclipse.m2e.core.maven2Builder
+
+
+
+
+
+ org.eclipse.jdt.core.javanature
+ org.eclipse.m2e.core.maven2Nature
+
+
diff --git a/bundles/org.openhab.binding.gree/NOTICE b/bundles/org.openhab.binding.gree/NOTICE
new file mode 100644
index 0000000000000..38d625e349232
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/NOTICE
@@ -0,0 +1,13 @@
+This content is produced and maintained by the openHAB project.
+
+* Project home: https://www.openhab.org
+
+== Declared Project Licenses
+
+This program and the accompanying materials are made available under the terms
+of the Eclipse Public License 2.0 which is available at
+https://www.eclipse.org/legal/epl-2.0/.
+
+== Source Code
+
+https://github.com/openhab/openhab-addons
diff --git a/bundles/org.openhab.binding.gree/README.md b/bundles/org.openhab.binding.gree/README.md
new file mode 100644
index 0000000000000..7f9d7af8a5ef7
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/README.md
@@ -0,0 +1,141 @@
+# GREE Binding
+
+This binding integrates GREE Air Conditioners.
+
+Note: The GREE Air Conditioner must already be setup on the WiFi network and must have a fixed IP Address.
+
+## Supported Things
+
+This binding supports one Thing type `airconditioner`.
+
+## Discovery
+
+Once the GREE is on the network (WiFi active) it could be discovery automatically.
+An IP broadcast message is sent and every responding unit gets added to the Inbox.
+
+## Binding Configuration
+
+No binding configuration is required.
+
+## Thing Configuration
+
+| Channel Name | Type | Description |
+|------------------|------------|-----------------------------------------------------------------------------------------------|
+| ipAddress | IP Address | IP address of the unit. |
+| broadcastAddress | IP Address | Broadcast address being used for discovery, usually derived from the IP interface address. |
+| refresh | Integer | Refresh interval in seconds for polling the device status. |
+
+The Air Conditioner's IP address is mandatory, all other parameters are optional.
+If the broadcast is not set (default) it will be derived from openHAB's network setting (PaperUI:Configuration:System:Network Settings).
+Only change this if you have a good reason to.
+
+## Channels
+
+The following channels are supported for fans:
+
+| Channel Name | Item Type | Description |
+|---------------|-----------|---------------------------------------------------------------------------------------------------|
+| power | Switch | Power on/off the Air Conditioner |
+| mode | String | Sets the operating mode of the Air Conditioner |
+| | | Mode can be one of auto/cool/eco/dry/fan/heat or on/off |
+| | | Check the Air Conditioner's operating manual for supported modes. |
+| temperature | Number:Temperature | Sets the desired room temperature |
+|Â air | Switch | Set on/off the Air Conditioner's Air function if applicable to the Air Conditioner model |
+| dry | Switch | Set on/off the Air Conditioner's Dry function if applicable to the Air Conditioner model |
+|Â health | Switch | Set on/off the Air Conditioner's Health function if applicable to the Air Conditioner model |
+| turbo | Switch | Set on/off the Air Conditioner's Turbo Mode. |
+| quiet | String | Set Quiet Mode: off/auto/quiet |
+| swingUpDown | Number | Sets the vertical (up..down) swing action on the Air Conditioner, |
+| | | OFF: 0, Full Swing: 1, Up: 2, MidUp: 3, Mid: 4, Mid Down: 5, Down : 6 |
+| swingLeftRight| Number | Sets the horizontal (left..right) swing action on the Air Conditioner |
+| | |Â OFF: 0, Full Swing: 1, Left: 2, Mid Left: 3, Mid: 4, Mid Right: 5, Right : 6 |
+|Â windspeed | Number | Sets the fan speed on the Air conditioner Auto:0, Low:1, MidLow:2, Mid:3, MidHigh:4, High:5 |
+| | |Â The number of speeds depends on the Air Conditioner model. |
+|Â powersave | Switch | Set on/off the Air Conditioner's Power Saving function if applicable to the Air Conditioner model |
+| light | Switch | Enable/disable the front display on the Air Conditioner if applicable to the Air Conditioner model|
+| | | Full Swing: 1, Up: 2, MidUp: 3, Mid: 4, Mid Down: 5, Down : 6 |
+
+
+When changing mode, the air conditioner will be turned on unless "off" is selected.
+
+## Full Example
+
+**Things**
+
+```
+Thing gree:airconditioner:a1234561 [ ipAddress="192.168.1.111", refresh=2 ]
+```
+
+**Items**
+
+```
+Switch AirconPower { channel="gree:airconditioner:a1234561:power" }
+Number AirconMode { channel="gree:airconditioner:a1234561:mode" }
+Switch AirconTurbo { channel="gree:airconditioner:a1234561:turbo" }
+Switch AirconLight { channel="gree:airconditioner:a1234561:light" }
+Number AirconTemp "Temperature [%.1f °C]" {channel="gree:airconditioner:a1234561:temperature" }
+Number AirconSwingVertical { channel="gree:airconditioner:a1234561:swingUpDown" }
+Number AirconSwingHorizontal { channel="gree:airconditioner:a1234561:swingLeftRight" }
+Number AirconFanSpeed { channel="gree:airconditioner:a1234561:windspeed" }
+Switch AirconAir { channel="gree:airconditioner:a1234561:air" }
+Switch AirconDry { channel="gree:airconditioner:a1234561:dry" }
+Switch AirconHealth { channel="gree:airconditioner:a1234561:health" }
+Switch AirconPowerSaving { channel="gree:airconditioner:a1234561:powersave" }
+```
+
+**Sitemap**
+
+This is an example of how to set up your sitemap.
+
+```
+Frame label="Controls"
+{
+ Switch item=AirconMode label="Mode" mappings=["auto"="Auto", "cool"="Cool", "eco"="Eco", "dry"="Dry", "fan"="Fan", "turbo"="Turbo", "heat"="Heat", "on"="ON", "off"="OFF"]
+ Setpoint item=AirconTemp label="Set temperature" icon=temperature minValue=16 maxValue=30 step=1
+}
+Frame label="Fan Speed"
+{
+ Switch item=AirconFanSpeed label="Fan Speed" mappings=[0="Auto", 1="Low", 2="Medium Low", 3="Medium", 4="Medium High", 5="High"] icon=fan
+}
+Frame label="Fan-Swing Direction"
+{
+ Switch item=AirconSwingVertical label="Direction V" mappings=[0="Off", 1="Full", 2="Up", 3="Mid-up", 4="Mid", 5="Mid-low", 6="Down"] icon=flow
+ Switch item=AirconSwingHorizontal label="Direction H" mappings=[0="Off", 1="Full", 2="Left", 3="Mid-left", 4="Mid", 5="Mid-right", 6="Right"] icon=flow
+}
+Frame label="Options"
+{
+ Switch item=AirconTurbo label="Turbo" icon=fan
+ Switch item=AirconLight label="Light" icon=light
+ Switch item=AirconAir label="Air" icon=flow
+ Switch item=AirconDry label="Dry" icon=rain
+ Switch item=AirconHealth label="Health" icon=smiley
+ Switch item=AirconPowerSaving label="Power Saving" icon=poweroutlet
+}
+```
+
+**Example**
+
+This example shows how to make a GREE Air Conditioner controllable by Google HA (A/C mode + temperature)
+
+**Items**
+
+```
+Group Gree_Modechannel "Gree" { ga="Thermostat" } // allows mapping for Google Home Assistent
+Switch GreeAirConditioner_Power "Aircon" {channel="gree:airconditioner:a1234561:power", ga="Switch"}
+Number GreeAirConditioner_Mode "Aircon Mode" {channel="gree:airconditioner:a1234561:mode", ga="thermostatMode"}
+Number GreeAirConditioner_Temp "Aircon Temperature" {channel="gree:airconditioner:a1234561:temperature}
+Switch GreeAirConditioner_Lightl "Light" {channel="gree:airconditioner:a1234561:light"}
+```
+
+**Rules**
+
+```
+rule "Mode changed"
+when
+ Item GreeAirConditioner_Mode changed
+then
+ if(GreeAirConditioner_Mode.state == "cool" ) {
+ logInfo("A/C", "Cooling has be turned on")
+ }
+end
+```
diff --git a/bundles/org.openhab.binding.gree/pom.xml b/bundles/org.openhab.binding.gree/pom.xml
new file mode 100644
index 0000000000000..8e0e96b29b4c9
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 2.5.7-SNAPSHOT
+
+
+ org.openhab.binding.gree
+
+ openHAB Add-ons :: Bundles :: Gree Binding
+
+
diff --git a/bundles/org.openhab.binding.gree/src/main/feature/feature.xml b/bundles/org.openhab.binding.gree/src/main/feature/feature.xml
new file mode 100644
index 0000000000000..3257cf303ac34
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.gree/${project.version}
+
+
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeBindingConstants.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeBindingConstants.java
new file mode 100644
index 0000000000000..dab4521858562
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeBindingConstants.java
@@ -0,0 +1,162 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal;
+
+import java.util.Collections;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+
+/**
+ * The {@link GreeBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author John Cunha - Initial contribution
+ * @author Markus Michels - Refactoring, adapted to OH 2.5x
+ */
+@NonNullByDefault
+public class GreeBindingConstants {
+
+ public static final String BINDING_ID = "gree";
+
+ public static final ThingTypeUID THING_TYPE_GREEAIRCON = new ThingTypeUID(BINDING_ID, "airconditioner");
+ public static final Set SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_GREEAIRCON);
+
+ // List of all Thing Type UIDs
+ public static final ThingTypeUID GREE_THING_TYPE = new ThingTypeUID(BINDING_ID, "airconditioner");
+
+ // Thing configuration items
+ public static final String PROPERTY_IP = "ipAddress";
+ public static final String PROPERTY_BROADCAST = "broadcastAddress";
+
+ // List of all Channel ids
+ public static final String POWER_CHANNEL = "power";
+ public static final String MODE_CHANNEL = "mode";
+ public static final String TURBO_CHANNEL = "turbo";
+ public static final String LIGHT_CHANNEL = "light";
+ public static final String TEMP_CHANNEL = "temperature";
+ public static final String SWINGUD_CHANNEL = "swingUpDown";
+ public static final String SWINGLR_CHANNEL = "swingLeftRight";
+ public static final String WINDSPEED_CHANNEL = "windspeed";
+ public static final String QUIET_CHANNEL = "quiet";
+ public static final String AIR_CHANNEL = "air";
+ public static final String DRY_CHANNEL = "dry";
+ public static final String HEALTH_CHANNEL = "health";
+ public static final String PWRSAV_CHANNEL = "powersave";
+
+ // Mode channel
+ public static final String MODE_AUTO = "auto";
+ public static final String MODE_COOL = "cool";
+ public static final String MODE_DRY = "dry";
+ public static final String MODE_FAN = "fan";
+ public static final String MODE_FAN2 = "fan-only";
+ public static final String MODE_HEAT = "heat";
+ public static final String MODE_ECO = "eco";
+ public static final String MODE_ON = "on";
+ public static final String MODE_OFF = "off";
+ public static final int GREE_MODE_AUTO = 0;
+ public static final int GREE_MODE_COOL = 1;
+ public static final int GREE_MODE_DRY = 2;
+ public static final int GREE_MODE_FAN = 3;
+ public static final int GREE_MODE_HEAT = 4;
+
+ // Quiet channel
+ public static final String QUIET_OFF = "off";
+ public static final String QUIET_AUTO = "auto";
+ public static final String QUIET_QUIET = "quiet";
+ public static final int GREE_QUIET_OFF = 0;
+ public static final int GREE_QUIET_AUTO = 1;
+ public static final int GREE_QUIET_QUIET = 2;
+
+ // UDPPort used to communicate using UDP with GREE Airconditioners. .
+ public static final String VENDOR_GREE = "gree";
+ public static final int GREE_PORT = 7000;
+
+ public static final String GREE_CID = "app";
+ public static final String GREE_CMDT_BIND = "bind";
+ public static final String GREE_CMDT_SCAN = "scan";
+ public static final String GREE_CMDT_STATUS = "status";
+ public static final String GREE_CMDT_CMD = "cmd";
+ public static final String GREE_CMDT_PACK = "pack";
+
+ public static final String GREE_CMD_OPT_NAME = "name"; // unit name
+ public static final String GREE_CMD_OPT_HOST = "host"; // remote host (cloud)
+
+ /*
+ * Note : Values can be:
+ * "Pow": Power (0 or 1)
+ * "Mod": Mode: Auto: 0, Cool: 1, Dry: 2, Fan: 3, Heat: 4
+ * "SetTem": Requested Temperature
+ * "WdSpd": Fan Speed : Low:1, Medium Low:2, Medium :3, Medium High :4, High :5
+ * "Air": Air Mode Enabled
+ * "Blo": Dry
+ * "Health": Health
+ * "SwhSlp": Sleep
+ * "SlpMod": ???
+ * "Lig": Light On
+ * "SwingLfRig": Swing Left Right
+ * "SwUpDn": Swing Up Down: // Ceiling:0, Upwards : 10, Downwards : 11, Full range : 1
+ * "Quiet": Quiet mode
+ * "Tur": Turbo
+ * "StHt": 0,
+ * "TemUn": Temperature unit, 0 for Celsius, 1 for Fahrenheit
+ * "HeatCoolType"
+ * "TemRec": (0 or 1), Send with SetTem, when TemUn==1, distinguishes between upper and lower integer Fahrenheit
+ * temp
+ * "SvSt": Power Saving
+ */
+ public static final String GREE_PROP_POWER = "Pow";
+ public static final String GREE_PROP_MODE = "Mod";
+ public static final String GREE_PROP_SWINGUPDOWN = "SwUpDn";
+ public static final String GREE_PROP_SWINGLEFTRIGHT = "SwingLfRig";
+ public static final String GREE_PROP_WINDSPEED = "WdSpd";
+ public static final String GREE_PROP_AIR = "Air";
+ public static final String GREE_PROP_DRY = "Blo";
+ public static final String GREE_PROP_TURBO = "Tur";
+ public static final String GREE_PROP_QUIET = "Quiet";
+ public static final String GREE_PROP_NOISE = "NoiseSet";
+ public static final String GREE_PROP_LIGHT = "Lig";
+ public static final String GREE_PROP_HEALTH = "Health";
+ public static final String GREE_PROP_SLEEP = "SwhSlp";
+ public static final String GREE_PROP_SLEEPMODE = "SlpMod";
+ public static final String GREE_PROP_PWR_SAVING = "SvSt";
+ public static final String GREE_PROP_SETTEMP = "SetTem";
+ public static final String GREE_PROP_TEMPUNIT = "TemUn";
+ public static final String GREE_PROP_TEMPREC = "TemRec";
+ public static final String GREE_PROP_HEAT = "StHt";
+ public static final String GREE_PROP_HEATCOOL = "HeatCoolType";
+ public static final String GREE_PROP_NOISESET = "NoiseSet";
+
+ // Temperatur types and min/max ranges
+ public static final int TEMP_UNIT_CELSIUS = 0;
+ public static final int TEMP_UNIT_FAHRENHEIT = 1;
+ public static final int TEMP_MIN_C = 16;
+ public static final int TEMP_MAX_C = 30;
+ public static final int TEMP_MIN_F = 61;
+ public static final int TEMP_MAX_F = 86;
+ public static final int TEMP_HALFSTEP_NO = 0;
+ public static final int TEMP_HALFSTEP_YES = 1;
+
+ /*
+ * The timeout for the Datagram socket used to communicate with Gree Airconditioners.
+ * This is particularly important when scanning for devices because this will effectively
+ * be the amount of time spent scanning.
+ */
+ public static final int DATAGRAM_SOCKET_TIMEOUT = 5000; // regular read timeout
+ public static final int DISCOVERY_TIMEOUT_MS = 7000; // do not change!!
+ public static final int MAX_SCAN_CYCLES = 3;
+ public static final int REFRESH_INTERVAL_SEC = 5;
+
+ public static final int DIGITS_TEMP = 1;
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeConfiguration.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeConfiguration.java
new file mode 100644
index 0000000000000..5f5c0baa09840
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeConfiguration.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link GreeConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author John Cunha - Initial contribution
+ * @author Markus Michels - Refactoring, adapted to OH 2.5x
+ */
+@NonNullByDefault
+public class GreeConfiguration {
+ public String ipAddress = "";
+ public String broadcastAddress = "";
+ public int refresh = 60;
+
+ @Override
+ public String toString() {
+ return "Config: ipAddress=" + ipAddress + ", broadcastAddress=" + broadcastAddress + ", refresh=" + refresh;
+ }
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeCryptoUtil.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeCryptoUtil.java
new file mode 100644
index 0000000000000..2f8f9f90b0d3c
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeCryptoUtil.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal;
+
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The CryptoUtil class provides functionality for encrypting and decrypting
+ * messages sent to and from the Air Conditioner
+ *
+ * @author John Cunha - Initial contribution
+ * @author Markus Michels - Refactoring, adapted to OH 2.5x
+ */
+@NonNullByDefault
+public class GreeCryptoUtil {
+ private static final String AES_KEY = "a3K8Bx%2r8Y7#xDh";
+
+ public static byte[] getAESGeneralKeyByteArray() {
+ return AES_KEY.getBytes(StandardCharsets.UTF_8);
+ }
+
+ public static String decryptPack(byte[] keyarray, String message) throws GreeException {
+ try {
+ Key key = new SecretKeySpec(keyarray, "AES");
+ Base64.Decoder decoder = Base64.getDecoder();
+ byte[] imageByte = decoder.decode(message);
+
+ Cipher aesCipher = Cipher.getInstance("AES");
+ aesCipher.init(Cipher.DECRYPT_MODE, key);
+ byte[] bytePlainText = aesCipher.doFinal(imageByte);
+
+ return new String(bytePlainText, StandardCharsets.UTF_8);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException | InvalidKeyException
+ | IllegalBlockSizeException ex) {
+ throw new GreeException("Decryption of recieved data failed", ex);
+ }
+ }
+
+ public static String encryptPack(byte[] keyarray, String message) throws GreeException {
+ try {
+ Key key = new SecretKeySpec(keyarray, "AES");
+ Cipher aesCipher = Cipher.getInstance("AES");
+ aesCipher.init(Cipher.ENCRYPT_MODE, key);
+ byte[] bytePlainText = aesCipher.doFinal(message.getBytes());
+
+ Base64.Encoder newencoder = Base64.getEncoder();
+ return new String(newencoder.encode(bytePlainText), StandardCharsets.UTF_8);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException | InvalidKeyException
+ | IllegalBlockSizeException ex) {
+ throw new GreeException("Unable to encrypt outbound data", ex);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeException.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeException.java
new file mode 100644
index 0000000000000..73e350a0805b9
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeException.java
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal;
+
+import java.net.MalformedURLException;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.text.MessageFormat;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * {@link GreeException} implements a binding specific exception class. This allows to unity exception handling on the
+ * higher levels, but still carrying the exception, which caused the problem.
+ *
+ * @author Markus Michels - Initial Contribution
+ */
+@NonNullByDefault
+public class GreeException extends Exception {
+ private static final long serialVersionUID = -2337258558995287405L;
+ private static String EX_NONE = "none";
+
+ public GreeException(Exception exception) {
+ super(exception);
+ }
+
+ public GreeException(String message) {
+ super(message);
+ }
+
+ public GreeException(String message, Exception exception) {
+ super(message, exception);
+ }
+
+ @Override
+ public String getMessage() {
+ return isEmpty() ? "" : nonNullString(super.getMessage());
+ }
+
+ @Override
+ public String toString() {
+ String message = nonNullString(super.getMessage());
+ String cause = getCauseClass().toString();
+ if (!isEmpty()) {
+ if (isUnknownHost()) {
+ String[] string = message.split(": "); // java.net.UnknownHostException: api.rach.io
+ message = MessageFormat.format("Unable to connect to {0} (Unknown host / Network down / Low signal)",
+ string[1]);
+ } else if (isMalformedURL()) {
+ message = "Invalid URL";
+ } else if (isTimeout()) {
+ message = "Device unreachable or API Timeout";
+ } else {
+ message = MessageFormat.format("{0} ({1})", message, cause);
+ }
+ } else {
+ message = getMessage();
+ }
+ return message;
+ }
+
+ public boolean isApiException() {
+ return getCauseClass() == GreeException.class;
+ }
+
+ public boolean isTimeout() {
+ Class> extype = !isEmpty() ? getCauseClass() : null;
+ return (extype != null) && ((extype == SocketTimeoutException.class) || (extype == TimeoutException.class)
+ || (extype == ExecutionException.class) || (extype == InterruptedException.class)
+ || getMessage().toLowerCase().contains("timeout"));
+ }
+
+ public boolean isUnknownHost() {
+ return getCauseClass() == MalformedURLException.class;
+ }
+
+ public boolean isMalformedURL() {
+ return getCauseClass() == UnknownHostException.class;
+ }
+
+ public boolean IsJSONException() {
+ return getCauseClass() == JsonSyntaxException.class;
+ }
+
+ private boolean isEmpty() {
+ return nonNullString(super.getMessage()).equals(EX_NONE);
+ }
+
+ private static String nonNullString(@Nullable String s) {
+ return s != null ? s : "";
+ }
+
+ private Class> getCauseClass() {
+ Throwable cause = getCause();
+ if (getCause() != null) {
+ return cause.getClass();
+ }
+ return GreeException.class;
+ }
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeHandlerFactory.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeHandlerFactory.java
new file mode 100644
index 0000000000000..981ef8dc92227
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeHandlerFactory.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal;
+
+import static org.openhab.binding.gree.internal.GreeBindingConstants.*;
+
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.net.NetworkAddressService;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingTypeUID;
+import org.eclipse.smarthome.core.thing.binding.BaseThingHandlerFactory;
+import org.eclipse.smarthome.core.thing.binding.ThingHandler;
+import org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory;
+import org.openhab.binding.gree.internal.discovery.GreeDeviceFinder;
+import org.openhab.binding.gree.internal.handler.GreeHandler;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * The {@link GreeHandlerFactory} is responsible for creating things and thing handlers.
+ *
+ * @author John Cunha - Initial contribution
+ * @author Markus Michels - Refactoring, adapted to OH 2.5x
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding." + BINDING_ID, service = ThingHandlerFactory.class)
+public class GreeHandlerFactory extends BaseThingHandlerFactory {
+ private final GreeTranslationProvider messages;
+ private final GreeDeviceFinder deviceFinder;
+
+ @Activate
+ public GreeHandlerFactory(@Reference NetworkAddressService networkAddressService,
+ @Reference GreeDeviceFinder deviceFinder, @Reference GreeTranslationProvider translationProvider,
+ ComponentContext componentContext, Map configProperties) {
+ super.activate(componentContext);
+ this.messages = translationProvider;
+ this.deviceFinder = deviceFinder;
+ }
+
+ @Override
+ public boolean supportsThingType(ThingTypeUID thingTypeUID) {
+ return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
+ }
+
+ @Override
+ protected @Nullable ThingHandler createHandler(Thing thing) {
+ if (THING_TYPE_GREEAIRCON.equals(thing.getThingTypeUID())) {
+ return new GreeHandler(thing, messages, deviceFinder);
+ }
+ return null;
+ }
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeTranslationProvider.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeTranslationProvider.java
new file mode 100644
index 0000000000000..265d1e9b8b836
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/GreeTranslationProvider.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal;
+
+import java.util.Locale;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.i18n.LocaleProvider;
+import org.eclipse.smarthome.core.i18n.TranslationProvider;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * {@link GreeTranslationProvider} provides i18n message lookup
+ *
+ * @author Markus Michels - Initial contribution
+ */
+@NonNullByDefault
+@Component(service = GreeTranslationProvider.class, immediate = true, configurationPid = "localization.gree")
+public class GreeTranslationProvider {
+
+ private final Bundle bundle;
+ private final TranslationProvider i18nProvider;
+ private final LocaleProvider localeProvider;
+
+ @Activate
+ public GreeTranslationProvider(@Reference TranslationProvider i18nProvider,
+ @Reference LocaleProvider localeProvider) {
+ this.bundle = FrameworkUtil.getBundle(this.getClass());
+ this.i18nProvider = i18nProvider;
+ this.localeProvider = localeProvider;
+ }
+
+ public GreeTranslationProvider(final GreeTranslationProvider other) {
+ this.bundle = other.bundle;
+ this.i18nProvider = other.i18nProvider;
+ this.localeProvider = other.localeProvider;
+ }
+
+ public String get(String key, @Nullable Object... arguments) {
+ return getText(key.contains("@text/") ? key : "message." + key, arguments);
+ }
+
+ public String getText(String key, @Nullable Object... arguments) {
+ try {
+ Locale locale = localeProvider.getLocale();
+ String message = i18nProvider.getText(bundle, key, getDefaultText(key), locale, arguments);
+ if (message != null) {
+ return message;
+ }
+ } catch (IllegalArgumentException e) {
+ }
+ return "Unable to load message for key " + key;
+ }
+
+ public @Nullable String getDefaultText(String key) {
+ return i18nProvider.getText(bundle, key, key, Locale.ENGLISH);
+ }
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDeviceFinder.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDeviceFinder.java
new file mode 100644
index 0000000000000..3d4100dcb036d
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDeviceFinder.java
@@ -0,0 +1,166 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal.discovery;
+
+import static org.openhab.binding.gree.internal.GreeBindingConstants.*;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.gree.internal.GreeCryptoUtil;
+import org.openhab.binding.gree.internal.GreeException;
+import org.openhab.binding.gree.internal.gson.GreeScanReponsePackDTO;
+import org.openhab.binding.gree.internal.gson.GreeScanRequestDTO;
+import org.openhab.binding.gree.internal.gson.GreeScanResponseDTO;
+import org.openhab.binding.gree.internal.handler.GreeAirDevice;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The GreeDeviceFinder provides functionality for searching for GREE Airconditioners on the network and keeping a list
+ * of found devices.
+ *
+ * @author John Cunha - Initial contribution
+ * @author Markus Michels - Refactoring, adapted to OH 2.5x
+ */
+@NonNullByDefault
+@Component(service = GreeDeviceFinder.class, immediate = true, configurationPid = "devicefinder.gree")
+public class GreeDeviceFinder {
+ private final Logger logger = LoggerFactory.getLogger(GreeDeviceFinder.class);
+ private static final Gson gson = (new GsonBuilder()).create();
+
+ protected final InetAddress ipAddress = InetAddress.getLoopbackAddress();
+ protected Map deviceTable = new HashMap<>();
+
+ @Activate
+ public GreeDeviceFinder() {
+ }
+
+ public void scan(DatagramSocket clientSocket, String broadcastAddress, boolean scanNetwork) throws GreeException {
+ InetAddress ipAddress;
+ try {
+ ipAddress = InetAddress.getByName(broadcastAddress);
+ } catch (UnknownHostException e) {
+ throw new GreeException("Unknown host or invalid IP address", e);
+ }
+ try {
+ byte[] sendData = new byte[1024];
+ byte[] receiveData = new byte[1024];
+
+ // Send the Scan message
+ GreeScanRequestDTO scanGson = new GreeScanRequestDTO();
+ scanGson.t = GREE_CMDT_SCAN;
+ String scanReq = gson.toJson(scanGson);
+ sendData = scanReq.getBytes(StandardCharsets.UTF_8);
+ logger.debug("Sending scan packet to {}", ipAddress.getHostAddress());
+ clientSocket.setSoTimeout(DISCOVERY_TIMEOUT_MS);
+ DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, ipAddress, DISCOVERY_TIMEOUT_MS);
+ clientSocket.send(sendPacket);
+
+ // Loop for respnses from devices until we get a timeout.
+ int retries = scanNetwork ? MAX_SCAN_CYCLES : 1;
+ while ((retries > 0)) {
+ // Receive a response
+ DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
+ try {
+ clientSocket.receive(receivePacket);
+ InetAddress remoteAddress = receivePacket.getAddress();
+ int remotePort = receivePacket.getPort();
+
+ // Read the response
+ GreeScanResponseDTO scanResponseGson = fromJson(receivePacket, GreeScanResponseDTO.class);
+
+ // If there was no pack, ignore the response
+ if (scanResponseGson.pack == null) {
+ logger.debug("Invalid packet format, ignore");
+ retries--;
+ continue;
+ }
+
+ // Decrypt message - a a GreeException is thrown when something went wrong
+ String decryptedMsg = scanResponseGson.decryptedPack = GreeCryptoUtil
+ .decryptPack(GreeCryptoUtil.getAESGeneralKeyByteArray(), scanResponseGson.pack);
+ logger.debug("Response received from address {}: {}", remoteAddress.getHostAddress(), decryptedMsg);
+
+ // Create the JSON to hold the response values
+ scanResponseGson.packJson = gson.fromJson(decryptedMsg, GreeScanReponsePackDTO.class);
+
+ // Now make sure the device is reported as a Gree device
+ if (scanResponseGson.packJson.brand.equalsIgnoreCase("gree")) {
+ // Create a new GreeDevice
+ logger.debug("Discovered device at {}:{}", remoteAddress.getHostAddress(), remotePort);
+ GreeAirDevice newDevice = new GreeAirDevice(remoteAddress, remotePort, scanResponseGson);
+ addDevice(newDevice);
+ } else {
+ logger.debug("Unit discovered, but brand is not GREE");
+ }
+ } catch (SocketTimeoutException e) {
+ return;
+ } catch (IOException | JsonSyntaxException e) {
+ retries--;
+ if (retries == 0) {
+ throw new GreeException("Exception on device scan", e);
+ }
+ }
+ }
+ } catch (IOException e) {
+ throw new GreeException("I/O exception during device scan", e);
+ }
+ }
+
+ private T fromJson(DatagramPacket packet, Class classOfT) {
+ String json = new String(packet.getData(), StandardCharsets.UTF_8).replace("\\u0000", "").trim();
+ return gson.fromJson(json, classOfT);
+ }
+
+ public void addDevice(GreeAirDevice newDevice) {
+ deviceTable.put(newDevice.getId(), newDevice);
+ }
+
+ public GreeAirDevice getDevice(String id) {
+ return deviceTable.get(id);
+ }
+
+ public Map getDevices() {
+ return deviceTable;
+ }
+
+ public @Nullable GreeAirDevice getDeviceByIPAddress(String ipAddress) {
+ for (GreeAirDevice currDevice : deviceTable.values()) {
+ if (currDevice.getAddress().getHostAddress().equals(ipAddress)) {
+ return currDevice;
+ }
+ }
+ return null;
+ }
+
+ public int getScannedDeviceCount() {
+ return deviceTable.size();
+ }
+
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDiscoveryService.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDiscoveryService.java
new file mode 100644
index 0000000000000..ca11d61332ac8
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/discovery/GreeDiscoveryService.java
@@ -0,0 +1,129 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+
+package org.openhab.binding.gree.internal.discovery;
+
+import static org.openhab.binding.gree.internal.GreeBindingConstants.*;
+
+import java.net.DatagramSocket;
+import java.net.SocketException;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.config.discovery.AbstractDiscoveryService;
+import org.eclipse.smarthome.config.discovery.DiscoveryResult;
+import org.eclipse.smarthome.config.discovery.DiscoveryResultBuilder;
+import org.eclipse.smarthome.config.discovery.DiscoveryService;
+import org.eclipse.smarthome.core.net.NetworkAddressService;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingUID;
+import org.openhab.binding.gree.internal.GreeException;
+import org.openhab.binding.gree.internal.GreeTranslationProvider;
+import org.openhab.binding.gree.internal.handler.GreeAirDevice;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * {@link GreeDiscoveryService} implements the device discovery service. UDP broadtcast ius used to find the devices on
+ * the local subnet.
+ *
+ * @author Markus Michels - Initial contribution
+ *
+ */
+@NonNullByDefault
+@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.gree")
+public class GreeDiscoveryService extends AbstractDiscoveryService {
+ private static final int TIMEOUT_SEC = 10;
+ private final Logger logger = LoggerFactory.getLogger(GreeDiscoveryService.class);
+ private final GreeDeviceFinder deviceFinder;
+ private final GreeTranslationProvider messages;
+ private final String broadcastAddress;
+
+ @Activate
+ public GreeDiscoveryService(@Reference GreeDeviceFinder deviceFinder,
+ @Reference NetworkAddressService networkAddressService,
+ @Reference GreeTranslationProvider translationProvider,
+ @Nullable Map configProperties) {
+ super(SUPPORTED_THING_TYPES_UIDS, TIMEOUT_SEC);
+ this.messages = translationProvider;
+ this.deviceFinder = deviceFinder;
+ String ip = networkAddressService.getConfiguredBroadcastAddress();
+ broadcastAddress = ip != null ? ip : "";
+ activate(configProperties);
+ }
+
+ @Override
+ @Modified
+ protected void modified(@Nullable Map configProperties) {
+ super.modified(configProperties);
+ }
+
+ @Override
+ protected void startBackgroundDiscovery() {
+ // It's very unusual that a new unit gets installed frequently so we run the discovery once when the binding is
+ // started, but not frequently
+ scheduler.execute(this::startScan);
+ }
+
+ @Override
+ protected void stopBackgroundDiscovery() {
+ stopScan();
+ }
+
+ @Override
+ protected void startScan() {
+ try (DatagramSocket clientSocket = new DatagramSocket()) {
+ deviceFinder.scan(clientSocket, broadcastAddress, true);
+
+ int count = deviceFinder.getScannedDeviceCount();
+ logger.debug("{}", messages.get("discovery.result", count));
+ if (count > 0) {
+ logger.debug("Adding uinits to Inbox");
+ createResult(deviceFinder.getDevices());
+ }
+ } catch (GreeException e) {
+ logger.info("Discovery: {}", messages.get("discovery.exception", e.getMessage()));
+ } catch (SocketException | RuntimeException e) {
+ logger.warn("Discovery: {}", messages.get("discovery.exception", "RuntimeException"), e);
+ }
+ }
+
+ public void createResult(Map deviceList) {
+ for (GreeAirDevice device : deviceList.values()) {
+ String ipAddress = device.getAddress().getHostAddress();
+ logger.debug("{}", messages.get("discovery.newunit", device.getName(), ipAddress, device.getId()));
+ Map properties = new HashMap<>();
+ properties.put(Thing.PROPERTY_VENDOR, device.getVendor());
+ properties.put(Thing.PROPERTY_MODEL_ID, device.getModel());
+ properties.put(Thing.PROPERTY_MAC_ADDRESS, device.getId());
+ properties.put(PROPERTY_IP, ipAddress);
+ properties.put(PROPERTY_BROADCAST, broadcastAddress);
+ ThingUID thingUID = new ThingUID(THING_TYPE_GREEAIRCON, device.getId());
+ DiscoveryResult result = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
+ .withRepresentationProperty(Thing.PROPERTY_MAC_ADDRESS).withLabel(device.getName()).build();
+ thingDiscovered(result);
+ }
+ }
+
+ @Override
+ public void deactivate() {
+ removeOlderResults(getTimestampOfLastScan());
+ super.deactivate();
+ }
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindRequestPackDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindRequestPackDTO.java
new file mode 100644
index 0000000000000..4d06ba43b5b0d
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindRequestPackDTO.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal.gson;
+
+/**
+ *
+ * The GreeBindRequestPack4Gson class is used by Gson to hold values to be send to
+ * the Air Conditioner during Binding
+ *
+ * @author John Cunha - Initial contribution
+ */
+public class GreeBindRequestPackDTO {
+ public String mac = null;
+ public String t = null;
+ public int uid = 0;
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindResponseDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindResponseDTO.java
new file mode 100644
index 0000000000000..cf1590980b05b
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindResponseDTO.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal.gson;
+
+/**
+ *
+ * The GreeBindResponse4Gson class is used by Gson to hold values returned from
+ * the Air Conditioner during Binding
+ *
+ * @author John Cunha - Initial contribution
+ */
+public class GreeBindResponseDTO {
+
+ public String t = null;
+ public int i = 0;
+ public int uid = 0;
+ public String cid = null;
+ public String tcid = null;
+ public String pack = null;
+
+ public transient String decryptedPack = null;
+ public transient GreeBindResponsePackDTO packJson = null;
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindResponsePackDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindResponsePackDTO.java
new file mode 100644
index 0000000000000..af490bb44ea2b
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeBindResponsePackDTO.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal.gson;
+
+/**
+ *
+ * The GreeBindResponsePack4Gson class is used by Gson to hold values returned from
+ * the Air Conditioner during Binding
+ *
+ * @author John Cunha - Initial contribution
+ */
+public class GreeBindResponsePackDTO {
+ public String t = null;
+ public String mac = null;
+ public String key = null;
+ public int r = 0;
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecResponseDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecResponseDTO.java
new file mode 100644
index 0000000000000..5fc79e2e4a17a
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecResponseDTO.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal.gson;
+
+/**
+ *
+ * The GreeExecResponse4Gson class is used by Gson to hold values returned from
+ * the Air Conditioner during requests for Execution of Commands to the
+ * Air Conditioner.
+ *
+ * @author John Cunha - Initial contribution
+ */
+public class GreeExecResponseDTO {
+
+ public String t = null;
+ public int i = 0;
+ public int uid = 0;
+ public String cid = null;
+ public String tcid = null;
+ public String pack = null;
+
+ public transient String decryptedPack = null;
+ public transient GreeExecResponsePackDTO packJson = null;
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecResponsePackDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecResponsePackDTO.java
new file mode 100644
index 0000000000000..9d6833071505c
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecResponsePackDTO.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal.gson;
+
+/**
+ *
+ * The GreeExecResponsePack4Gson class is used by Gson to hold values returned from
+ * the Air Conditioner during requests for Execution of Commands to the
+ * Air Conditioner.
+ *
+ * @author John Cunha - Initial contribution
+ */
+public class GreeExecResponsePackDTO {
+ public String t = null;
+ public String mac = null;
+ public int r = 0;
+ public String[] opt = null;
+ public Integer[] p = null;
+ public Integer[] val = null;
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecuteCommandPackDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecuteCommandPackDTO.java
new file mode 100644
index 0000000000000..eb591bd8cb153
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeExecuteCommandPackDTO.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal.gson;
+
+/**
+ *
+ * The GreeExecuteCommandPack4Gson class is used by Gson to hold values to be send to
+ * the Air Conditioner during requests for Execution of Commands to the
+ * Air Conditioner.
+ *
+ * @author John Cunha - Initial contribution
+ */
+public class GreeExecuteCommandPackDTO {
+
+ public String[] opt = null;
+ public Integer[] p = null;
+ public String t = null;
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeReqStatusDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeReqStatusDTO.java
new file mode 100644
index 0000000000000..0a0edf2cdd460
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeReqStatusDTO.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal.gson;
+
+/**
+ *
+ * The GreeReqStatus4Gson class is used by Gson to hold values to be send to
+ * the Air Conditioner during requests for Status Updates to the
+ * Air Conditioner.
+ *
+ * @author John Cunha - Initial contribution
+ */
+public class GreeReqStatusDTO {
+ public String cid = null;
+ public int i = 0;
+ public String t = null;
+ public int uid = 0;
+ public String pack = null;
+ public String tcid = null;
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeReqStatusPackDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeReqStatusPackDTO.java
new file mode 100644
index 0000000000000..1f47d5c096926
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeReqStatusPackDTO.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal.gson;
+
+/**
+ *
+ * The GreeReqStatusPack4Gson class is used by Gson to hold values to be send to
+ * the Air Conditioner during requests for Status Updates to the
+ * Air Conditioner.
+ *
+ * @author John Cunha - Initial contribution
+ */
+public class GreeReqStatusPackDTO {
+
+ public String t = null;
+ public String[] cols = null;
+ public String mac = null;
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeRequestDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeRequestDTO.java
new file mode 100644
index 0000000000000..b8b493b88e75f
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeRequestDTO.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal.gson;
+
+/**
+ *
+ * The GreeBindRequest4Gson class is used by Gson to hold values to be send to
+ * the Air Conditioner during Binding
+ *
+ * @author John Cunha - Initial contribution
+ */
+public class GreeRequestDTO {
+
+ public int uid = 0;
+ public String t = null;
+ public int i = 0;
+ public String pack = null;
+ public String cid = null;
+ public String tcid = null;
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanReponsePackDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanReponsePackDTO.java
new file mode 100644
index 0000000000000..e2b6d5c411c7c
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanReponsePackDTO.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal.gson;
+
+/**
+ *
+ * The GreeScanReponsePack4Gson class is used by Gson to hold values returned by
+ * the Air Conditioner during Scan Requests to the Air Conditioner.
+ *
+ * @author John Cunha - Initial contribution
+ */
+public class GreeScanReponsePackDTO {
+
+ public String t = null;
+ public String cid = null;
+ public String bc = null;
+ public String brand = null;
+ public String catalog = null;
+ public String mac = null;
+ public String mid = null;
+ public String model = null;
+ public String name = null;
+ public String series = null;
+ public String vender = null;
+ public String ver = null;
+ public int lock = 0;
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanRequestDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanRequestDTO.java
new file mode 100644
index 0000000000000..cc6fec59ac9bf
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanRequestDTO.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal.gson;
+
+/**
+ *
+ * The GreeScanRequest4Gson class is used by Gson to hold values sent to
+ * the Air Conditioner during Scan Requests to the Air Conditioner.
+ *
+ * @author John Cunha - Initial contribution
+ */
+public class GreeScanRequestDTO {
+ public String t = null;
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanResponseDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanResponseDTO.java
new file mode 100644
index 0000000000000..1b58834c28669
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeScanResponseDTO.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal.gson;
+
+/**
+ *
+ * The GreeScanResponse4Gson class is used by Gson to hold values returned by
+ * the Air Conditioner during Scan Requests to the Air Conditioner.
+ *
+ * @author John Cunha - Initial contribution
+ */
+public class GreeScanResponseDTO {
+ public String t = null;
+ public int i = 0;
+ public int uid = 0;
+ public String cid = null;
+ public String tcid = null;
+ public String pack = null;
+ public transient String decryptedPack = null;
+ public transient GreeScanReponsePackDTO packJson = null;
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeStatusResponseDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeStatusResponseDTO.java
new file mode 100644
index 0000000000000..baa012866861b
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeStatusResponseDTO.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal.gson;
+
+/**
+ *
+ * The GreeStatusResponse4Gson class is used by Gson to hold values returned from
+ * the Air Conditioner during requests for Status Updates to the
+ * Air Conditioner.
+ *
+ * @author John Cunha - Initial contribution
+ */
+public class GreeStatusResponseDTO {
+
+ public String t = null;
+ public int i = 0;
+ public int uid = 0;
+ public String cid = null;
+ public String tcid = null;
+ public String pack = null;
+
+ public transient String decryptedPack = null;
+ public transient GreeStatusResponsePackDTO packJson = null;
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeStatusResponsePackDTO.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeStatusResponsePackDTO.java
new file mode 100644
index 0000000000000..4cbdf08484ea4
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/gson/GreeStatusResponsePackDTO.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal.gson;
+
+/**
+ *
+ * The GreeStatusResponsePack4Gson class is used by Gson to hold values returned from
+ * the Air Conditioner during requests for Status Updates to the
+ * Air Conditioner.
+ *
+ * @author John Cunha - Initial contribution
+ */
+public class GreeStatusResponsePackDTO {
+
+ public GreeStatusResponsePackDTO(GreeStatusResponsePackDTO other) {
+ if (other.cols != null) {
+ cols = new String[other.cols.length];
+ dat = new Integer[other.dat.length];
+ System.arraycopy(other.cols, 0, cols, 0, other.cols.length);
+ System.arraycopy(other.dat, 0, dat, 0, other.dat.length);
+ } else {
+ cols = new String[0];
+ dat = new Integer[0];
+ }
+ }
+
+ public String t = null;
+ public String mac = null;
+ public int r = 0;
+ public String[] cols = null;
+ public Integer[] dat = null;
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeAirDevice.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeAirDevice.java
new file mode 100644
index 0000000000000..d6a66b65a3bb0
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeAirDevice.java
@@ -0,0 +1,512 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal.handler;
+
+import static org.openhab.binding.gree.internal.GreeBindingConstants.*;
+
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.smarthome.core.library.types.QuantityType;
+import org.eclipse.smarthome.core.library.unit.SIUnits;
+import org.openhab.binding.gree.internal.GreeCryptoUtil;
+import org.openhab.binding.gree.internal.GreeException;
+import org.openhab.binding.gree.internal.gson.GreeBindRequestPackDTO;
+import org.openhab.binding.gree.internal.gson.GreeBindResponseDTO;
+import org.openhab.binding.gree.internal.gson.GreeBindResponsePackDTO;
+import org.openhab.binding.gree.internal.gson.GreeExecResponseDTO;
+import org.openhab.binding.gree.internal.gson.GreeExecResponsePackDTO;
+import org.openhab.binding.gree.internal.gson.GreeExecuteCommandPackDTO;
+import org.openhab.binding.gree.internal.gson.GreeReqStatusPackDTO;
+import org.openhab.binding.gree.internal.gson.GreeRequestDTO;
+import org.openhab.binding.gree.internal.gson.GreeScanResponseDTO;
+import org.openhab.binding.gree.internal.gson.GreeStatusResponseDTO;
+import org.openhab.binding.gree.internal.gson.GreeStatusResponsePackDTO;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+
+/**
+ * The GreeDevice object repesents a Gree Airconditioner and provides
+ * device specific attributes as well a the functionality for the Air Conditioner
+ *
+ * @author John Cunha - Initial contribution
+ * @author Markus Michels - Refactoring, adapted to OH 2.5x
+ */
+@NonNullByDefault
+public class GreeAirDevice {
+ private final Logger logger = LoggerFactory.getLogger(GreeAirDevice.class);
+ private final static Gson gson = new Gson();
+ private boolean isBound = false;
+ private final InetAddress ipAddress;
+ private int port = 0;
+ private String encKey = "";
+ private Optional scanResponseGson = Optional.empty();
+ private Optional statusResponseGson = Optional.empty();
+ private Optional prevStatusResponsePackGson = Optional.empty();
+
+ public GreeAirDevice() {
+ ipAddress = InetAddress.getLoopbackAddress();
+ }
+
+ public GreeAirDevice(InetAddress ipAddress, int port, GreeScanResponseDTO scanResponse) {
+ this.ipAddress = ipAddress;
+ this.port = port;
+ this.scanResponseGson = Optional.of(scanResponse);
+ }
+
+ public void getDeviceStatus(DatagramSocket clientSocket) throws GreeException {
+
+ if (!isBound) {
+ throw new GreeException("Device not bound");
+ }
+ try {
+ // Set the values in the HashMap
+ ArrayList columns = new ArrayList<>();
+ columns.add(GREE_PROP_POWER);
+ columns.add(GREE_PROP_MODE);
+ columns.add(GREE_PROP_SETTEMP);
+ columns.add(GREE_PROP_WINDSPEED);
+ columns.add(GREE_PROP_AIR);
+ columns.add(GREE_PROP_DRY);
+ columns.add(GREE_PROP_HEALTH);
+ columns.add(GREE_PROP_SLEEP);
+ columns.add(GREE_PROP_LIGHT);
+ columns.add(GREE_PROP_SWINGLEFTRIGHT);
+ columns.add(GREE_PROP_SWINGUPDOWN);
+ columns.add(GREE_PROP_QUIET);
+ columns.add(GREE_PROP_TURBO);
+ columns.add(GREE_PROP_TEMPUNIT);
+ columns.add(GREE_PROP_HEAT);
+ columns.add(GREE_PROP_HEATCOOL);
+ columns.add(GREE_PROP_TEMPREC);
+ columns.add(GREE_PROP_PWR_SAVING);
+ columns.add(GREE_PROP_NOISESET);
+
+ // Convert the parameter map values to arrays
+ String[] colArray = columns.toArray(new String[0]);
+
+ // Prep the Command Request pack
+ GreeReqStatusPackDTO reqStatusPackGson = new GreeReqStatusPackDTO();
+ reqStatusPackGson.t = GREE_CMDT_STATUS;
+ reqStatusPackGson.cols = colArray;
+ reqStatusPackGson.mac = getId();
+ String reqStatusPackStr = gson.toJson(reqStatusPackGson);
+
+ // Encrypt and send the Status Request pack
+ String encryptedStatusReqPacket = GreeCryptoUtil.encryptPack(getKey(), reqStatusPackStr);
+ DatagramPacket sendPacket = createPackRequest(0,
+ new String(encryptedStatusReqPacket.getBytes(), StandardCharsets.UTF_8));
+ clientSocket.send(sendPacket);
+
+ // Keep a copy of the old response to be used to check if values have changed
+ // If first time running, there will not be a previous GreeStatusResponsePack4Gson
+ if (statusResponseGson.isPresent() && statusResponseGson.get().packJson != null) {
+ prevStatusResponsePackGson = Optional
+ .of(new GreeStatusResponsePackDTO(statusResponseGson.get().packJson));
+ }
+
+ // Read the response, create the JSON to hold the response values
+ GreeStatusResponseDTO resp = receiveResponse(clientSocket, GreeStatusResponseDTO.class);
+ resp.decryptedPack = GreeCryptoUtil.decryptPack(getKey(), resp.pack);
+ logger.debug("Response from device: {}", resp.decryptedPack);
+ resp.packJson = gson.fromJson(resp.decryptedPack, GreeStatusResponsePackDTO.class);
+
+ // save the results
+ statusResponseGson = Optional.of(resp);
+ updateTempFtoC();
+ } catch (IOException | JsonSyntaxException e) {
+ throw new GreeException("I/O exception while updating status", e);
+ } catch (RuntimeException e) {
+ logger.debug("Exception", e);
+ String json = statusResponseGson.map(r -> r.packJson.toString()).orElse("n/a");
+ throw new GreeException("Exception while updating status, JSON=" + json, e);
+ }
+ }
+
+ public void bindWithDevice(DatagramSocket clientSocket) throws GreeException {
+ try {
+ // Prep the Binding Request pack
+ GreeBindRequestPackDTO bindReqPackGson = new GreeBindRequestPackDTO();
+ bindReqPackGson.mac = getId();
+ bindReqPackGson.t = GREE_CMDT_BIND;
+ bindReqPackGson.uid = 0;
+ String bindReqPackStr = gson.toJson(bindReqPackGson);
+
+ // Encrypt and send the Binding Request pack
+ String encryptedBindReqPacket = GreeCryptoUtil.encryptPack(GreeCryptoUtil.getAESGeneralKeyByteArray(),
+ bindReqPackStr);
+ DatagramPacket sendPacket = createPackRequest(1, encryptedBindReqPacket);
+ clientSocket.send(sendPacket);
+
+ // Recieve a response, create the JSON to hold the response values
+ GreeBindResponseDTO resp = receiveResponse(clientSocket, GreeBindResponseDTO.class);
+ resp.decryptedPack = GreeCryptoUtil.decryptPack(GreeCryptoUtil.getAESGeneralKeyByteArray(), resp.pack);
+ resp.packJson = gson.fromJson(resp.decryptedPack, GreeBindResponsePackDTO.class);
+
+ // Now set the key and flag to indicate the bind was succesful
+ encKey = resp.packJson.key;
+
+ // save the outcome
+ isBound = true;
+ } catch (IOException | JsonSyntaxException e) {
+ throw new GreeException("Unable to bind to device", e);
+ }
+ }
+
+ public void setDevicePower(DatagramSocket clientSocket, int value) throws GreeException {
+ setCommandValue(clientSocket, GREE_PROP_POWER, value);
+ }
+
+ public void setDeviceMode(DatagramSocket clientSocket, int value) throws GreeException {
+ if ((value < 0 || value > 4)) {
+ throw new GreeException("Device mode out of range!");
+ }
+ setCommandValue(clientSocket, GREE_PROP_MODE, value);
+ }
+
+ public void setDeviceSwingUpDown(DatagramSocket clientSocket, int value) throws GreeException {
+ // Only values 0,1,2,3,4,5,6,10,11 allowed
+ if ((value < 0 || value > 11) || (value > 6 && value < 10)) {
+ throw new GreeException("SwingUpDown value out of range!");
+ }
+ setCommandValue(clientSocket, GREE_PROP_SWINGUPDOWN, value);
+ }
+
+ public void setDeviceSwingLeftRight(DatagramSocket clientSocket, int value) throws GreeException {
+ setCommandValue(clientSocket, GREE_PROP_SWINGLEFTRIGHT, value, 0, 6);
+ }
+
+ /**
+ * Only allow this to happen if this device has been bound and values are valid
+ * Possible values are :
+ * 0 : Auto
+ * 1 : Low
+ * 2 : Medium Low
+ * 3 : Medium
+ * 4 : Medium High
+ * 5 : High
+ */
+ public void setDeviceWindspeed(DatagramSocket clientSocket, int value) throws GreeException {
+ if (value < 0 || value > 5) {
+ throw new GreeException("Value out of range!");
+ }
+
+ HashMap parameters = new HashMap<>();
+ parameters.put(GREE_PROP_WINDSPEED, value);
+ parameters.put(GREE_PROP_QUIET, 0);
+ parameters.put(GREE_PROP_TURBO, 0);
+ parameters.put(GREE_PROP_NOISE, 0);
+ executeCommand(clientSocket, parameters);
+ }
+
+ public void setDeviceTurbo(DatagramSocket clientSocket, int value) throws GreeException {
+ setCommandValue(clientSocket, GREE_PROP_TURBO, value, 0, 1);
+ }
+
+ public void setQuietMode(DatagramSocket clientSocket, int value) throws GreeException {
+ setCommandValue(clientSocket, GREE_PROP_QUIET, value, 0, 2);
+ }
+
+ public int getDeviceTurbo() {
+ return getIntStatusVal(GREE_PROP_TURBO);
+ }
+
+ public void setDeviceLight(DatagramSocket clientSocket, int value) throws GreeException {
+ setCommandValue(clientSocket, GREE_PROP_LIGHT, value);
+ }
+
+ /**
+ * @param value set temperature in degrees Celsius or Fahrenheit
+ */
+ public void setDeviceTempSet(DatagramSocket clientSocket, QuantityType> temp) throws GreeException {
+ // If commanding Fahrenheit set halfStep to 1 or 0 to tell the A/C which F integer
+ // temperature to use as celsius alone is ambigious
+ double newVal = temp.doubleValue();
+ int CorF = temp.getUnit() == SIUnits.CELSIUS ? TEMP_UNIT_CELSIUS : TEMP_UNIT_FAHRENHEIT; // 0=Celsius,
+ // 1=Fahrenheit
+ if (((CorF == TEMP_UNIT_CELSIUS) && (newVal < TEMP_MIN_C || newVal > TEMP_MAX_C))
+ || ((CorF == TEMP_UNIT_FAHRENHEIT) && (newVal < TEMP_MIN_F || newVal > TEMP_MAX_F))) {
+ throw new IllegalArgumentException("Temp Value out of Range");
+ }
+
+ // Default for Celsius
+ int outVal = (int) newVal;
+ int halfStep = TEMP_HALFSTEP_NO; // for whatever reason halfStep is not supported for Celsius
+
+ // If value argument is degrees F, convert Fahrenheit to Celsius,
+ // SetTem input to A/C always in Celsius despite passing in 1 to TemUn
+ // ******************TempRec TemSet Mapping for setting Fahrenheit****************************
+ // F = [68...86]
+ // 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,
+ // 28.8, 29.4, 30.0]
+ //
+ // TemSet = [20..30] or [68..86]
+ // TemRec = value - (value) > 0 ? 1 : 1 -> when xx.5 is request xx will become TemSet and halfStep the indicator
+ // for "half on top of TemSet"
+ // ******************TempRec TemSet Mapping for setting Fahrenheit****************************
+ // subtract the float version - the int version to get the fractional difference
+ // if the difference is positive set halfStep to 1, negative to 0
+ if (CorF == TEMP_UNIT_FAHRENHEIT) { // If Fahrenheit,
+ halfStep = newVal - outVal > 0 ? TEMP_HALFSTEP_YES : TEMP_HALFSTEP_NO;
+ }
+ logger.debug("Converted temp from {}{} to temp={}, halfStep={}, unit={})", newVal, temp.getUnit(), outVal,
+ halfStep, CorF == TEMP_UNIT_CELSIUS ? "C" : "F");
+
+ // Set the values in the HashMap
+ HashMap parameters = new HashMap<>();
+ parameters.put(GREE_PROP_TEMPUNIT, CorF);
+ parameters.put(GREE_PROP_SETTEMP, outVal);
+ parameters.put(GREE_PROP_TEMPREC, halfStep);
+ executeCommand(clientSocket, parameters);
+ }
+
+ public void setDeviceAir(DatagramSocket clientSocket, int value) throws GreeException {
+ setCommandValue(clientSocket, GREE_PROP_AIR, value);
+ }
+
+ public void setDeviceDry(DatagramSocket clientSocket, int value) throws GreeException {
+ setCommandValue(clientSocket, GREE_PROP_DRY, value);
+ }
+
+ public void setDeviceHealth(DatagramSocket clientSocket, int value) throws GreeException {
+ setCommandValue(clientSocket, GREE_PROP_HEALTH, value);
+ }
+
+ public void setDevicePwrSaving(DatagramSocket clientSocket, int value) throws GreeException {
+ // Set the values in the HashMap
+ HashMap parameters = new HashMap<>();
+ parameters.put(GREE_PROP_PWR_SAVING, value);
+ parameters.put(GREE_PROP_WINDSPEED, 0);
+ parameters.put(GREE_PROP_QUIET, 0);
+ parameters.put(GREE_PROP_TURBO, 0);
+ parameters.put(GREE_PROP_SLEEP, 0);
+ parameters.put(GREE_PROP_SLEEPMODE, 0);
+ executeCommand(clientSocket, parameters);
+ }
+
+ public int getIntStatusVal(String valueName) {
+ /*
+ * Note : Values can be:
+ * "Pow": Power (0 or 1)
+ * "Mod": Mode: Auto: 0, Cool: 1, Dry: 2, Fan: 3, Heat: 4
+ * "SetTem": Requested Temperature
+ * "WdSpd": Fan Speed : Low:1, Medium Low:2, Medium :3, Medium High :4, High :5
+ * "Air": Air Mode Enabled
+ * "Blo": Dry
+ * "Health": Health
+ * "SwhSlp": Sleep
+ * "SlpMod": ???
+ * "Lig": Light On
+ * "SwingLfRig": Swing Left Right
+ * "SwUpDn": Swing Up Down: // Ceiling:0, Upwards : 10, Downwards : 11, Full range : 1
+ * "Quiet": Quiet mode
+ * "Tur": Turbo
+ * "StHt": 0,
+ * "TemUn": Temperature unit, 0 for Celsius, 1 for Fahrenheit
+ * "HeatCoolType"
+ * "TemRec": (0 or 1), Send with SetTem, when TemUn==1, distinguishes between upper and lower integer Fahrenheit
+ * temp
+ * "SvSt": Power Saving
+ */
+ // Find the valueName in the Returned Status object
+ if (isStatusAvailable()) {
+ List colList = Arrays.asList(statusResponseGson.get().packJson.cols);
+ List valList = Arrays.asList(statusResponseGson.get().packJson.dat);
+ int valueArrayposition = colList.indexOf(valueName);
+ if (valueArrayposition != -1) {
+ // get the Corresponding value
+ Integer value = valList.get(valueArrayposition);
+ return value;
+ }
+ }
+
+ return -1;
+ }
+
+ public boolean isStatusAvailable() {
+ return statusResponseGson.isPresent();
+ }
+
+ public boolean hasStatusValChanged(String valueName) throws GreeException {
+ if (!prevStatusResponsePackGson.isPresent()) {
+ return true; // update value if there is no previous one
+ }
+ // Find the valueName in the Current Status object
+ List currcolList = Arrays.asList(statusResponseGson.get().packJson.cols);
+ List currvalList = Arrays.asList(statusResponseGson.get().packJson.dat);
+ int currvalueArrayposition = currcolList.indexOf(valueName);
+ if (currvalueArrayposition == -1) {
+ throw new GreeException("Unable to decode device status");
+ }
+
+ // Find the valueName in the Previous Status object
+ List prevcolList = Arrays.asList(prevStatusResponsePackGson.get().cols);
+ List prevvalList = Arrays.asList(prevStatusResponsePackGson.get().dat);
+ int prevvalueArrayposition = prevcolList.indexOf(valueName);
+ if (prevvalueArrayposition == -1) {
+ throw new GreeException("Unable to get status value");
+ }
+
+ // Finally Compare the values
+ return currvalList.get(currvalueArrayposition) != prevvalList.get(prevvalueArrayposition);
+ }
+
+ protected void executeCommand(DatagramSocket clientSocket, Map parameters) throws GreeException {
+ // Only allow this to happen if this device has been bound
+ if (!getIsBound()) {
+ throw new GreeException("Device is not bound!");
+ }
+
+ try {
+ // Convert the parameter map values to arrays
+ String[] keyArray = parameters.keySet().toArray(new String[0]);
+ Integer[] valueArray = parameters.values().toArray(new Integer[0]);
+
+ // Prep the Command Request pack
+ GreeExecuteCommandPackDTO execCmdPackGson = new GreeExecuteCommandPackDTO();
+ execCmdPackGson.opt = keyArray;
+ execCmdPackGson.p = valueArray;
+ execCmdPackGson.t = GREE_CMDT_CMD;
+ String execCmdPackStr = gson.toJson(execCmdPackGson);
+
+ // Now encrypt and send the Command Request pack
+ String encryptedCommandReqPacket = GreeCryptoUtil.encryptPack(getKey(), execCmdPackStr);
+ DatagramPacket sendPacket = createPackRequest(0, encryptedCommandReqPacket);
+ clientSocket.send(sendPacket);
+
+ // Receive and decode result
+ GreeExecResponseDTO execResponseGson = receiveResponse(clientSocket, GreeExecResponseDTO.class);
+ execResponseGson.decryptedPack = GreeCryptoUtil.decryptPack(getKey(), execResponseGson.pack);
+
+ // Create the JSON to hold the response values
+ execResponseGson.packJson = gson.fromJson(execResponseGson.decryptedPack, GreeExecResponsePackDTO.class);
+ } catch (IOException | JsonSyntaxException e) {
+ throw new GreeException("Exception on command execution", e);
+ }
+ }
+
+ private void setCommandValue(DatagramSocket clientSocket, String command, int value) throws GreeException {
+ executeCommand(clientSocket, Collections.singletonMap(command, value));
+ }
+
+ private void setCommandValue(DatagramSocket clientSocket, String command, int value, int min, int max)
+ throws GreeException {
+ if ((value < min) || (value > max)) {
+ throw new GreeException("Command value out of range!");
+ }
+ executeCommand(clientSocket, Collections.singletonMap(command, value));
+ }
+
+ private DatagramPacket createPackRequest(int i, String pack) {
+ GreeRequestDTO request = new GreeRequestDTO();
+ request.cid = GREE_CID;
+ request.i = i;
+ request.t = GREE_CMDT_PACK;
+ request.uid = 0;
+ request.tcid = getId();
+ request.pack = pack;
+ byte[] sendData = gson.toJson(request).getBytes(StandardCharsets.UTF_8);
+ DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, ipAddress, port);
+ return sendPacket;
+ }
+
+ private T receiveResponse(DatagramSocket clientSocket, Class classOfT)
+ throws IOException, JsonSyntaxException {
+ byte[] receiveData = new byte[1024];
+ DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
+ clientSocket.receive(receivePacket);
+ String json = new String(receivePacket.getData(), StandardCharsets.UTF_8).replace("\\u0000", "").trim();
+ return gson.fromJson(json, classOfT);
+ }
+
+ private void updateTempFtoC() {
+ // Status message back from A/C always reports degrees C
+ // If using Fahrenheit, us SetTem, TemUn and TemRec to reconstruct the Fahrenheit temperature
+ // Get Celsius or Fahrenheit from status message
+ int CorF = getIntStatusVal(GREE_PROP_TEMPUNIT);
+ int newVal = getIntStatusVal(GREE_PROP_SETTEMP);
+ int halfStep = getIntStatusVal(GREE_PROP_TEMPREC);
+
+ if ((CorF == -1) || (newVal == -1) || (halfStep == -1)) {
+ throw new IllegalArgumentException("SetTem,TemUn or TemRec is invalid, not performing conversion");
+ } else if (CorF == 1) { // convert SetTem to Fahrenheit
+ // Find the valueName in the Returned Status object
+ String[] columns = statusResponseGson.get().packJson.cols;
+ Integer[] values = statusResponseGson.get().packJson.dat;
+ List colList = Arrays.asList(columns);
+ int valueArrayposition = colList.indexOf(GREE_PROP_SETTEMP);
+ if (valueArrayposition != -1) {
+ // convert Celsius to Fahrenheit,
+ // SetTem status returns degrees C regardless of TempUn setting
+
+ // Perform the float Celsius to Fahrenheit conversion add or subtract 0.5 based on the value of TemRec
+ // (0 = -0.5, 1 = +0.5). Pass into a rounding function, this yeild the correct Fahrenheit Temperature to
+ // match A/C display
+ newVal = (int) (Math.round(((newVal * 9.0 / 5.0) + 32.0) + halfStep - 0.5));
+
+ // Update the status array with F temp, assume this is updating the array in situ
+ values[valueArrayposition] = newVal;
+ }
+ }
+ }
+
+ public InetAddress getAddress() {
+ return ipAddress;
+ }
+
+ public boolean getIsBound() {
+ return isBound;
+ }
+
+ public byte[] getKey() {
+ return encKey.getBytes(StandardCharsets.UTF_8);
+ }
+
+ public String getId() {
+ return scanResponseGson.isPresent() ? scanResponseGson.get().packJson.mac : "";
+ }
+
+ public String getName() {
+ return scanResponseGson.isPresent() ? scanResponseGson.get().packJson.name : "";
+ }
+
+ public String getVendor() {
+ return scanResponseGson.isPresent()
+ ? scanResponseGson.get().packJson.brand + " " + scanResponseGson.get().packJson.vender
+ : "";
+ }
+
+ public String getModel() {
+ return scanResponseGson.isPresent()
+ ? scanResponseGson.get().packJson.series + " " + scanResponseGson.get().packJson.model
+ : "";
+ }
+
+ public void setScanResponseGson(GreeScanResponseDTO gson) {
+ scanResponseGson = Optional.of(gson);
+ }
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeHandler.java b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeHandler.java
new file mode 100644
index 0000000000000..256abbf883266
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/java/org/openhab/binding/gree/internal/handler/GreeHandler.java
@@ -0,0 +1,552 @@
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.gree.internal.handler;
+
+import static org.openhab.binding.gree.internal.GreeBindingConstants.*;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.net.DatagramSocket;
+import java.time.Instant;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import javax.measure.Unit;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.smarthome.core.library.types.DecimalType;
+import org.eclipse.smarthome.core.library.types.OnOffType;
+import org.eclipse.smarthome.core.library.types.QuantityType;
+import org.eclipse.smarthome.core.library.types.StringType;
+import org.eclipse.smarthome.core.library.unit.ImperialUnits;
+import org.eclipse.smarthome.core.library.unit.SIUnits;
+import org.eclipse.smarthome.core.thing.Channel;
+import org.eclipse.smarthome.core.thing.ChannelUID;
+import org.eclipse.smarthome.core.thing.Thing;
+import org.eclipse.smarthome.core.thing.ThingStatus;
+import org.eclipse.smarthome.core.thing.ThingStatusDetail;
+import org.eclipse.smarthome.core.thing.binding.BaseThingHandler;
+import org.eclipse.smarthome.core.types.Command;
+import org.eclipse.smarthome.core.types.RefreshType;
+import org.eclipse.smarthome.core.types.State;
+import org.openhab.binding.gree.internal.GreeConfiguration;
+import org.openhab.binding.gree.internal.GreeException;
+import org.openhab.binding.gree.internal.GreeTranslationProvider;
+import org.openhab.binding.gree.internal.discovery.GreeDeviceFinder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link GreeHandler} is responsible for handling commands, which are sent to one of the channels.
+ *
+ * @author John Cunha - Initial contribution
+ * @author Markus Michels - Refactoring, adapted to OH 2.5x
+ */
+@NonNullByDefault
+public class GreeHandler extends BaseThingHandler {
+ private final Logger logger = LoggerFactory.getLogger(GreeHandler.class);
+ private final GreeTranslationProvider messages;
+ private final GreeDeviceFinder deviceFinder;
+ private final String thingId;
+ private GreeConfiguration config = new GreeConfiguration();
+ private GreeAirDevice device = new GreeAirDevice();
+ private Optional clientSocket = Optional.empty();
+ private boolean forceRefresh = false;
+
+ private @Nullable ScheduledFuture> refreshTask;
+ private @Nullable Future> initializeFuture;
+ private long lastRefreshTime = 0;
+
+ public GreeHandler(Thing thing, GreeTranslationProvider messages, GreeDeviceFinder deviceFinder) {
+ super(thing);
+ this.messages = messages;
+ this.deviceFinder = deviceFinder;
+ this.thingId = getThing().getUID().getId();
+ }
+
+ @Override
+ public void initialize() {
+ config = getConfigAs(GreeConfiguration.class);
+ if (config.ipAddress.isEmpty() || (config.refresh < 0)) {
+ String message = messages.get("thinginit.invconf");
+ logger.warn("{}: {}", thingId, message);
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, message);
+ }
+
+ // set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
+ // the framework is then able to reuse the resources from the thing handler initialization.
+ updateStatus(ThingStatus.UNKNOWN);
+
+ // Start the automatic refresh cycles
+ startAutomaticRefresh();
+ initializeFuture = scheduler.submit(this::initializeThing);
+ }
+
+ private void initializeThing() {
+ String message = "";
+ try {
+ if (!clientSocket.isPresent()) {
+ clientSocket = Optional.of(new DatagramSocket());
+ clientSocket.get().setSoTimeout(DATAGRAM_SOCKET_TIMEOUT);
+ }
+ // Find the GREE device
+ deviceFinder.scan(clientSocket.get(), config.ipAddress, false);
+ GreeAirDevice newDevice = deviceFinder.getDeviceByIPAddress(config.ipAddress);
+ if (newDevice != null) {
+ // Ok, our device responded, now let's Bind with it
+ device = newDevice;
+ device.bindWithDevice(clientSocket.get());
+ if (device.getIsBound()) {
+ updateStatus(ThingStatus.ONLINE);
+ return;
+ }
+ }
+
+ message = messages.get("thinginit.failed");
+ logger.info("{}: {}", thingId, message);
+ } catch (GreeException e) {
+ logger.info("{}: {}", thingId, messages.get("thinginit.exception", e.getMessage()));
+ } catch (IOException e) {
+ logger.warn("{}: {}", thingId, messages.get("thinginit.exception", "I/O Error"), e);
+ } catch (RuntimeException e) {
+ logger.warn("{}: {}", thingId, messages.get("thinginit.exception", "RuntimeException"), e);
+ }
+
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
+ }
+
+ @Override
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ if (command instanceof RefreshType) {
+ // The thing is updated by the scheduled automatic refresh so do nothing here.
+ } else {
+ logger.debug("{}: Issue command {}Â to channe {}", thingId, command, channelUID.getIdWithoutGroup());
+ String channelId = channelUID.getIdWithoutGroup();
+ logger.debug("{}: Handle command {} for channel {}, command class {}", thingId, command, channelId,
+ command.getClass());
+ try {
+ DatagramSocket socket = clientSocket.get();
+ switch (channelId) {
+ case MODE_CHANNEL:
+ handleModeCommand(socket, command);
+ break;
+ case POWER_CHANNEL:
+ device.setDevicePower(socket, getOnOff(command));
+ break;
+ case TURBO_CHANNEL:
+ device.setDeviceTurbo(socket, getOnOff(command));
+ break;
+ case LIGHT_CHANNEL:
+ device.setDeviceLight(socket, getOnOff(command));
+ break;
+ case TEMP_CHANNEL:
+ // Set value, read back effective one and update channel
+ // e.g. 22.5C will result in 22.0, because the AC doesn't support half-steps for C
+ device.setDeviceTempSet(socket, convertTemp(command));
+ break;
+ case SWINGUD_CHANNEL:
+ device.setDeviceSwingUpDown(socket, getNumber(command));
+ break;
+ case SWINGLR_CHANNEL:
+ device.setDeviceSwingLeftRight(socket, getNumber(command));
+ break;
+ case WINDSPEED_CHANNEL:
+ device.setDeviceWindspeed(socket, getNumber(command));
+ break;
+ case QUIET_CHANNEL:
+ handleQuietCommand(socket, command);
+ break;
+ case AIR_CHANNEL:
+ device.setDeviceAir(socket, getOnOff(command));
+ break;
+ case DRY_CHANNEL:
+ device.setDeviceDry(socket, getOnOff(command));
+ break;
+ case HEALTH_CHANNEL:
+ device.setDeviceHealth(socket, getOnOff(command));
+ break;
+ case PWRSAV_CHANNEL:
+ device.setDevicePwrSaving(socket, getOnOff(command));
+ break;
+ }
+
+ // force refresh on next status refresh cycle
+ forceRefresh = true;
+ } catch (GreeException e) {
+ String message = logInfo("command.exception", command, channelId) + ": " + e.getMessage();
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
+ } catch (IllegalArgumentException e) {
+ logInfo("command.invarg", command, channelId);
+ } catch (RuntimeException e) {
+ logger.warn("{}: {}", thingId, messages.get("command.exception", command, channelId), e);
+ }
+ }
+ }
+
+ private void handleModeCommand(DatagramSocket socket, Command command) throws GreeException {
+ int mode = -1;
+ String modeStr = "";
+ boolean isNumber = false;
+ if (command instanceof DecimalType) {
+ // backward compatibility when channel was Number
+ mode = ((DecimalType) command).intValue();
+ } else if (command instanceof OnOffType) {
+ // Switch
+ logger.debug("{}: Send Power-{}", thingId, command);
+ device.setDevicePower(socket, getOnOff(command));
+ } else /* String */ {
+ modeStr = command.toString().toLowerCase();
+ switch (modeStr) {
+ case MODE_AUTO:
+ mode = GREE_MODE_AUTO;
+ break;
+ case MODE_COOL:
+ mode = GREE_MODE_COOL;
+ break;
+ case MODE_HEAT:
+ mode = GREE_MODE_HEAT;
+ break;
+ case MODE_DRY:
+ mode = GREE_MODE_DRY;
+ break;
+ case MODE_FAN:
+ case MODE_FAN2:
+ mode = GREE_MODE_FAN;
+ break;
+ case MODE_ECO:
+ // power saving will be set after the uinit was turned on
+ mode = GREE_MODE_COOL;
+ break;
+ case MODE_ON:
+ case MODE_OFF:
+ logger.debug("{}: Turn unit {}", thingId, modeStr);
+ device.setDevicePower(socket, modeStr.equals(MODE_ON) ? 1 : 0);
+ return;
+ default:
+ // fallback: mode number, pass transparent
+ // if string is not parsable parseInt() throws an exception
+ mode = Integer.parseInt(modeStr);
+ isNumber = true;
+ break;
+ }
+ logger.debug("{}: Mode {} mapped to {}", thingId, modeStr, mode);
+ }
+
+ if (mode == -1) {
+ throw new IllegalArgumentException("Invalid Mode selection");
+ }
+
+ // Turn on the unit if currently off
+ if (!isNumber && (device.getIntStatusVal(GREE_PROP_POWER) == 0)) {
+ logger.debug("{}: Send Auto-ON for mode {}", thingId, mode);
+ device.setDevicePower(socket, 1);
+ }
+
+ // Select mode
+ logger.debug("{}: Select mode {}", thingId, mode);
+ device.setDeviceMode(socket, mode);
+
+ // Check for secondary action
+ switch (modeStr) {
+ case MODE_ECO:
+ // Turn on power saving for eco mode
+ logger.debug("{}: Turn on Power-Saving", thingId);
+ device.setDevicePwrSaving(socket, 1);
+ break;
+ }
+ }
+
+ private void handleQuietCommand(DatagramSocket socket, Command command) throws GreeException {
+ int mode = -1;
+ if (command instanceof DecimalType) {
+ mode = ((DecimalType) command).intValue();
+ } else if (command instanceof StringType) {
+ switch (command.toString().toLowerCase()) {
+ case QUIET_OFF:
+ mode = GREE_QUIET_OFF;
+ break;
+ case QUIET_AUTO:
+ mode = GREE_QUIET_AUTO;
+ break;
+ case QUIET_QUIET:
+ mode = GREE_QUIET_QUIET;
+ break;
+ }
+ }
+ if (mode != -1) {
+ device.setQuietMode(socket, mode);
+ } else {
+ throw new IllegalArgumentException("Invalid QuietType");
+ }
+ }
+
+ private int getOnOff(Command command) {
+ if (command instanceof OnOffType) {
+ return command == OnOffType.ON ? 1 : 0;
+ }
+ if (command instanceof DecimalType) {
+ int value = ((DecimalType) command).intValue();
+ if ((value == 0) || (value == 1)) {
+ return value;
+ }
+ }
+ throw new IllegalArgumentException("Invalid OnOffType");
+ }
+
+ private int getNumber(Command command) {
+ if (command instanceof DecimalType) {
+ return ((DecimalType) command).intValue();
+ }
+ throw new IllegalArgumentException("Invalid Number type");
+ }
+
+ private QuantityType> convertTemp(Command command) {
+ if (command instanceof DecimalType) {
+ // The Number alone doesn't specify the temp unit
+ // for this get current setting from the A/C unit
+ int unit = device.getIntStatusVal(GREE_PROP_TEMPUNIT);
+ return toQuantityType((DecimalType) command, DIGITS_TEMP,
+ unit == TEMP_UNIT_CELSIUS ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT);
+ }
+ if (command instanceof QuantityType) {
+ return (QuantityType>) command;
+ }
+ throw new IllegalArgumentException("Invalud Temp type");
+ }
+
+ private void startAutomaticRefresh() {
+ Runnable refresher = () -> {
+ try {
+ // safeguard for multiple REFRESH commands
+ if (isMinimumRefreshTimeExceeded()) {
+ // Get the current status from the Airconditioner
+
+ if (getThing().getStatus() == ThingStatus.OFFLINE) {
+ initializeThing();
+ return;
+ }
+
+ if (clientSocket.isPresent()) {
+ device.getDeviceStatus(clientSocket.get());
+ logger.debug("{}: Executing automatic update of values", thingId);
+ List channels = getThing().getChannels();
+ for (Channel channel : channels) {
+ publishChannel(channel.getUID());
+ }
+ }
+ }
+ } catch (GreeException e) {
+ String subcode = "";
+ if (e.getCause() != null) {
+ subcode = " (" + e.getCause().getMessage() + ")";
+ }
+ String message = messages.get("update.exception", e.getMessage() + subcode);
+ if (getThing().getStatus() == ThingStatus.OFFLINE) {
+ logger.debug("{}: Thing still OFFLINE ({})", thingId, message);
+ } else {
+ if (!e.isTimeout()) {
+ logger.info("{}: {}", thingId, message);
+ } else {
+ logger.debug("{}: {}", thingId, message);
+ }
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
+ }
+ } catch (RuntimeException e) {
+ String message = messages.get("update.exception", "RuntimeException");
+ logger.warn("{}: {}", thingId, message, e);
+ }
+ };
+
+ if (refreshTask == null) {
+ refreshTask = scheduler.scheduleWithFixedDelay(refresher, 0, REFRESH_INTERVAL_SEC, TimeUnit.SECONDS);
+ logger.debug("{}: Automatic refresh started ({} second interval)", thingId, config.refresh);
+ forceRefresh = true;
+ }
+ }
+
+ private boolean isMinimumRefreshTimeExceeded() {
+ long currentTime = Instant.now().toEpochMilli();
+ long timeSinceLastRefresh = currentTime - lastRefreshTime;
+ if (!forceRefresh && (timeSinceLastRefresh < config.refresh * 1000)) {
+ return false;
+ }
+ lastRefreshTime = currentTime;
+ return true;
+ }
+
+ private void publishChannel(ChannelUID channelUID) {
+ String channelID = channelUID.getId();
+ try {
+ State state = null;
+ switch (channelUID.getIdWithoutGroup()) {
+ case POWER_CHANNEL:
+ state = updateOnOff(GREE_PROP_POWER);
+ break;
+ case MODE_CHANNEL:
+ state = updateMode();
+ break;
+ case TURBO_CHANNEL:
+ state = updateOnOff(GREE_PROP_TURBO);
+ break;
+ case LIGHT_CHANNEL:
+ state = updateOnOff(GREE_PROP_LIGHT);
+ break;
+ case TEMP_CHANNEL:
+ state = updateTemp();
+ break;
+ case SWINGUD_CHANNEL:
+ state = updateNumber(GREE_PROP_SWINGUPDOWN);
+ break;
+ case SWINGLR_CHANNEL:
+ state = updateNumber(GREE_PROP_SWINGLEFTRIGHT);
+ break;
+ case WINDSPEED_CHANNEL:
+ state = updateNumber(GREE_PROP_WINDSPEED);
+ break;
+ case QUIET_CHANNEL:
+ state = updateQuiet();
+ break;
+ case AIR_CHANNEL:
+ state = updateOnOff(GREE_PROP_AIR);
+ break;
+ case DRY_CHANNEL:
+ state = updateOnOff(GREE_PROP_DRY);
+ break;
+ case HEALTH_CHANNEL:
+ state = updateOnOff(GREE_PROP_HEALTH);
+ break;
+ case PWRSAV_CHANNEL:
+ state = updateOnOff(GREE_PROP_PWR_SAVING);
+ break;
+ }
+ if (state != null) {
+ logger.debug("{}: Updating channel {} : {}", thingId, channelID, state);
+ updateState(channelID, state);
+ }
+ } catch (GreeException e) {
+ logger.info("{}: {}", thingId, messages.get("channel.exception", channelID, e.getMessage()));
+ } catch (RuntimeException e) {
+ logger.warn("{}: {}", thingId, messages.get("channel.exception", "RuntimeException"), e);
+ }
+ }
+
+ private @Nullable State updateOnOff(final String valueName) throws GreeException {
+ if (device.hasStatusValChanged(valueName)) {
+ return device.getIntStatusVal(valueName) == 1 ? OnOffType.ON : OnOffType.OFF;
+ }
+ return null;
+ }
+
+ private @Nullable State updateNumber(final String valueName) throws GreeException {
+ if (device.hasStatusValChanged(valueName)) {
+ return new DecimalType(device.getIntStatusVal(valueName));
+ }
+ return null;
+ }
+
+ private @Nullable State updateMode() throws GreeException {
+ if (device.hasStatusValChanged(GREE_PROP_MODE)) {
+ int mode = device.getIntStatusVal(GREE_PROP_MODE);
+ String modeStr = "";
+ switch (mode) {
+ case GREE_MODE_AUTO:
+ modeStr = MODE_AUTO;
+ break;
+ case GREE_MODE_COOL:
+ boolean powerSave = device.getIntStatusVal(GREE_PROP_PWR_SAVING) == 1;
+ modeStr = !powerSave ? MODE_COOL : MODE_ECO;
+ break;
+ case GREE_MODE_DRY:
+ modeStr = MODE_DRY;
+ break;
+ case GREE_MODE_FAN:
+ modeStr = MODE_FAN;
+ break;
+ case GREE_MODE_HEAT:
+ modeStr = MODE_HEAT;
+ break;
+ default:
+ modeStr = String.valueOf(mode);
+
+ }
+ if (!modeStr.isEmpty()) {
+ logger.debug("{}: Updading mode channel with {}/{}", thingId, mode, modeStr);
+ return new StringType(modeStr);
+ }
+ }
+ return null;
+ }
+
+ private @Nullable State updateQuiet() throws GreeException {
+ if (device.hasStatusValChanged(GREE_PROP_QUIET)) {
+ switch (device.getIntStatusVal(GREE_PROP_QUIET)) {
+ case GREE_QUIET_OFF:
+ return new StringType(QUIET_OFF);
+ case GREE_QUIET_AUTO:
+ return new StringType(QUIET_AUTO);
+ case GREE_QUIET_QUIET:
+ return new StringType(QUIET_QUIET);
+ }
+ }
+ return null;
+ }
+
+ private @Nullable State updateTemp() throws GreeException {
+ if (device.hasStatusValChanged(GREE_PROP_SETTEMP) || device.hasStatusValChanged(GREE_PROP_TEMPUNIT)) {
+ int unit = device.getIntStatusVal(GREE_PROP_TEMPUNIT);
+ return toQuantityType(device.getIntStatusVal(GREE_PROP_SETTEMP), DIGITS_TEMP,
+ unit == TEMP_UNIT_CELSIUS ? SIUnits.CELSIUS : ImperialUnits.FAHRENHEIT);
+ }
+ return null;
+ }
+
+ private String logInfo(String msgKey, Object... arg) {
+ String message = messages.get(msgKey, arg);
+ logger.info("{}: {}", thingId, message);
+ return message;
+ }
+
+ public static QuantityType> toQuantityType(Number value, int digits, Unit> unit) {
+ BigDecimal bd = new BigDecimal(value.doubleValue());
+ return new QuantityType<>(bd.setScale(digits, BigDecimal.ROUND_HALF_EVEN), unit);
+ }
+
+ private void stopRefrestTask() {
+ forceRefresh = false;
+ if (refreshTask == null) {
+ return;
+ }
+ ScheduledFuture> task = refreshTask;
+ if (task != null) {
+ task.cancel(true);
+ }
+ refreshTask = null;
+ }
+
+ @Override
+ public void dispose() {
+ logger.debug("{}: Thing {} is disposing", thingId, thing.getUID());
+ if (clientSocket.isPresent()) {
+ clientSocket.get().close();
+ clientSocket = Optional.empty();
+ }
+ stopRefrestTask();
+ if (initializeFuture != null) {
+ initializeFuture.cancel(true);
+ }
+ }
+}
diff --git a/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/binding/binding.xml b/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/binding/binding.xml
new file mode 100644
index 0000000000000..b5f8cb1e46eb5
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/binding/binding.xml
@@ -0,0 +1,10 @@
+
+
+
+ GREE Binding
+ This is the binding for GREE air conditioners.
+ Markus Michels
+
+
diff --git a/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/i18n/gree.properties b/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/i18n/gree.properties
new file mode 100644
index 0000000000000..7ec07822b5a85
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/i18n/gree.properties
@@ -0,0 +1,91 @@
+# GREE Binding
+binding.gree.name = GREE Binding
+binding.gree.description = This binding integrates the GREE series of air conditioners
+
+# thing types
+thing-type.gree.airconditioner.label = Air Conditioner
+thing-type.gree.airconditioner.description = A GREE Air Conditioner with WiFi Module
+
+# thing type config description
+thing-type.config.gree.airconditioner.ipAddress.label = IP Address
+thing-type.config.gree.airconditioner.ipAddress.description = IP Address of the GREE unit.
+thing-type.config.gree.airconditioner.broadcastAddress.label = Subnet Broadcast Address
+thing-type.config.gree.airconditioner.broadcastAddress.description = Broadcast IP address of the local subnet.
+thing-type.config.gree.airconditioner.refresh.label = Refresh Interval
+thing-type.config.gree.airconditioner.refresh.description = Interval to query an update from the device.
+
+
+# channel types
+channel-type.gree.power.label = Power
+channel-type.gree.power.description = Turn power on/off
+channel-type.gree.mode.label = Unit Mode
+channel-type.gree.mode.description = Operating mode of the Air Conditioner: auto/cool/eco/fan/dry/turbo or on/off
+channel-type.gree.mode.state.option.auto = Auto
+channel-type.gree.mode.state.option.cool = Cool
+channel-type.gree.mode.state.option.eco = Eco
+channel-type.gree.mode.state.option.dry = Dry
+channel-type.gree.mode.state.option.fan = Fan
+channel-type.gree.mode.state.option.turbo = Turbo
+channel-type.gree.mode.state.option.heat = Heat
+channel-type.gree.mode.state.option.on = ON
+channel-type.gree.mode.state.option.off = OFF
+channel-type.gree.air.label = Air Mode
+channel-type.gree.air.description = Set on/off the Air Conditioner's Air function if applicable to the Air Conditioner model.
+channel-type.gree.dry.label = Dry Mode
+channel-type.gree.dry.description = Set on/off the Air Conditioner's Dry function if applicable to the Air Conditioner model.
+channel-type.gree.turbo.label = Turbo Mode
+channel-type.gree.turbo.description = Set on/off the Air Conditioner's Turbo mode.
+channel-type.gree.temperature.label = Temperature
+channel-type.gree.temperature.description = Sets the desired room temperature.
+channel-type.gree.windspeed.label = Wind Speed
+channel-type.gree.windspeed.description = Sets the fan speed on the Air conditioner: Auto:0, Low:1, MidLow:2, Mid:3, MidHigh:4, High:5. The number of speeds depends on the Air Conditioner model.
+channel-type.gree.windspeed.state.option.0 = Auto
+channel-type.gree.windspeed.state.option.1 = Low
+channel-type.gree.windspeed.state.option.2 = Medium Low
+channel-type.gree.windspeed.state.option.3 = Medium
+channel-type.gree.windspeed.state.option.4 = Medium High
+channel-type.gree.windspeed.state.option.5 = High
+channel-type.gree.mode.label = Unit Mode
+channel-type.gree.mode.description = Operating mode of the Air Conditioner: Auto: 0, Cool: 1, Dry: 2, Fan: 3, Heat: 4
+channel-type.gree.mode.state.option.auto = Auto
+channel-type.gree.swingupdown.label = Vertical Swing Mode
+channel-type.gree.swingupdown.description = Sets the vertical swing action on the Air Conditioner: 0=OFF, 1=Full Swing, 2=Up, 3=Mid-Up 4=Mid, 5=Mid-Down, 6=Down
+channel-type.gree.swingupdown.option.0 = OFF
+channel-type.gree.swingupdown.option.1 = Full Swing
+channel-type.gree.swingupdown.option.2 = Up
+channel-type.gree.swingupdown.option.3 = Mid-Up
+channel-type.gree.swingupdown.option.4 = Mid
+channel-type.gree.swingupdown.option.5 = Mid-Down
+channel-type.gree.swingupdown.option.6 = Down
+channel-type.gree.swingleftright.label = Horizontal Swing Mode
+channel-type.gree.swingleftright.description = Sets the horizontal swing action on the Air Conditioner: 0=OFF, 1=Full Swing, 2=Left, 3=Mid-Left, 4=Mid, 5=Mid-Right, 6=Right
+channel-type.gree.swingleftright.option.0 = OFF
+channel-type.gree.swingleftright.option.1 = Full Swing
+channel-type.gree.swingleftright.option.2 = Left
+channel-type.gree.swingleftright.option.3 = Mid-Left
+channel-type.gree.swingleftright.option.4 = Mid
+channel-type.gree.swingleftright.option.5 = Mid-Right
+channel-type.gree.swingleftright.option.6 = Right
+channel-type.gree.quiet.label = Quiet Mode
+channel-type.gree.quiet.description = Sets the quiet mode, 0=OFF, 1=Auto, 2=Quiet
+channel-type.gree.quiet.state.option.off = OFF
+channel-type.gree.quiet.state.option.auto = Auto
+channel-type.gree.quiet.state.option.quiet = Quiet
+channel-type.gree.powersave.label = Power Save
+channel-type.gree.powersave.description = Set on/off the Air Conditioner's Power Saving function if applicable to the Air Conditioner model.
+channel-type.gree.light.label = Light
+channel-type.gree.light.description = Enable/disable the front display on the Air.
+channel-type.gree.health.label = Health Mode
+channel-type.gree.health.description = Set on/off the Air Conditioner's Health function if applicable to the Air Conditioner model.
+
+# User Messages
+message.thinginit.failed = Unable to connect to air conditioner
+message.thinginit.invconf = Invalid configuration data
+message.thinginit.exception = Thing initialization failed: {0}
+message.command.invarg = Invalid command value {} for channel {}
+message.command.exception = Unable to execute command {0} for channel {1}
+message.update.exception = Unable to perform auto-update: {0}
+message.channel.exception = Unable to update channel {0} with {1}
+message.discovery.result = {0} units discovered.
+message.discovery.newunit = Device {0} discovered at {1}, MAC={2}
+message.discovery.exception = Device Discovery failed: {0}
diff --git a/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/i18n/gree_DE.properties b/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/i18n/gree_DE.properties
new file mode 100644
index 0000000000000..e93203ac2fe45
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/i18n/gree_DE.properties
@@ -0,0 +1,90 @@
+# GREE Binding
+binding.gree.name = GREE Binding
+binding.gree.label = GREE Air Conditioner
+binding.gree.description = Dieses Binding integriert Klimaanlagen der Marke GREE
+
+# thing types
+thing-type.gree.airconditioner.label = GREE Klimaanlage
+thing-type.gree.airconditioner.description = Eine GREE Klimaanlage mit WiFi Modul
+
+# thing type config description
+thing-type.config.gree.airconditioner.ipAddress.label = IP Adresse
+thing-type.config.gree.airconditioner.ipAddress.description = IP Adresse des GREE-Gerätes.
+thing-type.config.gree.airconditioner.broadcastAddress.label = IP Broadcast-Adresse
+thing-type.config.gree.airconditioner.broadcastAddress.description = Broadcast IP Adresse des lokalen Subnetzes.
+thing-type.config.gree.airconditioner.refresh.label = Aktualisierungsintervall
+thing-type.config.gree.airconditioner.refresh.description = Intervall, in dem der Status des Gerätes aktualisiert wird.
+
+
+# channel types
+channel-type.gree.power.label = Betrieb
+channel-type.gree.power.description = Schaltet das Gerät ein/aus.
+channel-type.gree.mode.label = Betriebsmodus
+channel-type.gree.mode.description = Betriebsmodus der Klimaanlage: auto/cool/eco/fan/dry/turbo or on/off
+channel-type.gree.mode.state.option.auto = Auto
+channel-type.gree.mode.state.option.cool = Kühlen
+channel-type.gree.mode.state.option.eco = Eco
+channel-type.gree.mode.state.option.dry = Trocknen
+channel-type.gree.mode.state.option.fan = Ventilator
+channel-type.gree.mode.state.option.turbo = Turbo
+channel-type.gree.mode.state.option.heat = Heizen
+channel-type.gree.mode.state.option.on = Ein
+channel-type.gree.mode.state.option.off = Aus
+channel-type.gree.air.label = L¸ftung
+channel-type.gree.air.description = Schaltet das Gerät in den L¸ftermodus (keine Kühlung). Verfügbarkeit ist abhängig vom Gerätemodell.
+channel-type.gree.dry.label = Trocknen
+channel-type.gree.dry.description = Schaltet den Trocknungsmodus ein/aus. Verfügbarkeit ist abhängig vom Gerätemodell.
+channel-type.gree.turbo.label = Turbo
+channel-type.gree.turbo.description = Schaltet den Turbomodus ein/aus. Verfügbarkeit ist abhängig vom Gerätemodell.
+channel-type.gree.temperature.label = Temperatur
+channel-type.gree.temperature.description = Setzt die Zieltemperatur.
+channel-type.gree.windspeed.label = Lüftergeschwindigkeit
+channel-type.gree.windspeed.description = Geschwindigkeit der Ventilation: 0:Auto, 1=niedrig, 2: langsam, 3: mittel, 4: schneller, 5: hoch. Verfügbarkeit der Geschwindigkeitsstufen ist abhängig vom Gerätemodell.
+channel-type.gree.windspeed.state.option.0 = Auto
+channel-type.gree.windspeed.state.option.1 = Niedrig
+channel-type.gree.windspeed.state.option.2 = Mittel
+channel-type.gree.windspeed.state.option.3 = Schnell
+channel-type.gree.windspeed.state.option.4 = Stark
+channel-type.gree.windspeed.state.option.5 = Max
+channel-type.gree.quiet.label = Leisemodus
+channel-type.gree.quiet.description = Leisemodus wählen: 0=Aus, 1=Auto, 2=Leise
+channel-type.gree.quiet.state.option.off = Aus
+channel-type.gree.quiet.state.option.auto = Auto
+channel-type.gree.quiet.state.option.quiet = Leise
+channel-type.gree.swingupdown.label = Lamellenmodus
+channel-type.gree.swingupdown.description = Auswahl des Lamellenmodus: 0=Aus, 1=Voller Flügel, 2=Hoch, 3=Mittelhoch, 3=Mitte, 5=Mitteltief, 6=Tief. Verfügbarkeit ist abhängig vom Gerätemodell.
+channel-type.gree.swingupdown.option.0 = Aus
+channel-type.gree.swingupdown.option.1 = Voller Flügel
+channel-type.gree.swingupdown.option.2 = Hoch
+channel-type.gree.swingupdown.option.3 = Mittelhoch
+channel-type.gree.swingupdown.option.4 = Mitte
+channel-type.gree.swingupdown.option.5 = Mitteltief
+channel-type.gree.swingupdown.option.6 = Tief
+channel-type.gree.swingleftright.label = Lamellenmodus
+channel-type.gree.swingleftright.description = Auswahl des Lamellenmodus: 0=Aus, 1=Voller Flügel, 2=Links, 3=Mitte-Links, 4=Mitte, 5=Mitte-Rechts, 6=Rechts. Verfügbarkeit ist abhängig vom Gerätemodell.
+channel-type.gree.swingleftright.option.0 = AUS
+channel-type.gree.swingleftright.option.1 = Voller Flügel
+channel-type.gree.swingleftright.option.2 = Links
+channel-type.gree.swingleftright.option.3 = Mitte-Links
+channel-type.gree.swingleftright.option.4 = Mitte
+channel-type.gree.swingleftright.option.5 = Mitte-Rechts
+channel-type.gree.swingleftright.option.6 = Rechts
+channel-type.gree.powersave.label = Energiesparen
+channel-type.gree.powersave.description = Aktivierung der Energiesparfunktion. Verfügbarkeit ist abhängig vom Gerätemodell.
+channel-type.gree.light.label = Kontrollleuchte
+channel-type.gree.light.description = Die Beleuchtung an der Frontseite ein/ausschalten.
+channel-type.gree.health.label = Betriebsbereitschaft
+channel-type.gree.health.description = Zeigt die Betriebsbeschreitschaft des Gerätes an.
+
+# User Messages
+message.thinginit.failed = Klimaanlage nicht erreichbar
+message.thinginit.invconf = Ungültiger Thing-Konfigurationswert
+message.thinginit.exception = Initialisierung fehlgeschlagen: {0}
+message.command.invarg = Ungültiger Befehlswert {0} für Channel {1}
+message.command.exception = Befehl {0} für Channel {1} kann nichts ausgeführt werden
+message.update.exception = Status-Update fehlgeschlagen: {0}
+message.channel.exception = Aktualisierung des Channels {0} mit dem Wert {1} ist fehlgeschlagen
+message.discovery.result = {0} Geräte gefunden.
+message.discovery.newunit = Gerät {0} wurde mit IP-Adresse {1} erkannt (MAC={2})
+message.discovery.exception =Geräteerkennung fehlgeschlagen: {0}
+
diff --git a/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/thing/thing-types.xml
new file mode 100644
index 0000000000000..1aa75875aad71
--- /dev/null
+++ b/bundles/org.openhab.binding.gree/src/main/resources/ESH-INF/thing/thing-types.xml
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ network-address
+
+
+ network-address
+
+
+ 60
+ seconds
+ true
+
+
+
+
+
+
+ String
+
+ @text/channel-type.gree.mode.description
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number:Temperature
+
+ @text/channel-type.gree.temperature.description
+
+
+
+ Switch
+
+ @text/channel-type.gree.air.description
+
+
+ Switch
+
+ @text/channel-type.gree.dry.description
+
+
+ Switch
+
+ @text/channel-type.gree.turbo.description
+
+
+ Number
+
+ @text/channel-type.gree.windspeed.description
+
+
+
+
+
+
+
+
+
+
+
+
+ String
+
+ @text/channel-type.gree.quiet.description
+
+
+
+
+
+
+
+
+
+ Number
+
+ @text/channel-type.gree.swingupdown.description
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number
+
+ @text/channel-type.gree.swingleftright.description
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Switch
+
+ @text/channel-type.gree.powersave.description
+
+
+ Switch
+
+ @text/channel-type.gree.light.description
+
+
+ Switch
+
+ @text/channel-type.gree.health.description
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index e8a0c60f11ed8..37e06979f4df3 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -103,6 +103,7 @@
org.openhab.binding.goechargerorg.openhab.binding.globalcacheorg.openhab.binding.gpstracker
+ org.openhab.binding.greeorg.openhab.binding.groheondusorg.openhab.binding.harmonyhuborg.openhab.binding.hdanywhere