Gree Air Conditioner: Initial support

This commit is contained in:
José Rebelo 2025-02-09 16:17:58 +00:00
parent feb5134b5d
commit 7b2b162d55
20 changed files with 1061 additions and 0 deletions

View File

@ -53,6 +53,7 @@
<w>gideão</w>
<w>girolamo</w>
<w>gobbetti</w>
<w>gree</w>
<w>greenberg</w>
<w>greenrobot</w>
<w>greffier</w>

View File

@ -72,6 +72,7 @@
-keepclassmembers,allowobfuscation class * {
@com.google.gson.annotations.SerializedName <fields>;
}
-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* { *; }

View File

@ -647,6 +647,10 @@
android:name=".devices.evenrealities.G1PairingActivity"
android:label="@string/title_activity_even_realities_g1_pairing"
android:parentActivityName=".activities.discovery.DiscoveryActivityV2" />
<activity
android:name=".devices.gree.GreeAcPairingActivity"
android:label="@string/devicetype_gree_ac"
android:parentActivityName=".activities.discovery.DiscoveryActivityV2" />
<activity
android:name=".devices.watch9.Watch9PairingActivity"
android:label="@string/title_activity_watch9_pairing" />

View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg2"
width="28.740126"
height="28.740126"
viewBox="0 0 28.740126 28.740126"
sodipodi:docname="ic_device_galaxy_buds.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata
id="metadata8"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs6" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1035"
id="namedview4"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="22.627418"
inkscape:cx="8.1096305"
inkscape:cy="10.982252"
inkscape:window-x="0"
inkscape:window-y="45"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
inkscape:pagecheckerboard="0"
inkscape:showpageshadow="2"
inkscape:deskcolor="#d1d1d1"><inkscape:grid
type="xygrid"
id="grid972"
originx="0"
originy="0"
spacingy="1"
spacingx="1"
units="px" /><inkscape:page
x="0"
y="0"
width="28.740126"
height="28.740126"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Background"
style="display:inline"><g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Bottom"
style="display:inline"
sodipodi:insensitive="true"><rect
style="display:inline;fill:#1f7fdb;fill-opacity:1;stroke:none;stroke-width:3.57115;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect817-5"
width="22.819572"
height="21.904638"
x="2.9239759"
y="3.8768768"
ry="0.9472276" /></g><g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Middle"
style="display:inline"
sodipodi:insensitive="true"><rect
style="display:inline;fill:#4dabf5;fill-opacity:1;stroke:none;stroke-width:3.57115;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect817-7"
width="22.819572"
height="21.904638"
x="2.9317887"
y="3.0349438"
ry="0.9472276" /></g><g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Top"
style="display:inline"
sodipodi:insensitive="true"><rect
style="fill:#2196f3;fill-opacity:1;stroke:none;stroke-width:3.57115;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect817"
width="22.819572"
height="21.904638"
x="2.9238882"
y="3.4130721"
ry="0.9472276" /></g><g
id="g2507"
transform="matrix(0.99999822,0,0,1,0.29668164,-0.00953878)"><g
style="fill:#ffffff"
id="g1"
transform="matrix(0.01921905,0,0,0.01921902,4.4167806,4.9004638)"><path
d="m 383.2,941.6 c -4,0 -6.4,-0.8 -9.6,-2.4 -14.4,-9.6 -82.4,-60 -25.6,-160 15.2,-27.2 19.2,-48.8 13.6,-66.4 -6.4,-20 -27.2,-28 -27.2,-28 -9.6,-4 -14.4,-15.2 -10.4,-24.8 3.2,-8 9.6,-12.8 17.6,-12.8 2.4,0 4.8,0.8 7.2,1.6 3.2,0 36.8,14.4 50.4,50.4 10.4,28.8 4.8,62.4 -16.8,100 -40,70.4 2.4,101.6 11.2,107.2 8.8,4.8 12,16.8 7.2,26.4 -4.8,6.4 -12,8.8 -17.6,8.8 z m 129.6,-42.4 c -4,0 -6.4,-0.8 -9.6,-2.4 -28.8,-16.8 -69.6,-68.8 -24,-150.4 13.6,-24.8 17.6,-44.8 12,-60 -5.6,-17.6 -24,-24.8 -24,-24.8 -10.4,-4 -15.2,-14.4 -11.2,-24.8 3.2,-8 9.6,-12.8 17.6,-12.8 2.4,0 4.8,0.8 7.2,1.6 8,3.2 36,16 47.2,48 9.6,27.2 4.8,58.4 -15.2,92.8 -38.4,68.8 8,96.8 10.4,97.6 4,2.4 7.2,6.4 8.8,11.2 1.6,4.8 0.8,10.4 -1.6,15.2 -4.8,6.4 -12,8.8 -17.6,8.8 z m -265.6,0 c -4,0 -6.4,-0.8 -9.6,-2.4 C 208.8,880 168,828 213.6,746.4 c 13.6,-24.8 17.6,-44.8 12,-60 -5.6,-17.6 -24,-24.8 -24,-24.8 -10.4,-4 -15.2,-14.4 -11.2,-24.8 3.2,-8 9.6,-12.8 17.6,-12.8 2.4,0 4.8,0.8 7.2,1.6 8,3.2 36,16.8 47.2,48 9.6,27.2 4,58.4 -15.2,93.6 -38.4,68.8 8,96.8 10.4,97.6 4,2.4 7.2,6.4 8.8,11.2 1.6,4.8 0.8,10.4 -1.6,15.2 -4,5.6 -12,8 -17.6,8 z M 734.4,828 c -6.4,0 -11.2,-2.4 -15.2,-6.4 -4,-4 -6.4,-9.6 -6.4,-15.2 0,-8.8 5.6,-16.8 13.6,-20.8 h 0.8 v -43.2 l -40.8,23.2 v 0.8 c 0,6.4 -2.4,14.4 -10.4,18.4 -3.2,2.4 -7.2,3.2 -11.2,3.2 -8,0 -15.2,-4 -18.4,-10.4 -3.2,-5.6 -4,-11.2 -2.4,-16.8 1.6,-5.6 4.8,-10.4 10.4,-12.8 3.2,-2.4 7.2,-3.2 10.4,-3.2 4,0 8,0.8 11.2,3.2 l 36.8,-20.8 -36,-20.8 c -4.8,1.6 -8.8,2.4 -12,2.4 -4,0 -7.2,-0.8 -10.4,-2.4 -9.6,-6.4 -13.6,-20 -8,-29.6 4,-6.4 11.2,-10.4 19.2,-10.4 4,0 8,0.8 11.2,3.2 6.4,4 10.4,10.4 10.4,18.4 v 0.8 L 728,712 v -43.2 h -0.8 c -8,-4 -13.6,-12 -13.6,-20.8 0,-12 9.6,-21.6 21.6,-21.6 6.4,0 12,2.4 16,6.4 4,4 6.4,9.6 6.4,15.2 0,7.2 -4,14.4 -11.2,18.4 H 744 V 708 l 35.2,-20 v -0.8 c 0,-6.4 2.4,-14.4 10.4,-18.4 3.2,-2.4 7.2,-3.2 11.2,-3.2 8,0 15.2,4 18.4,10.4 3.2,5.6 4,11.2 2.4,16.8 -1.6,5.6 -4.8,10.4 -10.4,12.8 -3.2,2.4 -7.2,3.2 -10.4,3.2 -4,0 -8,-0.8 -11.2,-3.2 l -36.8,20.8 36,20.8 c 4.8,-1.6 8.8,-2.4 12,-2.4 4,0 7.2,0.8 10.4,2.4 9.6,6.4 13.6,20 8,29.6 -4,6.4 -11.2,10.4 -19.2,10.4 -4,0 -8,-0.8 -11.2,-3.2 -6.4,-4 -10.4,-10.4 -10.4,-18.4 v -0.8 L 744,745.6 v 41.6 h 0.8 c 7.2,3.2 11.2,10.4 11.2,18.4 0,12.8 -9.6,22.4 -21.6,22.4 z M 152,575.2 C 91.2,575.2 42.4,526.4 42.4,465.6 V 204.8 C 42.4,144 91.2,95.2 152,95.2 h 721.6 c 60.8,0 109.6,48.8 109.6,109.6 v 260.8 c 0,60.8 -48.8,109.6 -109.6,109.6 z m -11.2,-432 c -28.8,0 -52,24 -52,54.4 V 476 c 0,29.6 23.2,54.4 52,54.4 H 884 c 28.8,0 52,-24 52,-54.4 V 197.6 c 0,-29.6 -23.2,-54.4 -52,-54.4 z m 21.6,312 c -10.4,0 -19.2,-8.8 -19.2,-19.2 0,-10.4 8.8,-19.2 19.2,-19.2 h 682.4 c 10.4,0 19.2,8.8 19.2,19.2 0,10.4 -8.8,19.2 -19.2,19.2 z m 0,-80.8 c -10.4,0 -19.2,-8.8 -19.2,-19.2 0,-10.4 8.8,-19.2 19.2,-19.2 h 682.4 c 10.4,0 19.2,8.8 19.2,19.2 0,10.4 -8.8,19.2 -19.2,19.2 z"
fill=""
id="path1"
style="fill:#ffffff" /></g></g></g><g
inkscape:groupmode="layer"
id="layer5"
inkscape:label="Original"
style="display:none"
sodipodi:insensitive="true"><image
y="0.0079936879"
x="0.010951031"
id="image4124"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAA3NCSVQICAjb4U/gAAAACXBIWXMA ADFOAAAxTgGacD5yAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAABF0RVh0 VGl0bGUAUGViYmxlIEljb26KvwD9AAAADHRFWHRBdXRob3IAeHBobnh/4OzMAAAAY3RFWHRDb3B5 cmlnaHQAQ0MgQXR0cmlidXRpb24tTm9uQ29tbWVyY2lhbC1TaGFyZUFsaWtlIGh0dHA6Ly9jcmVh dGl2ZWNvbW1vbnMub3JnL2xpY2Vuc2VzL2J5LW5jLXNhLzMuMC/eBBrlAAABy1BMVEUAAAAAAAAA AP8AAIAAAFUAAKoAVaoAAIAAQIAAM2YAM5kzM2YzM5krK4AkJG0kJJIgIIAcHHEcHI4aGoAXF3QX F4sXLnQXLosVK4AUJ3YUJ4kSJIAkJIARInciInciIogQIIAgIIAeHngeHoccHIAcK4AbKHkbKIYa GoAaJoAYJHkXI4AXI4sWIXoWIYUVIIAfH3ofH4UdJ4ATHIQcJnsbJIAaI4QaIoAZIXsZIYQYIHgY IIAXH4MXJ3wXHoAXJoBLpfAWJHwdJHwdJIMcI4Acas0VIoMbIoAaIXwaIIAZH3wZH4MZJXwYHoAY HoYYJIAehtsYHoIefdtHoO0jetZGovNEn+4gg+EfgN5Ep/VKqfREpvQgfdkgfttKqvQggd4hjesw nPMwnPRJqfUhgd4hjutJqfUgfttNq/VNq/QhlPEhlPIhlvMkmPMglvIff9sgjeoglPAhk/AhlPEh lPIhlvMkl/MkmPMlmPMmmPMnmfMpmvMzn/Q0n/Q1n/Q2oPRIqfVNq/VasfZbsvZhtPZitfdtuveQ yvmXzvmj0/qk1Pqu2fu83/vR6f3T6v3Z7f3i8f3j8v3l8/7m8/7p9f7q9f7x+P7y+f7////ougeB AAAAb3RSTlMAAQECAwMDBAQFBQUFBgcHCAkJCgsLCwsMDQ0ODg8PDxAQERESEhMTFBQVFhYXFxgZ GRobGxwdHh8fICAhISIiIiMjIyQkJSYnKCkpKSoqKiorKyssLC2xs7O0tba2tu7u7u7u7+/w8fHy +/v7+/ywJhX9AAAC0klEQVRo3u2a+VfTQBCAm00iW0nSTVNKbKQYbIMUC6jU4q2IB2rF+74vPEAR LSjIIXgAAiqef65py6MNJe17y1RF8/26L/N1N9O8ZGZcLof/DiYFoiV1cZHwiGVZjh7zalRAYYbn sFsQ6RFwGccyBeJjwSMrPn8lHX6fQqRy3tbAsFiU1So9FA4bNIRD63XNTwTO5pSYc9fuvwbgwaW9 SxqYs/eGBgcAGBy+fRrlpRPDsJdfJoEYuMgvzibz/vK3kmDcFMuxNZvM/BG74QTdFV6PJZsYlhe9 XXCCrlCVStwcyhG45QCkoCmiV0o4uwXEib4aQMHjbc0RTXYvCMx/mGdNHaSgraWxRhEXzsgUEC0C KWjfszlUYRWsrYcUHGttNlSRzxUEo5CCjrZYrSqVVlD3Nwj6vqcZsovzKrPeTy3o/5lmxE4wnFl/ 4QhKLvjy2YY5IEExHMH0B1umQQSj9o+EUUewIBh/m8c4qOBbfu58dQRLCmYnJmZLKphKJicdwR8X vJsymSmhIM37FSv4OJlhxnlUOAJHAC8Ye5PHmPNe9PsEJX/5dT5AigvmPtkw53wnr3xB34809uWc zDp9OWflV7xgBaBlzY79MWORALhuui+vbhoArfwe3rUp5MtWfl2Il1QDUtAaj+rKakv1XdEhBfGm Wo1kq+/pM1Ih+wdRI6iIq3IFvEAewgke6QHFg1mU26PjhbtwghsKEbG1X8ew3PmnUPGfXRAwv7hr yqAD13t6nwPQ23PnBMeivG4gg85c7XwCQOeVU+avZ5bslcrrNm7dfehIInGSguOJo+0Hd27ZoOam j3ULvOjTI02x+PYddLTEY42GJpfnpo/L0g50k8pqI1IfbWikoSFaXxfSFKnMrqFsZpKbKKoWDFbT EdQCqlfCdhtIG7AoESLTQogk4EJd/VRTn8fLgecKTg3MjyUsA4SKDD7MT1bQ8++NmvwCRmydh2Fg UvEAAAAASUVORK5CYII= "
style="opacity:0.490741;image-rendering:optimizeQuality"
preserveAspectRatio="none"
height="28.710279"
width="28.710279" /></g></svg>

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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),

