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.bundles org.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.goecharger org.openhab.binding.globalcache org.openhab.binding.gpstracker + org.openhab.binding.gree org.openhab.binding.groheondus org.openhab.binding.harmonyhub org.openhab.binding.hdanywhere