mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[gree] Use GCM encryption when bind fails (#17398)
* [gree]: use GCM encryption when binding fails Signed-off-by: Zhivka Dimova <zhivka.dimova@myforest.net>
This commit is contained in:
parent
e6b372c053
commit
90442a3864
@ -19,15 +19,18 @@ No binding configuration is required.
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
| Channel Name | Type | Description |
|
||||
|--------------------------|------------|-----------------------------------------------------------------------------------------------|
|
||||
| ipAddress | IP Address | IP address of the unit. |
|
||||
| broadcastAddress | IP Address | Broadcast address being used for discovery, usually derived from the IP interface address. |
|
||||
| refresh | Integer | Refresh interval in seconds for polling the device status. |
|
||||
| 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
|
||||
@ -64,7 +67,7 @@ When changing mode, the air conditioner will be turned on unless "off" is select
|
||||
### 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
|
||||
|
@ -12,7 +12,11 @@
|
||||
*/
|
||||
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;
|
||||
@ -39,6 +43,8 @@ public class GreeBindingConstants {
|
||||
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";
|
||||
@ -174,4 +180,17 @@ public class GreeBindingConstants {
|
||||
* 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -12,6 +12,8 @@
|
||||
*/
|
||||
package org.openhab.binding.gree.internal;
|
||||
|
||||
import static org.openhab.binding.gree.internal.GreeBindingConstants.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
@ -32,10 +34,11 @@ public class GreeConfiguration {
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,8 @@
|
||||
*/
|
||||
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;
|
||||
@ -45,11 +47,6 @@ public class GreeCryptoUtil {
|
||||
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);
|
||||
}
|
||||
@ -86,6 +83,10 @@ public class GreeCryptoUtil {
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -95,6 +96,10 @@ public class GreeCryptoUtil {
|
||||
|
||||
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 {
|
||||
|
@ -106,7 +106,7 @@ public class GreeException extends Exception {
|
||||
|
||||
private Class<?> getCauseClass() {
|
||||
Throwable cause = getCause();
|
||||
if (getCause() != null) {
|
||||
if (cause != null) {
|
||||
return cause.getClass();
|
||||
}
|
||||
return GreeException.class;
|
||||
|
@ -61,7 +61,8 @@ public class GreeDeviceFinder {
|
||||
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);
|
||||
@ -103,7 +104,8 @@ public class GreeDeviceFinder {
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
|
@ -87,7 +87,7 @@ public class GreeDiscoveryService extends AbstractDiscoveryService {
|
||||
@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));
|
||||
@ -112,6 +112,7 @@ public class GreeDiscoveryService extends AbstractDiscoveryService {
|
||||
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();
|
||||
|
@ -64,7 +64,7 @@ public class GreeAirDevice {
|
||||
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();
|
||||
@ -148,7 +148,7 @@ public class GreeAirDevice {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@ -158,6 +158,7 @@ public class GreeAirDevice {
|
||||
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);
|
||||
@ -174,7 +175,12 @@ public class GreeAirDevice {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -460,7 +466,7 @@ public class GreeAirDevice {
|
||||
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 {
|
||||
@ -519,6 +525,24 @@ public class GreeAirDevice {
|
||||
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);
|
||||
}
|
||||
@ -528,7 +552,12 @@ public class GreeAirDevice {
|
||||
}
|
||||
|
||||
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() {
|
||||
|
@ -108,12 +108,12 @@ public class GreeHandler extends BaseThingHandler {
|
||||
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;
|
||||
@ -138,7 +138,7 @@ public class GreeHandler extends BaseThingHandler {
|
||||
@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();
|
||||
@ -377,8 +377,9 @@ public class GreeHandler extends BaseThingHandler {
|
||||
}
|
||||
} 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) {
|
||||
|
@ -1,12 +1,15 @@
|
||||
# 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
|
||||
@ -15,8 +18,13 @@ 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.
|
||||
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
|
||||
@ -70,7 +78,7 @@ 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.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
|
||||
@ -83,7 +91,8 @@ channel-type.gree.light.description = Enable/disable the front display on the Ai
|
||||
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}
|
||||
@ -92,5 +101,5 @@ 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}
|
||||
message.discovery.newunit = Device {0} discovered at {1}, MAC={2}
|
||||
message.discovery.exception = Device Discovery failed: {0}
|
||||
|
@ -41,6 +41,13 @@
|
||||
<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>
|
||||
|
Loading…
Reference in New Issue
Block a user