View File

@ -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";
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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");

View File

@ -0,0 +1,33 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28.7dp"
android:height="28.7dp"
android:viewportWidth="28.74"
android:viewportHeight="28.74">
<!--
Adapted from https://www.svgrepo.com/svg/493976/air-conditioner
-->
<path
android:fillColor="?attr/deviceIconLight"
android:pathData="M3.871,3.877L24.796,3.877A0.947,0.947 0,0 1,25.744 4.824L25.744,24.834A0.947,0.947 0,0 1,24.796 25.782L3.871,25.782A0.947,0.947 0,0 1,2.924 24.834L2.924,4.824A0.947,0.947 0,0 1,3.871 3.877z"
android:strokeWidth="3.57115"
android:strokeColor="#00000000" />
<path
android:fillColor="?attr/deviceIconDark"
android:pathData="M3.879,3.035L24.804,3.035A0.947,0.947 0,0 1,25.751 3.982L25.751,23.992A0.947,0.947 0,0 1,24.804 24.94L3.879,24.94A0.947,0.947 0,0 1,2.932 23.992L2.932,3.982A0.947,0.947 0,0 1,3.879 3.035z"
android:strokeWidth="3.57115"
android:strokeColor="#00000000" />
<path
android:fillColor="?attr/deviceIconPrimary"
android:pathData="M3.871,3.413L24.796,3.413A0.947,0.947 0,0 1,25.743 4.36L25.743,24.37A0.947,0.947 0,0 1,24.796 25.318L3.871,25.318A0.947,0.947 0,0 1,2.924 24.37L2.924,4.36A0.947,0.947 0,0 1,3.871 3.413z"
android:strokeWidth="3.57115"
android:strokeColor="#00000000" />
<path
android:fillColor="?attr/deviceIconOnPrimary"
android:pathData="m12.078,22.988c-0.077,0 -0.123,-0.015 -0.185,-0.046 -0.277,-0.185 -1.584,-1.153 -0.492,-3.075 0.292,-0.523 0.369,-0.938 0.261,-1.276 -0.123,-0.384 -0.523,-0.538 -0.523,-0.538 -0.185,-0.077 -0.277,-0.292 -0.2,-0.477 0.062,-0.154 0.185,-0.246 0.338,-0.246 0.046,0 0.092,0.015 0.138,0.031 0.062,0 0.707,0.277 0.969,0.969 0.2,0.554 0.092,1.199 -0.323,1.922 -0.769,1.353 0.046,1.953 0.215,2.06 0.169,0.092 0.231,0.323 0.138,0.507 -0.092,0.123 -0.231,0.169 -0.338,0.169zM14.569,22.173c-0.077,0 -0.123,-0.015 -0.185,-0.046 -0.554,-0.323 -1.338,-1.322 -0.461,-2.891 0.261,-0.477 0.338,-0.861 0.231,-1.153 -0.108,-0.338 -0.461,-0.477 -0.461,-0.477 -0.2,-0.077 -0.292,-0.277 -0.215,-0.477 0.062,-0.154 0.185,-0.246 0.338,-0.246 0.046,0 0.092,0.015 0.138,0.031 0.154,0.062 0.692,0.308 0.907,0.923 0.185,0.523 0.092,1.122 -0.292,1.784 -0.738,1.322 0.154,1.86 0.2,1.876 0.077,0.046 0.138,0.123 0.169,0.215 0.031,0.092 0.015,0.2 -0.031,0.292 -0.092,0.123 -0.231,0.169 -0.338,0.169zM9.464,22.173c-0.077,0 -0.123,-0.015 -0.185,-0.046C8.726,21.804 7.942,20.804 8.819,19.236c0.261,-0.477 0.338,-0.861 0.231,-1.153 -0.108,-0.338 -0.461,-0.477 -0.461,-0.477 -0.2,-0.077 -0.292,-0.277 -0.215,-0.477 0.062,-0.154 0.185,-0.246 0.338,-0.246 0.046,0 0.092,0.015 0.138,0.031 0.154,0.062 0.692,0.323 0.907,0.923 0.185,0.523 0.077,1.122 -0.292,1.799 -0.738,1.322 0.154,1.86 0.2,1.876 0.077,0.046 0.138,0.123 0.169,0.215 0.031,0.092 0.015,0.2 -0.031,0.292 -0.077,0.108 -0.231,0.154 -0.338,0.154zM18.828,20.804c-0.123,0 -0.215,-0.046 -0.292,-0.123 -0.077,-0.077 -0.123,-0.185 -0.123,-0.292 0,-0.169 0.108,-0.323 0.261,-0.4l0.015,0l0,-0.83l-0.784,0.446l0,0.015c0,0.123 -0.046,0.277 -0.2,0.354 -0.062,0.046 -0.138,0.062 -0.215,0.062 -0.154,0 -0.292,-0.077 -0.354,-0.2 -0.062,-0.108 -0.077,-0.215 -0.046,-0.323 0.031,-0.108 0.092,-0.2 0.2,-0.246 0.062,-0.046 0.138,-0.062 0.2,-0.062 0.077,0 0.154,0.015 0.215,0.062l0.707,-0.4 -0.692,-0.4c-0.092,0.031 -0.169,0.046 -0.231,0.046 -0.077,0 -0.138,-0.015 -0.2,-0.046 -0.185,-0.123 -0.261,-0.384 -0.154,-0.569 0.077,-0.123 0.215,-0.2 0.369,-0.2 0.077,0 0.154,0.015 0.215,0.062 0.123,0.077 0.2,0.2 0.2,0.354l0,0.015L18.705,18.575l0,-0.83l-0.015,0c-0.154,-0.077 -0.261,-0.231 -0.261,-0.4 0,-0.231 0.185,-0.415 0.415,-0.415 0.123,0 0.231,0.046 0.308,0.123 0.077,0.077 0.123,0.185 0.123,0.292 0,0.138 -0.077,0.277 -0.215,0.354L19.012,17.698L19.012,18.498l0.677,-0.384l0,-0.015c0,-0.123 0.046,-0.277 0.2,-0.354 0.062,-0.046 0.138,-0.062 0.215,-0.062 0.154,0 0.292,0.077 0.354,0.2 0.062,0.108 0.077,0.215 0.046,0.323 -0.031,0.108 -0.092,0.2 -0.2,0.246 -0.062,0.046 -0.138,0.062 -0.2,0.062 -0.077,0 -0.154,-0.015 -0.215,-0.062l-0.707,0.4 0.692,0.4c0.092,-0.031 0.169,-0.046 0.231,-0.046 0.077,0 0.138,0.015 0.2,0.046 0.185,0.123 0.261,0.384 0.154,0.569 -0.077,0.123 -0.215,0.2 -0.369,0.2 -0.077,0 -0.154,-0.015 -0.215,-0.062 -0.123,-0.077 -0.2,-0.2 -0.2,-0.354l0,-0.015L19.012,19.221l0,0.8l0.015,0c0.138,0.062 0.215,0.2 0.215,0.354 0,0.246 -0.185,0.431 -0.415,0.431zM7.635,15.946C6.466,15.946 5.528,15.008 5.528,13.839L5.528,8.827C5.528,7.658 6.466,6.721 7.635,6.721l13.868,0c1.169,0 2.106,0.938 2.106,2.106l0,5.012c0,1.169 -0.938,2.106 -2.106,2.106zM7.419,7.643c-0.554,0 -0.999,0.461 -0.999,1.046L6.42,14.039c0,0.569 0.446,1.046 0.999,1.046L21.703,15.085c0.554,0 0.999,-0.461 0.999,-1.046L22.702,8.689c0,-0.569 -0.446,-1.046 -0.999,-1.046zM7.835,13.639c-0.2,0 -0.369,-0.169 -0.369,-0.369 0,-0.2 0.169,-0.369 0.369,-0.369l13.115,0c0.2,0 0.369,0.169 0.369,0.369 0,0.2 -0.169,0.369 -0.369,0.369zM7.835,12.087c-0.2,0 -0.369,-0.169 -0.369,-0.369 0,-0.2 0.169,-0.369 0.369,-0.369l13.115,0c0.2,0 0.369,0.169 0.369,0.369 0,0.2 -0.169,0.369 -0.369,0.369z" />
</vector>

