## Thing Configuration
-| Channel Name | Type | Description |
-|--------------------------|------------|-----------------------------------------------------------------------------------------------|
-| ipAddress | IP Address | IP address of the unit. |
-| broadcastAddress | IP Address | Broadcast address being used for discovery, usually derived from the IP interface address. |
-| refresh | Integer | Refresh interval in seconds for polling the device status. |
-| currentTemperatureOffset | Decimal | Offset in Celsius for the current temperature value received from the device. |
+| Channel Name | Type | Description |
+|--------------------------|-----------------|-----------------------------------------------------------------------------------------------|
+| ipAddress | IP Address | IP address of the unit. |
+| broadcastAddress | IP Address | Broadcast address being used for discovery, usually derived from the IP interface address. |
+| refresh | Integer | Refresh interval in seconds for polling the device status. |
+| currentTemperatureOffset | Decimal | Offset in Celsius for the current temperature value received from the device. |
+| encryptionType | EncryptionTypes | Encryption type (ECB or GCM) used for communicating with the AC device |
The Air Conditioner's IP address is mandatory, all other parameters are optional.
If the broadcast is not set (default) it will be derived from openHAB's network setting (Check Network Settings in the openHAB UI).
+The binding tries to automatically detect the encryption type when communicating with the AC.
+If this fails, you might need need to set the encryption type manually.
Only change this if you have a good reason to.
## Channels
### Things
```java
-Thing gree:airconditioner:a1234561 [ ipAddress="192.168.1.111", refresh=2 ]
+Thing gree:airconditioner:a1234561 [ ipAddress="192.168.1.111", refresh=2, encryptionType="ECB" ]
```
### Items
*/
package org.openhab.binding.gree.internal;
+import java.util.Map;
import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
public static final String PROPERTY_IP = "ipAddress";
public static final String PROPERTY_BROADCAST = "broadcastAddress";
+ public static final String PROPERTY_ENCRYPTION_TYPE = "encryptionType";
+
// List of all Channel ids
public static final String POWER_CHANNEL = "power";
public static final String MODE_CHANNEL = "mode";
* for more details.
*/
public static final double INTERNAL_TEMP_SENSOR_OFFSET = -40.0;
+
+ public enum EncryptionTypes {
+ UNKNOWN,
+ ECB,
+ GCM;
+
+ private static final Map<String, EncryptionTypes> MAP = Stream.of(EncryptionTypes.values())
+ .collect(Collectors.toMap(Enum::name, Function.identity()));
+
+ public static EncryptionTypes of(final String name) {
+ return MAP.getOrDefault(name, UNKNOWN);
+ }
+ };
}
*/
package org.openhab.binding.gree.internal;
+import static org.openhab.binding.gree.internal.GreeBindingConstants.*;
+
import java.math.BigDecimal;
import org.eclipse.jdt.annotation.NonNullByDefault;
* of the temperature sensor.
*/
public BigDecimal currentTemperatureOffset = new BigDecimal(0.0);
+ public EncryptionTypes encryptionType = EncryptionTypes.UNKNOWN;
@Override
public String toString() {
return "Config: ipAddress=" + ipAddress + ", broadcastAddress=" + broadcastAddress + ", refresh=" + refresh
- + ", currentTemperatureOffset=" + currentTemperatureOffset;
+ + ", currentTemperatureOffset=" + currentTemperatureOffset + ", encryptionType=" + encryptionType;
}
}
*/
package org.openhab.binding.gree.internal;
+import static org.openhab.binding.gree.internal.GreeBindingConstants.EncryptionTypes;
+
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
private static final String GCM_ADD = "qualcomm-test";
private static final int TAG_LENGTH = 16;
- public enum EncryptionTypes {
- ECB,
- GCM
- };
-
public static byte[] getAESGeneralKeyByteArray() {
return AES_KEY.getBytes(StandardCharsets.UTF_8);
}
}
public static <T extends GreeBaseDTO> String decrypt(T response, EncryptionTypes encType) throws GreeException {
+ if (encType == EncryptionTypes.UNKNOWN) {
+ encType = getEncryptionType(response);
+ }
+
if (encType == EncryptionTypes.GCM) {
return decrypt(getGCMGeneralKeyByteArray(), response, encType);
} else {
public static <T extends GreeBaseDTO> String decrypt(byte[] keyarray, T response, EncryptionTypes encType)
throws GreeException {
+ if (encType == EncryptionTypes.UNKNOWN) {
+ encType = getEncryptionType(response);
+ }
+
if (encType == EncryptionTypes.GCM) {
return decryptGCMPack(keyarray, response.pack, response.tag);
} else {
private Class<?> getCauseClass() {
Throwable cause = getCause();
- if (getCause() != null) {
+ if (cause != null) {
return cause.getClass();
}
return GreeException.class;
public GreeDeviceFinder() {
}
- public void scan(DatagramSocket clientSocket, String broadcastAddress, boolean scanNetwork) throws GreeException {
+ public void scan(DatagramSocket clientSocket, String broadcastAddress, boolean scanNetwork,
+ EncryptionTypes encryptionTypeConfig) throws GreeException {
InetAddress ipAddress;
try {
ipAddress = InetAddress.getByName(broadcastAddress);
}
// Decrypt message - a GreeException is thrown when something went wrong
- String decryptedMsg = scanResponseGson.decryptedPack = GreeCryptoUtil.decrypt(scanResponseGson);
+ String decryptedMsg = scanResponseGson.decryptedPack = GreeCryptoUtil.decrypt(scanResponseGson,
+ encryptionTypeConfig);
logger.debug("Response received from address {}: {}", remoteAddress.getHostAddress(), decryptedMsg);
@Override
protected void startScan() {
try (DatagramSocket clientSocket = new DatagramSocket()) {
- deviceFinder.scan(clientSocket, broadcastAddress, true);
+ deviceFinder.scan(clientSocket, broadcastAddress, true, EncryptionTypes.UNKNOWN);
int count = deviceFinder.getScannedDeviceCount();
logger.debug("{}", messages.get("discovery.result", count));
properties.put(Thing.PROPERTY_MAC_ADDRESS, device.getId());
properties.put(PROPERTY_IP, ipAddress);
properties.put(PROPERTY_BROADCAST, broadcastAddress);
+ properties.put(PROPERTY_ENCRYPTION_TYPE, device.getEncryptionType());
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();
private final InetAddress ipAddress;
private int port = 0;
private String encKey = "";
- private GreeCryptoUtil.EncryptionTypes encType = GreeCryptoUtil.EncryptionTypes.ECB;
+ private EncryptionTypes encType = EncryptionTypes.UNKNOWN;
private Optional<GreeScanResponseDTO> scanResponseGson = Optional.empty();
private Optional<GreeStatusResponseDTO> statusResponseGson = Optional.empty();
private Optional<GreeStatusResponsePackDTO> prevStatusResponsePackGson = Optional.empty();
}
}
- public void bindWithDevice(DatagramSocket clientSocket) throws GreeException {
+ public void bindWithDevice(DatagramSocket clientSocket, EncryptionTypes encryptionTypeConfig) throws GreeException {
try {
// Prep the Binding Request pack
GreeBindRequestPackDTO bindReqPackGson = new GreeBindRequestPackDTO();
String bindReqPackStr = GSON.toJson(bindReqPackGson);
// Encrypt and send the Binding Request pack
+ setEncryptionType(encryptionTypeConfig);
String[] encryptedBindReqData = GreeCryptoUtil.encrypt(GreeCryptoUtil.getGeneralKeyByteArray(encType),
bindReqPackStr, encType);
DatagramPacket sendPacket = createPackRequest(1, encryptedBindReqData);
// save the outcome
isBound = true;
} catch (IOException | JsonSyntaxException e) {
- throw new GreeException("Unable to bind to device", e);
+ if (encType != EncryptionTypes.GCM) {
+ logger.debug("Unable to bind to device - changing the encryption mode to GCM and trying again", e);
+ bindWithDevice(clientSocket, EncryptionTypes.GCM);
+ } else {
+ throw new GreeException("Unable to bind to device", e);
+ }
}
}
request.uid = 0;
request.tcid = getId();
request.pack = data[0];
- if (encType == GreeCryptoUtil.EncryptionTypes.GCM) {
+ if (encType == EncryptionTypes.GCM) {
if (data.length > 1) {
request.tag = data[1];
} else {
return isBound;
}
+ public void setEncryptionType(EncryptionTypes value) {
+ if (value == EncryptionTypes.UNKNOWN) {
+ logger.debug("Trying to set encryption type to 'UNKNOWN' for device: {}, current value: {}", getName(),
+ encType);
+ if (encType == EncryptionTypes.UNKNOWN) {
+ logger.debug("Falling back to 'ECB' for device: {}", getName());
+ encType = EncryptionTypes.ECB;
+ }
+ } else {
+ logger.debug("Change encryption type for device: {}, from : {}, to: {}", getName(), encType, value);
+ encType = value;
+ }
+ }
+
+ public EncryptionTypes getEncryptionType() {
+ return encType;
+ }
+
public byte[] getKey() {
return encKey.getBytes(StandardCharsets.UTF_8);
}
}
public String getName() {
- return scanResponseGson.isPresent() ? scanResponseGson.get().packJson.name : "";
+ if (scanResponseGson.isPresent()) {
+ String name = scanResponseGson.get().packJson.name;
+ return name.trim().isEmpty() ? getId() : name;
+ }
+
+ return "";
}
public String getVendor() {
clientSocket.get().setSoTimeout(DATAGRAM_SOCKET_TIMEOUT);
}
// Find the GREE device
- deviceFinder.scan(clientSocket.get(), config.ipAddress, false);
+ deviceFinder.scan(clientSocket.get(), config.ipAddress, false, config.encryptionType);
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());
+ device.bindWithDevice(clientSocket.get(), config.encryptionType);
if (device.getIsBound()) {
updateStatus(ThingStatus.ONLINE);
return;
@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.
+ initializeThing();
} else {
logger.debug("{}: Issue command {} to channe {}", thingId, command, channelUID.getIdWithoutGroup());
String channelId = channelUID.getIdWithoutGroup();
}
} catch (GreeException e) {
String subcode = "";
- if (e.getCause() != null) {
- subcode = " (" + e.getCause().getMessage() + ")";
+ Throwable cause = e.getCause();
+ if (cause != null) {
+ subcode = " (" + cause.getMessage() + ")";
}
String message = messages.get("update.exception", e.getMessageString() + subcode);
if (getThing().getStatus() == ThingStatus.OFFLINE) {
-# GREE Binding
+# add-on
+
addon.gree.name = GREE Binding
-addon.gree.description = This binding integrates the GREE series of air conditioners
+addon.gree.description = This is the binding for GREE 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.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.
+thing-type.config.gree.airconditioner.encryptionType.label = Encryption type
+thing-type.config.gree.airconditioner.encryptionType.description = The encryption type used for encrypting the data send to the AC device.
+thing-type.config.gree.airconditioner.encryptionType.state.option.ECB = ECB
+thing-type.config.gree.airconditioner.encryptionType.state.option.GCM = GCM
# 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.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.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.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
+# 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.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}
+message.discovery.newunit = Device {0} discovered at {1}, MAC={2}
+message.discovery.exception = Device Discovery failed: {0}
<unitLabel>Degrees Celsius</unitLabel>
<advanced>true</advanced>
</parameter>
+ <parameter name="encryptionType" type="text">
+ <options>
+ <option value="ECB">@text/thing-type.config.gree.airconditioner.encryptionType.state.option.ECB</option>
+ <option value="GCM">@text/thing-type.config.gree.airconditioner.encryptionType.state.option.GCM</option>
+ </options>
+ <advanced>true</advanced>
+ </parameter>
</config-description>
</thing-type>