diff --git a/.idea/dictionaries/t.xml b/.idea/dictionaries/t.xml
index c82ca81bd..18f9cb697 100644
--- a/.idea/dictionaries/t.xml
+++ b/.idea/dictionaries/t.xml
@@ -53,6 +53,7 @@
gideão
girolamo
gobbetti
+ gree
greenberg
greenrobot
greffier
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index 2b33bfa43..9f572d6e3 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -72,6 +72,7 @@
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.SerializedName ;
}
+-keep class nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages.** {*; }
# Somehow the rule above was not enough for some
-keep class nodomain.freeyourgadget.gadgetbridge.devices.pinetime.InfiniTimeDFU* { *; }
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9534a737d..75bd18f1b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -647,6 +647,10 @@
android:name=".devices.evenrealities.G1PairingActivity"
android:label="@string/title_activity_even_realities_g1_pairing"
android:parentActivityName=".activities.discovery.DiscoveryActivityV2" />
+
diff --git a/app/src/main/assets/ic_device_air_conditioning.svg b/app/src/main/assets/ic_device_air_conditioning.svg
new file mode 100644
index 000000000..061a88062
--- /dev/null
+++ b/app/src/main/assets/ic_device_air_conditioning.svg
@@ -0,0 +1,124 @@
+
+
+
+
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gree/GreeAcCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gree/GreeAcCoordinator.java
new file mode 100644
index 000000000..c310a7104
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gree/GreeAcCoordinator.java
@@ -0,0 +1,72 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.gree;
+
+import android.app.Activity;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.regex.Pattern;
+
+import nodomain.freeyourgadget.gadgetbridge.GBException;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.gree.GreeAcSupport;
+
+public class GreeAcCoordinator extends AbstractBLEDeviceCoordinator {
+ @Override
+ protected void deleteDevice(@NonNull final GBDevice gbDevice, @NonNull final Device device, @NonNull final DaoSession session) throws GBException {
+ }
+
+ @Override
+ protected Pattern getSupportedDeviceName() {
+ // GR-AC_10001_09_xxxx_SC
+ return Pattern.compile("^GR-AC_\\d{5}_\\d{2}_[0-9a-f]{4}_SC$");
+ }
+
+ @Override
+ public String getManufacturer() {
+ return "Gree";
+ }
+
+ @NonNull
+ @Override
+ public Class extends DeviceSupport> getDeviceSupportClass() {
+ return GreeAcSupport.class;
+ }
+
+ @Override
+ public int getBondingStyle() {
+ return BONDING_STYLE_NONE;
+ }
+
+ @Override
+ public int getDeviceNameResource() {
+ return R.string.devicetype_gree_ac;
+ }
+
+ @Override
+ public int getDefaultIconResource() {
+ return R.drawable.ic_device_air_conditioning;
+ }
+
+ @Override
+ public int getDisabledIconResource() {
+ return R.drawable.ic_device_air_conditioning_disabled;
+ }
+
+ @Override
+ public boolean suggestUnbindBeforePair() {
+ // shouldn't matter
+ return false;
+ }
+
+ @Nullable
+ @Override
+ public Class extends Activity> getPairingActivity() {
+ return GreeAcPairingActivity.class;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gree/GreeAcPairingActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gree/GreeAcPairingActivity.java
new file mode 100644
index 000000000..d65439c2a
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gree/GreeAcPairingActivity.java
@@ -0,0 +1,265 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.gree;
+
+import android.content.BroadcastReceiver;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.appcompat.app.ActionBar;
+import androidx.core.content.ContextCompat;
+
+import com.google.android.material.textfield.TextInputEditText;
+import com.google.android.material.textfield.TextInputLayout;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
+import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.gree.GreeAcPrefs;
+import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
+
+public class GreeAcPairingActivity extends AbstractGBActivity {
+ private static final Logger LOG = LoggerFactory.getLogger(GreeAcPairingActivity.class);
+
+ public static final String ACTION_BIND_STATUS = "nodomain.freeyourgadget.gadgetbridge.gree.bind_status";
+ public static final String EXTRA_BIND_KEY = "extra_bind_key";
+ public static final String EXTRA_BIND_MESSAGE = "extra_bind_message";
+
+ private GBDeviceCandidate deviceCandidate;
+
+ private TextInputLayout textLayoutSsid;
+ private TextInputLayout textLayoutPassword;
+ private TextInputLayout textLayoutHost;
+ private TextInputEditText editTextSsid;
+ private TextInputEditText editTextPassword;
+ private TextInputEditText editTextHost;
+ private ProgressBar progressBar;
+ private TextView pairResultTextView;
+ private Button buttonPair;
+ private Button buttonCopy;
+
+ private GBDevice gbDevice;
+ private String bindKey;
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(final Context context, final Intent intent) {
+ final String action = intent.getAction();
+ if (action == null) {
+ return;
+ }
+
+ if (ACTION_BIND_STATUS.equals(action)) {
+ final String bindMessage = intent.getStringExtra(EXTRA_BIND_MESSAGE);
+ bindKey = intent.getStringExtra(EXTRA_BIND_KEY);
+
+ progressBar.setVisibility(View.GONE);
+ if ("1".equals(bindMessage)) {
+ pairResultTextView.setText(getString(R.string.gree_pair_status_success, String.valueOf(bindKey)));
+ buttonCopy.setVisibility(View.VISIBLE);
+ } else {
+ pairResultTextView.setText(getString(R.string.gree_pair_status_failure, bindMessage));
+ }
+
+ pairResultTextView.setVisibility(View.VISIBLE);
+ return;
+ }
+
+ if (GBDevice.ACTION_DEVICE_CHANGED.equals(action)) {
+ final GBDevice actionDevice = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE);
+ if (actionDevice == null || !actionDevice.getAddress().equals(deviceCandidate.getMacAddress())) {
+ return;
+ }
+
+ LOG.debug("Got device state: {}", actionDevice.getState());
+
+ if (actionDevice.getState() == GBDevice.State.NOT_CONNECTED && buttonCopy.getVisibility() != View.VISIBLE) {
+ pairResultTextView.setText(getString(R.string.gree_pair_status_failure, actionDevice.getState().toString()));
+ pairResultTextView.setVisibility(View.VISIBLE);
+
+ editTextSsid.setEnabled(true);
+ editTextPassword.setEnabled(true);
+ editTextHost.setEnabled(true);
+ buttonPair.setVisibility(View.VISIBLE);
+ progressBar.setVisibility(View.GONE);
+ }
+ }
+
+ LOG.error("Unknown action {}", action);
+ }
+ };
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ final Intent intent = getIntent();
+ deviceCandidate = intent.getParcelableExtra(DeviceCoordinator.EXTRA_DEVICE_CANDIDATE);
+
+ if (deviceCandidate == null) {
+ GB.toast(this, "Device candidate missing", Toast.LENGTH_LONG, GB.ERROR);
+ finish();
+ return;
+ }
+
+ setContentView(R.layout.activity_gree_ac_pairing);
+
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(ACTION_BIND_STATUS);
+ filter.addAction(GBDevice.ACTION_DEVICE_CHANGED);
+ ContextCompat.registerReceiver(this, mReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED);
+
+ final TextView textPairInfo = findViewById(R.id.gree_pair_info);
+ textLayoutSsid = findViewById(R.id.gree_pair_ssid_layout);
+ textLayoutPassword = findViewById(R.id.gree_pair_password_layout);
+ textLayoutHost = findViewById(R.id.gree_pair_host_layout);
+ editTextSsid = findViewById(R.id.gree_pair_ssid_text);
+ editTextPassword = findViewById(R.id.gree_pair_password_text);
+ editTextHost = findViewById(R.id.gree_pair_host_text);
+ progressBar = findViewById(R.id.gree_pair_progress_bar);
+ pairResultTextView = findViewById(R.id.gree_pair_result);
+ buttonPair = findViewById(R.id.gree_button_pair);
+ buttonCopy = findViewById(R.id.gree_button_copy);
+
+ textPairInfo.setText(getString(R.string.gree_pair_info, deviceCandidate.getName(), deviceCandidate.getMacAddress()));
+
+ final DevicePrefs devicePrefs = new DevicePrefs(GBApplication.getDeviceSpecificSharedPrefs(deviceCandidate.getMacAddress()), gbDevice);
+ editTextSsid.setText(devicePrefs.getString(GreeAcPrefs.PREF_SSID, ""));
+ editTextPassword.setText(devicePrefs.getString(GreeAcPrefs.PREF_PASSWORD, ""));
+ editTextHost.setText(devicePrefs.getString(GreeAcPrefs.PREF_HOST, ""));
+
+ editTextSsid.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {
+ }
+
+ @Override
+ public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
+ }
+
+ @Override
+ public void afterTextChanged(final Editable s) {
+ textLayoutSsid.setError(null);
+ }
+ });
+ editTextPassword.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {
+ }
+
+ @Override
+ public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
+ }
+
+ @Override
+ public void afterTextChanged(final Editable s) {
+ textLayoutPassword.setError(null);
+ }
+ });
+ editTextHost.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {
+ }
+
+ @Override
+ public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
+ }
+
+ @Override
+ public void afterTextChanged(final Editable s) {
+ textLayoutHost.setError(null);
+ }
+ });
+
+ buttonPair.setOnClickListener(v -> {
+ savePrefs();
+
+ final String ssid = editTextSsid.getText() != null ? editTextSsid.getText().toString() : "";
+ if (ssid.isEmpty() || ssid.length() > 32) {
+ textLayoutSsid.setError("Invalid SSID");
+ return;
+ }
+ final String password = editTextPassword.getText() != null ? editTextPassword.getText().toString() : "";
+ if (password.isEmpty() || password.length() < 8 || password.length() > 63) {
+ textLayoutPassword.setError("Invalid password");
+ return;
+ }
+ final String host = editTextHost.getText() != null ? editTextHost.getText().toString() : "";
+ if (host.isEmpty()) {
+ textLayoutHost.setError("Invalid host");
+ return;
+ }
+
+ editTextSsid.setEnabled(false);
+ editTextPassword.setEnabled(false);
+ editTextHost.setEnabled(false);
+ buttonPair.setVisibility(View.GONE);
+ progressBar.setVisibility(View.VISIBLE);
+ pairResultTextView.setVisibility(View.GONE);
+
+ GBApplication.deviceService().disconnect();
+ gbDevice = DeviceHelper.getInstance().toSupportedDevice(deviceCandidate.getDevice());
+ GBApplication.deviceService(gbDevice).connect(true);
+ });
+
+ buttonCopy.setOnClickListener(v -> {
+ final ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ final String clipboardData = getString(
+ R.string.gree_pair_clipboard,
+ deviceCandidate.getName(),
+ deviceCandidate.getMacAddress(),
+ bindKey
+ );
+ final ClipData clip = ClipData.newPlainText(deviceCandidate.getName(), clipboardData);
+ clipboard.setPrimaryClip(clip);
+ GB.toast(getString(R.string.copied_to_clipboard), Toast.LENGTH_LONG, GB.INFO);
+ });
+
+ final ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(deviceCandidate.getName());
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (gbDevice != null) {
+ GBApplication.deviceService(gbDevice).disconnect();
+ }
+ unregisterReceiver(mReceiver);
+ }
+
+ private void savePrefs() {
+ final DevicePrefs devicePrefs = new DevicePrefs(GBApplication.getDeviceSpecificSharedPrefs(deviceCandidate.getMacAddress()), gbDevice);
+ final SharedPreferences.Editor editor = devicePrefs.getPreferences().edit();
+ if (editTextSsid != null && editTextSsid.getText() != null) {
+ editor.putString(GreeAcPrefs.PREF_SSID, editTextSsid.getText().toString());
+ }
+ if (editTextPassword != null && editTextPassword.getText() != null) {
+ editor.putString(GreeAcPrefs.PREF_PASSWORD, editTextPassword.getText().toString());
+ }
+ if (editTextHost != null && editTextHost.getText() != null) {
+ editor.putString(GreeAcPrefs.PREF_HOST, editTextHost.getText().toString());
+ }
+ editor.apply();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
index 56f77036d..fd7807a5a 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
@@ -120,6 +120,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.vivomove.Garm
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.vivomove.GarminVivomoveTrendCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.vivosmart.GarminVivosmart5Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.vivosport.GarminVivosportCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.gree.GreeAcCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hama.fit6900.HamaFit6900DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.EXRIZUK8Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator;
@@ -514,6 +515,7 @@ public enum DeviceType {
GARMIN_VIVOACTIVE_5(GarminVivoActive5Coordinator.class),
GARMIN_VIVOSMART_5(GarminVivosmart5Coordinator.class),
GARMIN_VIVOSPORT(GarminVivosportCoordinator.class),
+ GREE_AC(GreeAcCoordinator.class),
VIBRATISSIMO(VibratissimoCoordinator.class),
SONY_SWR12(SonySWR12DeviceCoordinator.class),
LIVEVIEW(LiveviewCoordinator.class),
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/GreeAcPrefs.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/GreeAcPrefs.java
new file mode 100644
index 000000000..07001a1c2
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/GreeAcPrefs.java
@@ -0,0 +1,7 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.gree;
+
+public class GreeAcPrefs {
+ public static final String PREF_SSID = "gree_ssid";
+ public static final String PREF_PASSWORD = "gree_password";
+ public static final String PREF_HOST = "gree_host";
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/GreeAcSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/GreeAcSupport.java
new file mode 100644
index 000000000..e6d2d51d3
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/GreeAcSupport.java
@@ -0,0 +1,231 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.gree;
+
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.content.Intent;
+import android.util.Base64;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.util.Locale;
+import java.util.UUID;
+
+import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
+import nodomain.freeyourgadget.gadgetbridge.devices.gree.GreeAcPairingActivity;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages.AbstractGreeMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages.GreeBindMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages.GreeBleInfoMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages.GreeBleKeyMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages.GreePackMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages.GreeWlanMessage;
+import nodomain.freeyourgadget.gadgetbridge.util.CryptoUtils;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
+
+public class GreeAcSupport extends AbstractBTLEDeviceSupport {
+ private static final Logger LOG = LoggerFactory.getLogger(GreeAcSupport.class);
+
+ public static final UUID UUID_SERVICE_GREE_PACK = UUID.fromString("0000fd06-173c-93d2-488e-fe144d2e12a2");
+ public static final UUID UUID_CHARACTERISTIC_PACK_TX = UUID.fromString("0000fd03-0000-1000-8000-00805f9b34fb");
+ public static final UUID UUID_CHARACTERISTIC_PACK_RX = UUID.fromString("0000fd04-0000-1000-8000-00805f9b34fb");
+
+ public static final byte[] DEFAULT_KEY = "a3K8Bx%2r8Y7#xDh".getBytes();
+
+ private byte[] bindKey = null;
+
+ private BluetoothGattCharacteristic characteristicTx;
+
+ public GreeAcSupport() {
+ super(LOG);
+ addSupportedService(UUID_SERVICE_GREE_PACK);
+ }
+
+ @Override
+ public boolean useAutoConnect() {
+ return false;
+ }
+
+ @Override
+ protected TransactionBuilder initializeDevice(final TransactionBuilder builder) {
+ characteristicTx = getCharacteristic(UUID_CHARACTERISTIC_PACK_TX);
+ final BluetoothGattCharacteristic characteristicRx = getCharacteristic(UUID_CHARACTERISTIC_PACK_RX);
+
+ if (characteristicTx == null || characteristicRx == null) {
+ LOG.warn("Pack characteristics are null");
+ builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.NOT_CONNECTED, getContext()));
+ return builder;
+ }
+
+ characteristicTx.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
+
+ builder.notify(getCharacteristic(UUID_CHARACTERISTIC_PACK_RX), true);
+
+ final String mac = getDevice().getAddress().trim().replace(":", "").toLowerCase(Locale.ROOT);
+ writeMessage(builder, new GreeBindMessage(mac.substring(mac.length() - 4)));
+
+ return builder;
+ }
+
+ @Override
+ public boolean onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
+ if (super.onCharacteristicChanged(gatt, characteristic)) {
+ return true;
+ }
+
+ final UUID characteristicUUID = characteristic.getUuid();
+ final byte[] value = characteristic.getValue();
+
+ if (UUID_CHARACTERISTIC_PACK_RX.equals(characteristicUUID)) {
+ final String packMessageJson = new String(value, StandardCharsets.UTF_8);
+ LOG.debug("Got pack: {}", packMessageJson);
+
+ final AbstractGreeMessage message;
+ try {
+ message = AbstractGreeMessage.fromJson(packMessageJson);
+ } catch (final Exception e) {
+ LOG.error("Failed to deserialize message from json", e);
+ return true;
+ }
+
+ handleMessage(message);
+
+ return true;
+ }
+
+ LOG.warn("Unknown characteristic {} changed: {}", characteristicUUID, GB.hexdump(value));
+
+ return false;
+ }
+
+ private void handleMessage(final AbstractGreeMessage message) {
+ if (message instanceof GreePackMessage) {
+ final GreePackMessage packMessage = (GreePackMessage) message;
+ final int encryptionKeyNum = packMessage.getEncryptionKey();
+
+ final byte[] key;
+ switch (encryptionKeyNum) {
+ case GreePackMessage.KEY_BIND:
+ key = bindKey;
+ break;
+ case GreePackMessage.KEY_DEFAULT:
+ key = DEFAULT_KEY;
+ break;
+ default:
+ LOG.warn("Unknown pack message encryption key {}", encryptionKeyNum);
+ return;
+ }
+
+ if (key == null) {
+ LOG.error("Key {} is not known", encryptionKeyNum);
+ return;
+ }
+
+ final byte[] encryptedBytes = Base64.decode(packMessage.getPack(), Base64.DEFAULT);
+ final byte[] decryptedBytes;
+ try {
+ decryptedBytes = CryptoUtils.decryptAES_ECB_Pad(encryptedBytes, key);
+ } catch (final GeneralSecurityException e) {
+ LOG.error("Failed to decrypt pack", e);
+ return;
+ }
+
+ final AbstractGreeMessage packSubMessage;
+ try {
+ final String subMessageJson = new String(decryptedBytes, StandardCharsets.UTF_8);
+ LOG.debug("Pack sub message: {}", subMessageJson);
+
+ packSubMessage = AbstractGreeMessage.fromJson(subMessageJson);
+ } catch (final Exception e) {
+ LOG.error("Failed to deserialize pack sub message from json", e);
+ return;
+ }
+
+ handleMessage(packSubMessage);
+ return;
+ }
+
+ if (message instanceof GreeBleKeyMessage) {
+ final GreeBleKeyMessage bleKeyMessage = (GreeBleKeyMessage) message;
+
+ LOG.debug("Got bind key: {}", bleKeyMessage.getKey());
+ bindKey = bleKeyMessage.getKey().getBytes(StandardCharsets.UTF_8);
+
+ evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences("authkey", bleKeyMessage.getKey()));
+
+ final DevicePrefs devicePrefs = getDevicePrefs();
+ final String host = devicePrefs.getString(GreeAcPrefs.PREF_HOST, "");
+ final String psw = devicePrefs.getString(GreeAcPrefs.PREF_PASSWORD, "");
+ final String ssid = devicePrefs.getString(GreeAcPrefs.PREF_SSID, "");
+ if (host.isEmpty() || psw.isEmpty() || ssid.isEmpty()) {
+ final Intent intent = new Intent(GreeAcPairingActivity.ACTION_BIND_STATUS);
+ intent.setPackage(BuildConfig.APPLICATION_ID);
+ intent.putExtra(GreeAcPairingActivity.EXTRA_BIND_MESSAGE, "missing wlan params");
+ getContext().sendBroadcast(intent);
+ return;
+ }
+
+ final TransactionBuilder builder = createTransactionBuilder("setup wifi");
+ writeMessage(builder, new GreeWlanMessage(host, psw, ssid, 1));
+ builder.queue(getQueue());
+
+ return;
+ }
+
+ if (message instanceof GreeBleInfoMessage) {
+ final GreeBleInfoMessage bleInfoMessage = (GreeBleInfoMessage) message;
+
+ LOG.debug("Got ble info, wificon = {}", bleInfoMessage.getWificon());
+
+ final Intent intent = new Intent(GreeAcPairingActivity.ACTION_BIND_STATUS);
+ intent.setPackage(BuildConfig.APPLICATION_ID);
+ intent.putExtra(GreeAcPairingActivity.EXTRA_BIND_KEY, new String(bindKey));
+ intent.putExtra(GreeAcPairingActivity.EXTRA_BIND_MESSAGE, String.valueOf(bleInfoMessage.getWificon()));
+ getContext().sendBroadcast(intent);
+
+ // TODO disconnect - not much else we can do
+ }
+
+ LOG.warn("Unhandled message: {}", message);
+ }
+
+ private void writeMessage(final TransactionBuilder builder, final AbstractGreeMessage message) {
+ final String messageJson = message.toString();
+
+ LOG.debug("Will send: {}", messageJson);
+
+ if (message instanceof GreePackMessage) {
+ builder.write(characteristicTx, messageJson.getBytes(StandardCharsets.UTF_8));
+ return;
+ }
+
+ final int key;
+ final byte[] encryptedBytes;
+ try {
+ if (message instanceof GreeBindMessage) {
+ key = GreePackMessage.KEY_DEFAULT;
+ encryptedBytes = CryptoUtils.encryptAES_ECB_Pad(messageJson.getBytes(StandardCharsets.UTF_8), DEFAULT_KEY);
+ } else {
+ if (bindKey == null) {
+ LOG.error("No bind key, unable to encrypt");
+ return;
+ }
+ key = GreePackMessage.KEY_BIND;
+ encryptedBytes = CryptoUtils.encryptAES_ECB_Pad(messageJson.getBytes(StandardCharsets.UTF_8), bindKey);
+ }
+ } catch (final GeneralSecurityException e) {
+ LOG.error("Failed to encrypt message", e);
+ return;
+ }
+
+ final GreePackMessage packMessage = new GreePackMessage(Base64.encodeToString(encryptedBytes, Base64.DEFAULT).replace("\n", "").trim(), key);
+ writeMessage(builder, packMessage);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/AbstractGreeMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/AbstractGreeMessage.java
new file mode 100644
index 000000000..3443cf087
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/AbstractGreeMessage.java
@@ -0,0 +1,37 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages;
+
+import androidx.annotation.NonNull;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.TypeAdapterFactory;
+import com.google.gson.typeadapters.RuntimeTypeAdapterFactory;
+
+public abstract class AbstractGreeMessage {
+ private static final Gson GSON = new GsonBuilder()
+ .registerTypeAdapterFactory(getTypeAdapterFactory())
+ .serializeNulls()
+ .disableHtmlEscaping()
+ .create();
+
+ @NonNull
+ @Override
+ public String toString() {
+ return GSON.toJson(this);
+ }
+
+ public static TypeAdapterFactory getTypeAdapterFactory() {
+ return RuntimeTypeAdapterFactory
+ .of(AbstractGreeMessage.class, "t")
+ .registerSubtype(GreePackMessage.class, GreePackMessage.TYPE)
+ .registerSubtype(GreeBindMessage.class, GreeBindMessage.TYPE)
+ .registerSubtype(GreeBleInfoMessage.class, GreeBleInfoMessage.TYPE)
+ .registerSubtype(GreeBleKeyMessage.class, GreeBleKeyMessage.TYPE)
+ .registerSubtype(GreeWlanMessage.class, GreeWlanMessage.TYPE)
+ .recognizeSubtypes();
+ }
+
+ public static AbstractGreeMessage fromJson(final String json) {
+ return GSON.fromJson(json, AbstractGreeMessage.class);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBindMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBindMessage.java
new file mode 100644
index 000000000..f108ad28d
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBindMessage.java
@@ -0,0 +1,13 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages;
+
+public class GreeBindMessage extends AbstractGreeMessage {
+ public static final String TYPE = "bind";
+
+ private final String mac;
+ private final int IsCP;
+
+ public GreeBindMessage(final String mac) {
+ this.mac = mac;
+ this.IsCP = 1;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBleInfoMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBleInfoMessage.java
new file mode 100644
index 000000000..57571141a
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBleInfoMessage.java
@@ -0,0 +1,33 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages;
+
+public class GreeBleInfoMessage extends AbstractGreeMessage {
+ public static final String TYPE = "bleinfo";
+
+ private final int wificon;
+ private final String mac;
+ private final String mid;
+ private final String ver;
+
+ public GreeBleInfoMessage(final int wificon, final String mac, final String mid, final String ver) {
+ this.wificon = wificon;
+ this.mac = mac;
+ this.mid = mid;
+ this.ver = ver;
+ }
+
+ public int getWificon() {
+ return wificon;
+ }
+
+ public String getMac() {
+ return mac;
+ }
+
+ public String getMid() {
+ return mid;
+ }
+
+ public String getVer() {
+ return ver;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBleKeyMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBleKeyMessage.java
new file mode 100644
index 000000000..0d8fa8b5c
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeBleKeyMessage.java
@@ -0,0 +1,15 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages;
+
+public class GreeBleKeyMessage extends AbstractGreeMessage {
+ public static final String TYPE = "blekey";
+
+ private final String key;
+
+ public GreeBleKeyMessage(final String key) {
+ this.key = key;
+ }
+
+ public String getKey() {
+ return key;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreePackMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreePackMessage.java
new file mode 100644
index 000000000..85317adc1
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreePackMessage.java
@@ -0,0 +1,26 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages;
+
+public class GreePackMessage extends AbstractGreeMessage {
+ public static final String TYPE = "pack";
+
+ public static final int KEY_DEFAULT = 1;
+ public static final int KEY_BIND = 0;
+
+ private final String pack;
+ private final int i;
+ private final int pIn;
+
+ public GreePackMessage(final String pack, final int encryptionKey) {
+ this.pack = pack;
+ this.i = encryptionKey;
+ this.pIn = 0;
+ }
+
+ public String getPack() {
+ return pack;
+ }
+
+ public int getEncryptionKey() {
+ return i;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeWlanMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeWlanMessage.java
new file mode 100644
index 000000000..1f52335f0
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gree/messages/GreeWlanMessage.java
@@ -0,0 +1,17 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.gree.messages;
+
+public class GreeWlanMessage extends AbstractGreeMessage {
+ public static final String TYPE = "wlan";
+
+ private final String host;
+ private final String psw;
+ private final String ssid;
+ private final int num;
+
+ public GreeWlanMessage(final String host, final String psw, final String ssid, final int num) {
+ this.host = host;
+ this.psw = psw;
+ this.ssid = ssid;
+ this.num = num;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java
index c5e4cc686..6b99dd3b5 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CryptoUtils.java
@@ -21,6 +21,7 @@ import android.annotation.SuppressLint;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
@@ -71,6 +72,20 @@ public class CryptoUtils {
return cipher.doFinal(data);
}
+ public static byte[] encryptAES_ECB_Pad(byte[] data, byte[] key) throws GeneralSecurityException {
+ final Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+ final SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
+ cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
+ return cipher.doFinal(data);
+ }
+
+ public static byte[] decryptAES_ECB_Pad(byte[] data, byte[] key) throws GeneralSecurityException {
+ final Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
+ final SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
+ cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
+ return cipher.doFinal(data);
+ }
+
public static byte[] encryptAES_GCM_NoPad(byte[] data, byte[] key, byte[] iv, byte[] aad) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
diff --git a/app/src/main/res/drawable/ic_device_air_conditioning.xml b/app/src/main/res/drawable/ic_device_air_conditioning.xml
new file mode 100644
index 000000000..c1fb8d6aa
--- /dev/null
+++ b/app/src/main/res/drawable/ic_device_air_conditioning.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_device_air_conditioning_disabled.xml b/app/src/main/res/drawable/ic_device_air_conditioning_disabled.xml
new file mode 100644
index 000000000..708f09c04
--- /dev/null
+++ b/app/src/main/res/drawable/ic_device_air_conditioning_disabled.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_gree_ac_pairing.xml b/app/src/main/res/layout/activity_gree_ac_pairing.xml
new file mode 100644
index 000000000..cf08303dc
--- /dev/null
+++ b/app/src/main/res/layout/activity_gree_ac_pairing.xml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c639ff868..83ace7874 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1358,6 +1358,7 @@
Bound to %1$s.
Pair with %1$s?
Select Pair to pair your devices. If this fails, try again without pairing.
+ Copy
Pair
Don\'t Pair
@@ -1814,6 +1815,7 @@
Garmin Vívoactive 5
Garmin Vívosmart 5
Garmin Vívosport
+ Gree Air Conditioner
Vibratissimo
UM-25
LiveView
@@ -3631,4 +3633,9 @@
Do not enable this if you plan to use the official app or connect the watch to the internet. After enabling this setting, you may need to factory reset the watch to authenticate again.
Send fake OAuth responses
Fixes some functions such as weather and AGPS updates without connecting to the official app every 90 days.
+ Host
+ You are about to pair with %s (%s). Please enter the Wi-Fi and host server information for pairing. See the website for more information.
+ Pairing success.\n\nBind key: %s
+ Pairing failed: %s
+ Device name: %s\nMac address: %s\nBind key: %s