View File

@ -0,0 +1,33 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28.7dp"
android:height="28.7dp"
android:viewportWidth="28.74"
android:viewportHeight="28.74">
<!--
Adapted from https://www.svgrepo.com/svg/493976/air-conditioner
-->
<path
android:fillColor="#7a7a7a"
android:pathData="M3.871,3.877L24.796,3.877A0.947,0.947 0,0 1,25.744 4.824L25.744,24.834A0.947,0.947 0,0 1,24.796 25.782L3.871,25.782A0.947,0.947 0,0 1,2.924 24.834L2.924,4.824A0.947,0.947 0,0 1,3.871 3.877z"
android:strokeWidth="3.57115"
android:strokeColor="#00000000" />
<path
android:fillColor="#9f9f9f"
android:pathData="M3.879,3.035L24.804,3.035A0.947,0.947 0,0 1,25.751 3.982L25.751,23.992A0.947,0.947 0,0 1,24.804 24.94L3.879,24.94A0.947,0.947 0,0 1,2.932 23.992L2.932,3.982A0.947,0.947 0,0 1,3.879 3.035z"
android:strokeWidth="3.57115"
android:strokeColor="#00000000" />
<path
android:fillColor="#8a8a8a"
android:pathData="M3.871,3.413L24.796,3.413A0.947,0.947 0,0 1,25.743 4.36L25.743,24.37A0.947,0.947 0,0 1,24.796 25.318L3.871,25.318A0.947,0.947 0,0 1,2.924 24.37L2.924,4.36A0.947,0.947 0,0 1,3.871 3.413z"
android:strokeWidth="3.57115"
android:strokeColor="#00000000" />
<path
android:fillColor="#ffffff"
android:pathData="m12.078,22.988c-0.077,0 -0.123,-0.015 -0.185,-0.046 -0.277,-0.185 -1.584,-1.153 -0.492,-3.075 0.292,-0.523 0.369,-0.938 0.261,-1.276 -0.123,-0.384 -0.523,-0.538 -0.523,-0.538 -0.185,-0.077 -0.277,-0.292 -0.2,-0.477 0.062,-0.154 0.185,-0.246 0.338,-0.246 0.046,0 0.092,0.015 0.138,0.031 0.062,0 0.707,0.277 0.969,0.969 0.2,0.554 0.092,1.199 -0.323,1.922 -0.769,1.353 0.046,1.953 0.215,2.06 0.169,0.092 0.231,0.323 0.138,0.507 -0.092,0.123 -0.231,0.169 -0.338,0.169zM14.569,22.173c-0.077,0 -0.123,-0.015 -0.185,-0.046 -0.554,-0.323 -1.338,-1.322 -0.461,-2.891 0.261,-0.477 0.338,-0.861 0.231,-1.153 -0.108,-0.338 -0.461,-0.477 -0.461,-0.477 -0.2,-0.077 -0.292,-0.277 -0.215,-0.477 0.062,-0.154 0.185,-0.246 0.338,-0.246 0.046,0 0.092,0.015 0.138,0.031 0.154,0.062 0.692,0.308 0.907,0.923 0.185,0.523 0.092,1.122 -0.292,1.784 -0.738,1.322 0.154,1.86 0.2,1.876 0.077,0.046 0.138,0.123 0.169,0.215 0.031,0.092 0.015,0.2 -0.031,0.292 -0.092,0.123 -0.231,0.169 -0.338,0.169zM9.464,22.173c-0.077,0 -0.123,-0.015 -0.185,-0.046C8.726,21.804 7.942,20.804 8.819,19.236c0.261,-0.477 0.338,-0.861 0.231,-1.153 -0.108,-0.338 -0.461,-0.477 -0.461,-0.477 -0.2,-0.077 -0.292,-0.277 -0.215,-0.477 0.062,-0.154 0.185,-0.246 0.338,-0.246 0.046,0 0.092,0.015 0.138,0.031 0.154,0.062 0.692,0.323 0.907,0.923 0.185,0.523 0.077,1.122 -0.292,1.799 -0.738,1.322 0.154,1.86 0.2,1.876 0.077,0.046 0.138,0.123 0.169,0.215 0.031,0.092 0.015,0.2 -0.031,0.292 -0.077,0.108 -0.231,0.154 -0.338,0.154zM18.828,20.804c-0.123,0 -0.215,-0.046 -0.292,-0.123 -0.077,-0.077 -0.123,-0.185 -0.123,-0.292 0,-0.169 0.108,-0.323 0.261,-0.4l0.015,0l0,-0.83l-0.784,0.446l0,0.015c0,0.123 -0.046,0.277 -0.2,0.354 -0.062,0.046 -0.138,0.062 -0.215,0.062 -0.154,0 -0.292,-0.077 -0.354,-0.2 -0.062,-0.108 -0.077,-0.215 -0.046,-0.323 0.031,-0.108 0.092,-0.2 0.2,-0.246 0.062,-0.046 0.138,-0.062 0.2,-0.062 0.077,0 0.154,0.015 0.215,0.062l0.707,-0.4 -0.692,-0.4c-0.092,0.031 -0.169,0.046 -0.231,0.046 -0.077,0 -0.138,-0.015 -0.2,-0.046 -0.185,-0.123 -0.261,-0.384 -0.154,-0.569 0.077,-0.123 0.215,-0.2 0.369,-0.2 0.077,0 0.154,0.015 0.215,0.062 0.123,0.077 0.2,0.2 0.2,0.354l0,0.015L18.705,18.575l0,-0.83l-0.015,0c-0.154,-0.077 -0.261,-0.231 -0.261,-0.4 0,-0.231 0.185,-0.415 0.415,-0.415 0.123,0 0.231,0.046 0.308,0.123 0.077,0.077 0.123,0.185 0.123,0.292 0,0.138 -0.077,0.277 -0.215,0.354L19.012,17.698L19.012,18.498l0.677,-0.384l0,-0.015c0,-0.123 0.046,-0.277 0.2,-0.354 0.062,-0.046 0.138,-0.062 0.215,-0.062 0.154,0 0.292,0.077 0.354,0.2 0.062,0.108 0.077,0.215 0.046,0.323 -0.031,0.108 -0.092,0.2 -0.2,0.246 -0.062,0.046 -0.138,0.062 -0.2,0.062 -0.077,0 -0.154,-0.015 -0.215,-0.062l-0.707,0.4 0.692,0.4c0.092,-0.031 0.169,-0.046 0.231,-0.046 0.077,0 0.138,0.015 0.2,0.046 0.185,0.123 0.261,0.384 0.154,0.569 -0.077,0.123 -0.215,0.2 -0.369,0.2 -0.077,0 -0.154,-0.015 -0.215,-0.062 -0.123,-0.077 -0.2,-0.2 -0.2,-0.354l0,-0.015L19.012,19.221l0,0.8l0.015,0c0.138,0.062 0.215,0.2 0.215,0.354 0,0.246 -0.185,0.431 -0.415,0.431zM7.635,15.946C6.466,15.946 5.528,15.008 5.528,13.839L5.528,8.827C5.528,7.658 6.466,6.721 7.635,6.721l13.868,0c1.169,0 2.106,0.938 2.106,2.106l0,5.012c0,1.169 -0.938,2.106 -2.106,2.106zM7.419,7.643c-0.554,0 -0.999,0.461 -0.999,1.046L6.42,14.039c0,0.569 0.446,1.046 0.999,1.046L21.703,15.085c0.554,0 0.999,-0.461 0.999,-1.046L22.702,8.689c0,-0.569 -0.446,-1.046 -0.999,-1.046zM7.835,13.639c-0.2,0 -0.369,-0.169 -0.369,-0.369 0,-0.2 0.169,-0.369 0.369,-0.369l13.115,0c0.2,0 0.369,0.169 0.369,0.369 0,0.2 -0.169,0.369 -0.369,0.369zM7.835,12.087c-0.2,0 -0.369,-0.169 -0.369,-0.369 0,-0.2 0.169,-0.369 0.369,-0.369l13.115,0c0.2,0 0.369,0.169 0.369,0.369 0,0.2 -0.169,0.369 -0.369,0.369z" />
</vector>

