added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.gree-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-gree" description="Gree Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.gree/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* 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.openhab.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<ThingTypeUID> 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 TARGET_TEMP_CHANNEL = "temperature";
|
||||
public static final String CURRENT_TEMP_CHANNEL = "currentTemperature";
|
||||
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";
|
||||
public static final String GREE_PROP_CURRENT_TEMP_SENSOR = "TemSen";
|
||||
|
||||
// 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 MAX_API_RETRIES = 3;
|
||||
|
||||
public static final int DIGITS_TEMP = 1;
|
||||
|
||||
/**
|
||||
* The internal offset for the temperature sensor which is set to a constant of -40 degrees Celsius. GREE
|
||||
* airconditioners usually return a value from the temperature sensor which is offset by +40 degrees Celsius. The
|
||||
* temperature value shown on the device LCD display should match the value shown by this binding when the config
|
||||
* parameter currentTemperatureOffset is set to 0.
|
||||
*
|
||||
* @See https://github.com/tomikaa87/gree-remote#getting-the-current-temperature-reading-from-the-internal-sensor
|
||||
* for more details.
|
||||
*/
|
||||
public static final double INTERNAL_TEMP_SENSOR_OFFSET = -40.0;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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.math.BigDecimal;
|
||||
|
||||
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;
|
||||
/**
|
||||
* The currentTemperatureOffset is configureable in case the user wants to offset this temperature for calibration
|
||||
* of the temperature sensor.
|
||||
*/
|
||||
public BigDecimal currentTemperatureOffset = new BigDecimal(0.0);
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Config: ipAddress=" + ipAddress + ", broadcastAddress=" + broadcastAddress + ", refresh=" + refresh
|
||||
+ ", currentTemperatureOffset=" + currentTemperatureOffset;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.openhab.binding.gree.internal.discovery.GreeDeviceFinder;
|
||||
import org.openhab.binding.gree.internal.handler.GreeHandler;
|
||||
import org.openhab.core.net.NetworkAddressService;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
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<String, Object> 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;
|
||||
}
|
||||
}
|
||||
@@ -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.openhab.core.i18n.LocaleProvider;
|
||||
import org.openhab.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/**
|
||||
* 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<String, GreeAirDevice> 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> T fromJson(DatagramPacket packet, Class<T> 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<String, GreeAirDevice> 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();
|
||||
}
|
||||
}
|
||||
@@ -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.openhab.binding.gree.internal.GreeException;
|
||||
import org.openhab.binding.gree.internal.GreeTranslationProvider;
|
||||
import org.openhab.binding.gree.internal.handler.GreeAirDevice;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.net.NetworkAddressService;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
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<String, @Nullable Object> 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<String, @Nullable Object> 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<String, GreeAirDevice> deviceList) {
|
||||
for (GreeAirDevice device : deviceList.values()) {
|
||||
String ipAddress = device.getAddress().getHostAddress();
|
||||
logger.debug("{}", messages.get("discovery.newunit", device.getName(), ipAddress, device.getId()));
|
||||
Map<String, Object> 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,544 @@
|
||||
/**
|
||||
* 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.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.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
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<GreeScanResponseDTO> scanResponseGson = Optional.empty();
|
||||
private Optional<GreeStatusResponseDTO> statusResponseGson = Optional.empty();
|
||||
private Optional<GreeStatusResponsePackDTO> 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<String> 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);
|
||||
columns.add(GREE_PROP_CURRENT_TEMP_SENSOR);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* SwUpDn: controls the swing mode of the vertical air blades
|
||||
*
|
||||
* 0: default
|
||||
* 1: swing in full range
|
||||
* 2: fixed in the upmost position (1/5)
|
||||
* 3: fixed in the middle-up position (2/5)
|
||||
* 4: fixed in the middle position (3/5)
|
||||
* 5: fixed in the middle-low position (4/5)
|
||||
* 6: fixed in the lowest position (5/5)
|
||||
* 7: swing in the downmost region (5/5)
|
||||
* 8: swing in the middle-low region (4/5)
|
||||
* 9: swing in the middle region (3/5)
|
||||
* 10: swing in the middle-up region (2/5)
|
||||
* 11: swing in the upmost region (1/5)
|
||||
*/
|
||||
public void setDeviceSwingUpDown(DatagramSocket clientSocket, int value) throws GreeException {
|
||||
if (value < 0 || value > 11) {
|
||||
throw new GreeException("SwingUpDown value is out of range!");
|
||||
}
|
||||
setCommandValue(clientSocket, GREE_PROP_SWINGUPDOWN, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* SwingLfRig: controls the swing mode of the horizontal air blades (available on limited number of devices, e.g.
|
||||
* some Cooper & Hunter units - thanks to mvmn)
|
||||
*
|
||||
* 0: default
|
||||
* 1: full swing
|
||||
* 2-6: fixed position from leftmost to rightmost
|
||||
* Full swing, like for SwUpDn is not supported
|
||||
*/
|
||||
public void setDeviceSwingLeftRight(DatagramSocket clientSocket, int value) throws GreeException {
|
||||
if (value < 0 || value > 6) {
|
||||
throw new GreeException("SwingLeftRight value is out of range!");
|
||||
}
|
||||
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<String, Integer> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tur: sets fan speed to the maximum. Fan speed cannot be changed while active and only available in Dry and Cool
|
||||
* mode.
|
||||
*
|
||||
* 0: off
|
||||
* 1: on
|
||||
*/
|
||||
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 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<String, Integer> 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<String, Integer> 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<String> colList = Arrays.asList(statusResponseGson.get().packJson.cols);
|
||||
List<Integer> 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() && (statusResponseGson.get().packJson.cols != null)
|
||||
&& (statusResponseGson.get().packJson.dat != null);
|
||||
}
|
||||
|
||||
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<String> currcolList = Arrays.asList(statusResponseGson.get().packJson.cols);
|
||||
List<Integer> 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<String> prevcolList = Arrays.asList(prevStatusResponsePackGson.get().cols);
|
||||
List<Integer> 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<String, Integer> 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> T receiveResponse(DatagramSocket clientSocket, Class<T> 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<String> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,597 @@
|
||||
/**
|
||||
* 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.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.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
import org.openhab.core.library.unit.ImperialUnits;
|
||||
import org.openhab.core.library.unit.SIUnits;
|
||||
import org.openhab.core.thing.Channel;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
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<DatagramSocket> clientSocket = Optional.empty();
|
||||
private boolean forceRefresh = false;
|
||||
|
||||
private @Nullable ScheduledFuture<?> refreshTask;
|
||||
private @Nullable Future<?> initializeFuture;
|
||||
private long lastRefreshTime = 0;
|
||||
private long apiRetries = 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);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
if (getThing().getStatus() != ThingStatus.OFFLINE) {
|
||||
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());
|
||||
|
||||
int retries = MAX_API_RETRIES;
|
||||
do {
|
||||
try {
|
||||
sendRequest(channelId, command);
|
||||
// force refresh on next status refresh cycle
|
||||
forceRefresh = true;
|
||||
apiRetries = 0;
|
||||
return; // successful
|
||||
} catch (GreeException e) {
|
||||
retries--;
|
||||
if (retries > 0) {
|
||||
logger.debug("{}: Command {} failed for channel {}, retry", thingId, command, channelId);
|
||||
} else {
|
||||
String message = logInfo(
|
||||
messages.get("command.exception", command, channelId) + ": " + e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
logInfo("command.invarg", command, channelId);
|
||||
retries = 0;
|
||||
} catch (RuntimeException e) {
|
||||
logger.warn("{}: {}", thingId, messages.get("command.exception", command, channelId), e);
|
||||
retries = 0;
|
||||
}
|
||||
} while (retries > 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendRequest(String channelId, Command command) throws GreeException {
|
||||
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 TARGET_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;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
// try to re-initialize thing access
|
||||
logger.debug("{}: Re-initialize device", thingId);
|
||||
initializeThing();
|
||||
return;
|
||||
}
|
||||
|
||||
if (clientSocket.isPresent()) {
|
||||
device.getDeviceStatus(clientSocket.get());
|
||||
apiRetries = 0; // the call was successful without an exception
|
||||
logger.debug("{}: Executing automatic update of values", thingId);
|
||||
List<Channel> 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);
|
||||
}
|
||||
|
||||
apiRetries++;
|
||||
if (apiRetries > MAX_API_RETRIES) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
|
||||
apiRetries = 0;
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
String message = messages.get("update.exception", "RuntimeException");
|
||||
logger.warn("{}: {}", thingId, message, e);
|
||||
apiRetries++;
|
||||
}
|
||||
};
|
||||
|
||||
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 TARGET_TEMP_CHANNEL:
|
||||
state = updateTargetTemp();
|
||||
break;
|
||||
case CURRENT_TEMP_CHANNEL:
|
||||
state = updateCurrentTemp();
|
||||
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 updateTargetTemp() 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 @Nullable State updateCurrentTemp() throws GreeException {
|
||||
if (device.hasStatusValChanged(GREE_PROP_CURRENT_TEMP_SENSOR)) {
|
||||
double temp = device.getIntStatusVal(GREE_PROP_CURRENT_TEMP_SENSOR);
|
||||
return temp != 0
|
||||
? new DecimalType(
|
||||
temp + INTERNAL_TEMP_SENSOR_OFFSET + config.currentTemperatureOffset.doubleValue())
|
||||
: UnDefType.UNDEF;
|
||||
}
|
||||
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 stopRefreshTask() {
|
||||
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();
|
||||
}
|
||||
stopRefreshTask();
|
||||
if (initializeFuture != null) {
|
||||
initializeFuture.cancel(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="gree" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>GREE Binding</name>
|
||||
<description>This is the binding for GREE air conditioners.</description>
|
||||
<author>Markus Michels</author>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,97 @@
|
||||
# GREE Binding
|
||||
binding.gree.name = GREE Binding
|
||||
binding.gree.label = GREE Air Conditioner
|
||||
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.
|
||||
thing-type.config.gree.airconditioner.currentTemperatureOffset.label = Offset for Current Temperature
|
||||
thing-type.config.gree.airconditioner.currentTemperatureOffset.description = The offset in Celsius for the current temperature value received 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.targettemperature.label = Target Temperature
|
||||
channel-type.gree.targettemperature.description = Sets the desired room temperature.
|
||||
channel-type.gree.currenttemperature.label = Current Temperature
|
||||
channel-type.gree.currenttemperature.description = Displays the current 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.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.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.swingupdown.option.7 = Swing Downmost
|
||||
channel-type.gree.swingupdown.option.8 = Swing Middle-Low
|
||||
channel-type.gree.swingupdown.option.9 = Swing Middle
|
||||
channel-type.gree.swingupdown.option.10 = Swing Middle-Up
|
||||
channel-type.gree.swingupdown.option.11 = Swing Upmost
|
||||
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.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}
|
||||
@@ -0,0 +1,97 @@
|
||||
# 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.
|
||||
thing-type.config.gree.airconditioner.currentTemperatureOffset.label = Offset zum Ablesen des Temperatursensors
|
||||
thing-type.config.gree.airconditioner.currentTemperatureOffset.description = Der Offset in Celsius für den aktuellen Temperaturwert, der vom Gerät empfangen 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.targettemperature.label = Zieltemperatur
|
||||
channel-type.gree.targettemperature.description = Setzt die Zieltemperatur.
|
||||
channel-type.gree.currenttemperature.label = Aktuelle Temperatur
|
||||
channel-type.gree.currenttemperature.description = Zeigt die aktuelle Temperatur an.
|
||||
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 = V-Lamellenmodus
|
||||
channel-type.gree.swingupdown.description = Auswahl des vertikalen 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.swingupdown.option.7 = Unterre Region
|
||||
channel-type.gree.swingupdown.option.8 = Mittlere Region
|
||||
channel-type.gree.swingupdown.option.9 = Region Mitte-unten
|
||||
channel-type.gree.swingupdown.option.10 = Region Mitte-oben
|
||||
channel-type.gree.swingupdown.option.11 = Obere Region
|
||||
channel-type.gree.swingleftright.label = H-Lamellenmodus
|
||||
channel-type.gree.swingleftright.description = Auswahl des horizontalen 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}
|
||||
@@ -0,0 +1,166 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="gree"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<thing-type id="airconditioner">
|
||||
<label>@text/thing-type.gree.airconditioner.label</label>
|
||||
<channels>
|
||||
<channel id="power" typeId="system.power"/>
|
||||
<channel id="mode" typeId="mode"/>
|
||||
<channel id="temperature" typeId="targetTemperature"/>
|
||||
<channel id="currentTemperature" typeId="currentTemperature"/>
|
||||
<channel id="air" typeId="air"/>
|
||||
<channel id="dry" typeId="dry"/>
|
||||
<channel id="turbo" typeId="turbo"/>
|
||||
<channel id="windspeed" typeId="windspeed"/>
|
||||
<channel id="quiet" typeId="quiet"/>
|
||||
<channel id="swingUpDown" typeId="swingUpDown"/>
|
||||
<channel id="swingLeftRight" typeId="swingLeftRight"/>
|
||||
<channel id="powersave" typeId="powersave"/>
|
||||
<channel id="light" typeId="light"/>
|
||||
<channel id="health" typeId="health"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="ipAddress" type="text" required="true">
|
||||
<context>network-address</context>
|
||||
</parameter>
|
||||
<parameter name="broadcastAddress" type="text" required="false">
|
||||
<context>network-address</context>
|
||||
</parameter>
|
||||
<parameter name="refresh" type="integer" required="true" unit="s">
|
||||
<default>60</default>
|
||||
<unitLabel>seconds</unitLabel>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="currentTemperatureOffset" type="decimal" step="0.5" required="true" unit="C">
|
||||
<default>0</default>
|
||||
<unitLabel>Degrees Celsius</unitLabel>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="mode">
|
||||
<item-type>String</item-type>
|
||||
<label>@text/channel-type.gree.mode.label</label>
|
||||
<description>@text/channel-type.gree.mode.description</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="auto">@text/channel-type.gree.mode.state.option.auto</option>
|
||||
<option value="cool">@text/channel-type.gree.mode.state.option.cool</option>
|
||||
<option value="eco">@text/channel-type.gree.mode.state.option.eco</option>
|
||||
<option value="dry">@text/channel-type.gree.mode.state.option.dry</option>
|
||||
<option value="fan">@text/channel-type.gree.mode.state.option.fan</option>
|
||||
<option value="heat">@text/channel-type.gree.mode.state.option.heat</option>
|
||||
<option value="on">@text/channel-type.gree.mode.state.option.on</option>
|
||||
<option value="off">@text/channel-type.gree.mode.state.option.off</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="targetTemperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>@text/channel-type.gree.targettemperature.label</label>
|
||||
<description>@text/channel-type.gree.targettemperature.description</description>
|
||||
<state min="16" max="86" step="1" pattern="%d %unit%"></state>
|
||||
</channel-type>
|
||||
<channel-type id="currentTemperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>@text/channel-type.gree.currenttemperature.label</label>
|
||||
<description>@text/channel-type.gree.currenttemperature.description</description>
|
||||
<state readOnly="true" pattern="%d %unit%"></state>
|
||||
</channel-type>
|
||||
<channel-type id="air">
|
||||
<item-type>Switch</item-type>
|
||||
<label>@text/channel-type.gree.air.label</label>
|
||||
<description>@text/channel-type.gree.air.description</description>
|
||||
</channel-type>
|
||||
<channel-type id="dry">
|
||||
<item-type>Switch</item-type>
|
||||
<label>@text/channel-type.gree.dry.label</label>
|
||||
<description>@text/channel-type.gree.dry.description</description>
|
||||
</channel-type>
|
||||
<channel-type id="turbo">
|
||||
<item-type>Switch</item-type>
|
||||
<label>@text/channel-type.gree.turbo.label</label>
|
||||
<description>@text/channel-type.gree.turbo.description</description>
|
||||
</channel-type>
|
||||
<channel-type id="windspeed">
|
||||
<item-type>Number</item-type>
|
||||
<label>@text/channel-type.gree.windspeed.label</label>
|
||||
<description>@text/channel-type.gree.windspeed.description</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="0">@text/channel-type.gree.windspeed.state.option.0</option>
|
||||
<option value="1">@text/channel-type.gree.windspeed.state.option.1</option>
|
||||
<option value="2">@text/channel-type.gree.windspeed.state.option.2</option>
|
||||
<option value="3">@text/channel-type.gree.windspeed.state.option.3</option>
|
||||
<option value="4">@text/channel-type.gree.windspeed.state.option.4</option>
|
||||
<option value="5">@text/channel-type.gree.windspeed.state.option.5</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="quiet">
|
||||
<item-type>String</item-type>
|
||||
<label>@text/channel-type.gree.quiet.label</label>
|
||||
<description>@text/channel-type.gree.quiet.description</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="off">@text/channel-type.gree.quiet.state.option.off</option>
|
||||
<option value="auto">@text/channel-type.gree.quiet.state.option.auto</option>
|
||||
<option value="quiet">@text/channel-type.gree.quiet.state.option.quiet</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="swingUpDown">
|
||||
<item-type>Number</item-type>
|
||||
<label>@text/channel-type.gree.swingupdown.label</label>
|
||||
<description>@text/channel-type.gree.swingupdown.description</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="0">@text/channel-type.gree.swingupdown.option.0</option>
|
||||
<option value="1">@text/channel-type.gree.swingupdown.option.1</option>
|
||||
<option value="2">@text/channel-type.gree.swingupdown.option.2</option>
|
||||
<option value="3">@text/channel-type.gree.swingupdown.option.3</option>
|
||||
<option value="4">@text/channel-type.gree.swingupdown.option.4</option>
|
||||
<option value="5">@text/channel-type.gree.swingupdown.option.5</option>
|
||||
<option value="6">@text/channel-type.gree.swingupdown.option.6</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="swingLeftRight">
|
||||
<item-type>Number</item-type>
|
||||
<label>@text/channel-type.gree.swingleftright.label</label>
|
||||
<description>@text/channel-type.gree.swingleftright.description</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="0">@text/channel-type.gree.swingleftright.option.0</option>
|
||||
<option value="1">@text/channel-type.gree.swingleftright.option.1</option>
|
||||
<option value="2">@text/channel-type.gree.swingleftright.option.2</option>
|
||||
<option value="3">@text/channel-type.gree.swingleftright.option.3</option>
|
||||
<option value="4">@text/channel-type.gree.swingleftright.option.4</option>
|
||||
<option value="5">@text/channel-type.gree.swingleftright.option.5</option>
|
||||
<option value="6">@text/channel-type.gree.swingleftright.option.6</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="powersave">
|
||||
<item-type>Switch</item-type>
|
||||
<label>@text/channel-type.gree.powersave.label</label>
|
||||
<description>@text/channel-type.gree.powersave.description</description>
|
||||
</channel-type>
|
||||
<channel-type id="light">
|
||||
<item-type>Switch</item-type>
|
||||
<label>@text/channel-type.gree.light.label</label>
|
||||
<description>@text/channel-type.gree.light.description</description>
|
||||
</channel-type>
|
||||
<channel-type id="health">
|
||||
<item-type>Switch</item-type>
|
||||
<label>@text/channel-type.gree.health.label</label>
|
||||
<description>@text/channel-type.gree.health.description</description>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
Reference in New Issue
Block a user