View File

@ -0,0 +1,125 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.discovery.DiscoveryActivityV2">
<ScrollView
android:id="@+id/gree_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/about_margin"
android:paddingRight="@dimen/about_margin"
tools:context="nodomain.freeyourgadget.gadgetbridge.activities.OpenFwAppInstallerActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/gree_pair_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:text="@string/gree_pair_info"
android:textIsSelectable="true" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/gree_pair_ssid_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/gree_pair_ssid_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:hint="@string/wifi_ssid"
android:inputType="textFilter|textNoSuggestions"
android:maxLength="32"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/gree_pair_password_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/gree_pair_password_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:autofillHints="wifiPassword"
android:hint="@string/prefs_password"
android:inputType="textPassword"
android:maxLength="63"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/gree_pair_host_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/gree_pair_host_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:hint="@string/gree_controller_host"
android:inputType="textFilter|textNoSuggestions"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/gree_button_pair"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:text="@string/discovery_yes_pair" />
<ProgressBar
android:id="@+id/gree_pair_progress_bar"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:layout_weight="1"
android:visibility="gone" />
<TextView
android:id="@+id/gree_pair_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:gravity="center"
android:text="@string/stats_empty_value"
android:textIsSelectable="true"
android:visibility="gone" />
<Button
android:id="@+id/gree_button_copy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/copy_to_clipboard"
android:visibility="gone" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -1358,6 +1358,7 @@
<string name="discovery_successfully_bonded">Bound to %1$s.</string>
<string name="discovery_pair_title">Pair with %1$s?</string>
<string name="discovery_pair_question">Select Pair to pair your devices. If this fails, try again without pairing.</string>
<string name="copy_to_clipboard">Copy</string>
<string name="discovery_yes_pair">Pair</string>
<string name="discovery_dont_pair">Don\'t Pair</string>
<!-- strings sent to pebble watches for quick actions -->
@ -1814,6 +1815,7 @@
<string name="devicetype_garmin_vivoactive_5">Garmin Vívoactive 5</string>
<string name="devicetype_garmin_vivosmart_5">Garmin Vívosmart 5</string>
<string name="devicetype_garmin_vivosport">Garmin Vívosport</string>
<string name="devicetype_gree_ac">Gree Air Conditioner</string>
<string name="devicetype_vibratissimo">Vibratissimo</string>
<string name="devicetype_um25">UM-25</string>
<string name="devicetype_liveview">LiveView</string>
@ -3631,4 +3633,9 @@
<string name="pref_garmin_fake_oauth_warning">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.</string>
<string name="pref_garmin_fake_oauth_title">Send fake OAuth responses</string>
<string name="pref_garmin_fake_oauth_summary">Fixes some functions such as weather and AGPS updates without connecting to the official app every 90 days.</string>
<string name="gree_controller_host">Host</string>
<string name="gree_pair_info">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.</string>
<string name="gree_pair_status_success">Pairing success.\n\nBind key: %s</string>
<string name="gree_pair_status_failure">Pairing failed: %s</string>
<string name="gree_pair_clipboard">Device name: %s\nMac address: %s\nBind key: %s</string>
</resources>