1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-13 16:40:50 +02:00

Mi Band 8: Initial support (WIP)

This commit is contained in:
José Rebelo 2023-10-02 09:48:10 +01:00
parent fac566c7da
commit fda3b53657
22 changed files with 2961 additions and 1 deletions

View File

@ -292,6 +292,10 @@ dependencies {
implementation 'com.google.protobuf:protobuf-javalite:3.21.7'
implementation 'com.android.volley:volley:1.2.1'
// TODO pull just the needed classes into GB?
implementation 'org.bouncycastle:bcpkix-jdk15to18:1.71'
implementation 'org.bouncycastle:bcprov-jdk15to18:1.71'
// NON-FOSS dependencies
// implementation('androidx.core:core-google-shortcuts:1.0.1') {
// exclude group:'com.google.android.gms'

View File

@ -125,7 +125,7 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
}
@Override
public void deleteDevice(final GBDevice gbDevice) throws GBException {
public final void deleteDevice(final GBDevice gbDevice) throws GBException {
LOG.info("will try to delete device: " + gbDevice.getName());
if (gbDevice.isConnected() || gbDevice.isConnecting()) {
GBApplication.deviceService(gbDevice).disconnect();

View File

@ -0,0 +1,28 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
public class XiaomiActivitySummaryParser implements ActivitySummaryParser {
@Override
public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary) {
// TODO parse it
return summary;
}
}

View File

@ -0,0 +1,438 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
import android.app.Activity;
import android.bluetooth.le.ScanFilter;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.ArrayUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.capabilities.HeartRateCapability;
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPairingActivity;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.AbstractNotificationPattern;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.PaiSample;
import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiLanguageType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
@NonNull
@Override
public Collection<? extends ScanFilter> createBLEScanFilters() {
return super.createBLEScanFilters();
}
@Override
protected void deleteDevice(@NonNull final GBDevice gbDevice,
@NonNull final Device device,
@NonNull final DaoSession session) throws GBException {
// TODO, and fix on zepp
}
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(final GBDevice device, DaoSession session) {
return new XiaomiSampleProvider(device, session);
}
@Override
public TimeSampleProvider<? extends StressSample> getStressSampleProvider(final GBDevice device, final DaoSession session) {
// TODO XiaomiStressSampleProvider
return super.getStressSampleProvider(device, session);
}
@Override
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(final GBDevice device, final DaoSession session) {
// TODO XiaomiSpo2SampleProvider
return super.getSpo2SampleProvider(device, session);
}
@Override
public TimeSampleProvider<? extends HeartRateSample> getHeartRateMaxSampleProvider(final GBDevice device, final DaoSession session) {
// TODO XiaomiHeartRateMaxSampleProvider
return super.getHeartRateMaxSampleProvider(device, session);
}
@Override
public TimeSampleProvider<? extends HeartRateSample> getHeartRateRestingSampleProvider(final GBDevice device, final DaoSession session) {
// TODO XiaomiHeartRateRestingSampleProvider
return super.getHeartRateRestingSampleProvider(device, session);
}
@Override
public TimeSampleProvider<? extends HeartRateSample> getHeartRateManualSampleProvider(final GBDevice device, final DaoSession session) {
// TODO XiaomiHeartRateManualSampleProvider
return super.getHeartRateManualSampleProvider(device, session);
}
@Override
public TimeSampleProvider<? extends PaiSample> getPaiSampleProvider(final GBDevice device, final DaoSession session) {
// TODO XiaomiPaiSampleProvider
return super.getPaiSampleProvider(device, session);
}
@Override
public TimeSampleProvider<? extends SleepRespiratoryRateSample> getSleepRespiratoryRateSampleProvider(final GBDevice device, final DaoSession session) {
// TODO XiaomiSleepRespiratoryRateSampleProvider
return super.getSleepRespiratoryRateSampleProvider(device, session);
}
@Nullable
@Override
public ActivitySummaryParser getActivitySummaryParser(final GBDevice device) {
return new XiaomiActivitySummaryParser();
}
@Override
public boolean supportsFlashing() {
return true;
}
@Override
public int getAlarmSlotCount(final GBDevice device) {
// TODO the watch returns the slot count
return 10;
}
@Override
public boolean supportsSmartWakeup(final GBDevice device) {
return true;
}
@Override
public boolean supportsAppsManagement(final GBDevice device) {
// TODO maybe for watchfaces or widgets?
return super.supportsAppsManagement(device);
}
@Override
public int getBondingStyle() {
return BONDING_STYLE_REQUIRE_KEY;
}
@Override
public boolean supportsCalendarEvents() {
return true;
}
@Override
public boolean supportsActivityDataFetching() {
return true;
}
@Override
public boolean supportsActivityTracking() {
return true;
}
@Override
public boolean supportsActivityTracks() {
return true;
}
@Override
public boolean supportsStressMeasurement() {
return true;
}
@Override
public boolean supportsSpo2() {
return true;
}
@Override
public boolean supportsHeartRateStats() {
// TODO does it?
return true;
}
@Override
public boolean supportsPai() {
// TODO does it?
return true;
}
@Override
public boolean supportsSleepRespiratoryRate() {
// TODO does it?
return true;
}
@Override
public boolean supportsAlarmSnoozing() {
// TODO does it?
return false;
}
@Override
public boolean supportsAlarmDescription(final GBDevice device) {
// TODO does it?
return false;
}
@Override
public boolean supportsMusicInfo() {
return true;
}
@Override
public int getMaximumReminderMessageLength() {
// TODO does it?
return 0;
}
@Override
public int getReminderSlotCount(final GBDevice device) {
// TODO Does it?
return 0;
}
@Override
public int getWorldClocksSlotCount() {
// TODO how many?
return 5;
}
@Override
public int getWorldClocksLabelLength() {
// TODO how much?
return 5;
}
@Override
public boolean supportsDisabledWorldClocks() {
// TODO does it?
return false;
}
@Override
public boolean supportsHeartRateMeasurement(final GBDevice device) {
return true;
}
@Override
public boolean supportsManualHeartRateMeasurement(final GBDevice device) {
// TODO orchestrate
return true;
}
@Override
public String getManufacturer() {
return "Xiaomi";
}
@Override
public boolean supportsRealtimeData() {
// TODO supports steps?
return true;
}
@Override
public boolean supportsRemSleep() {
return true;
}
@Override
public boolean supportsWeather() {
return true;
}
@Override
public boolean supportsFindDevice() {
return true;
}
@Override
public boolean supportsUnicodeEmojis() {
return true;
}
@Override
public int[] getSupportedDeviceSpecificSettings(final GBDevice device) {
final List<Integer> settings = new ArrayList<>();
// TODO review this
//
// Time
//
settings.add(R.xml.devicesettings_header_time);
settings.add(R.xml.devicesettings_timeformat);
settings.add(R.xml.devicesettings_dateformat_2);
if (getWorldClocksSlotCount() > 0) {
settings.add(R.xml.devicesettings_world_clocks);
}
//
// Display
//
settings.add(R.xml.devicesettings_header_display);
settings.add(R.xml.devicesettings_huami2021_displayitems);
settings.add(R.xml.devicesettings_huami2021_shortcuts);
settings.add(R.xml.devicesettings_nightmode);
settings.add(R.xml.devicesettings_sleep_mode);
settings.add(R.xml.devicesettings_liftwrist_display_sensitivity_with_smart);
settings.add(R.xml.devicesettings_password);
settings.add(R.xml.devicesettings_always_on_display);
settings.add(R.xml.devicesettings_screen_timeout);
//
// Health
//
settings.add(R.xml.devicesettings_header_health);
settings.add(R.xml.devicesettings_heartrate_sleep_alert_activity_stress_spo2);
settings.add(R.xml.devicesettings_inactivity_dnd_no_threshold);
settings.add(R.xml.devicesettings_goal_notification);
//
// Workout
//
settings.add(R.xml.devicesettings_header_workout);
settings.add(R.xml.devicesettings_workout_start_on_phone);
settings.add(R.xml.devicesettings_workout_send_gps_to_band);
//
// Notifications
//
settings.add(R.xml.devicesettings_header_notifications);
settings.add(R.xml.devicesettings_display_caller);
settings.add(R.xml.devicesettings_vibrationpatterns);
settings.add(R.xml.devicesettings_donotdisturb_withauto_and_always);
settings.add(R.xml.devicesettings_screen_on_on_notifications);
settings.add(R.xml.devicesettings_autoremove_notifications);
settings.add(R.xml.devicesettings_canned_reply_16);
//
// Calendar
//
settings.add(R.xml.devicesettings_header_calendar);
settings.add(R.xml.devicesettings_sync_calendar);
//
// Other
//
settings.add(R.xml.devicesettings_header_other);
settings.add(R.xml.devicesettings_camera_remote);
return ArrayUtils.toPrimitive(settings.toArray(new Integer[0]));
}
@Override
public int[] getSupportedDeviceSpecificAuthenticationSettings() {
return new int[]{R.xml.devicesettings_pairingkey};
}
@Override
public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) {
return new XiaomiSettingsCustomizer();
}
@Override
public String[] getSupportedLanguageSettings(final GBDevice device) {
// TODO check which are supported
final List<String> allLanguages = new ArrayList<>(HuamiLanguageType.idLookup.keySet());
allLanguages.add(0, "auto");
return allLanguages.toArray(new String[0]);
}
@Override
public Class<? extends Activity> getPairingActivity() {
return MiBandPairingActivity.class;
}
@Override
public PasswordCapabilityImpl.Mode getPasswordCapability() {
return PasswordCapabilityImpl.Mode.NUMBERS_6;
}
@Override
public List<HeartRateCapability.MeasurementInterval> getHeartRateMeasurementIntervals() {
return Arrays.asList(
HeartRateCapability.MeasurementInterval.OFF,
HeartRateCapability.MeasurementInterval.SMART,
HeartRateCapability.MeasurementInterval.MINUTES_1,
HeartRateCapability.MeasurementInterval.MINUTES_10,
HeartRateCapability.MeasurementInterval.MINUTES_30
);
}
@Override
public boolean supportsNotificationVibrationPatterns() {
// TODO maybe can used this
return true;
}
@Override
public boolean supportsNotificationVibrationRepetitionPatterns() {
// TODO maybe can used this
return true;
}
@Override
public boolean supportsNotificationLedPatterns() {
return false;
}
@Override
public AbstractNotificationPattern[] getNotificationVibrationPatterns() {
// TODO maybe can used this
return new AbstractNotificationPattern[0];
}
@Override
public AbstractNotificationPattern[] getNotificationVibrationRepetitionPatterns() {
// TODO maybe can used this
return new AbstractNotificationPattern[0];
}
@Override
public AbstractNotificationPattern[] getNotificationLedPatterns() {
return new AbstractNotificationPattern[0];
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return XiaomiSupport.class;
}
}

View File

@ -0,0 +1,79 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuamiExtendedActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
// TODO s/HuamiExtendedActivitySample/XiaomiActivitySample/g
public class XiaomiSampleProvider extends AbstractSampleProvider<HuamiExtendedActivitySample> {
protected XiaomiSampleProvider(final GBDevice device, final DaoSession session) {
super(device, session);
}
@Override
public AbstractDao<HuamiExtendedActivitySample, ?> getSampleDao() {
return getSession().getHuamiExtendedActivitySampleDao();
}
@Nullable
@Override
protected Property getRawKindSampleProperty() {
return HuamiExtendedActivitySampleDao.Properties.RawKind;
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return HuamiExtendedActivitySampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return HuamiExtendedActivitySampleDao.Properties.DeviceId;
}
@Override
public int normalizeType(final int rawType) {
// TODO
return rawType;
}
@Override
public int toRawActivityKind(final int activityKind) {
return activityKind;
}
@Override
public float normalizeIntensity(final int rawIntensity) {
return rawIntensity;
}
@Override
public HuamiExtendedActivitySample createActivitySample() {
return new HuamiExtendedActivitySample();
}
}

View File

@ -0,0 +1,64 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
import android.os.Parcel;
import androidx.preference.Preference;
import java.util.Collections;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class XiaomiSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
@Override
public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) {
}
@Override
public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs) {
}
@Override
public Set<String> getPreferenceKeysWithSummary() {
return Collections.emptySet();
}
public static final Creator<XiaomiSettingsCustomizer> CREATOR = new Creator<XiaomiSettingsCustomizer>() {
@Override
public XiaomiSettingsCustomizer createFromParcel(final Parcel in) {
return new XiaomiSettingsCustomizer();
}
@Override
public XiaomiSettingsCustomizer[] newArray(final int size) {
return new XiaomiSettingsCustomizer[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
}
}

View File

@ -0,0 +1,67 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class MiBand8Coordinator extends XiaomiCoordinator {
private final Pattern NAME_PATTTERN = Pattern.compile("^Xiaomi Smart Band 8 [A-Z0-9]{4}$");
@NonNull
@Override
public DeviceType getSupportedType(final GBDeviceCandidate candidate) {
if (NAME_PATTTERN.matcher(candidate.getName()).matches()) {
return DeviceType.MIBAND8;
}
return DeviceType.UNKNOWN;
}
@Nullable
@Override
public InstallHandler findInstallHandler(final Uri uri, final Context context) {
// TODO implement this
return super.findInstallHandler(uri, context);
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_miband8;
}
@Override
public int getDefaultIconResource() {
return R.drawable.ic_device_miband6;
}
@Override
public int getDisabledIconResource() {
return R.drawable.ic_device_miband6_disabled;
}
}

View File

@ -44,6 +44,7 @@ public class ActivityUser {
private int activityUserCaloriesBurntGoal;
private int activityUserDistanceGoalMeters;
private int activityUserActiveTimeGoalMinutes;
private int activityUserStandingTimeGoalHours;
private int activityUserStepLengthCm;
private static final String defaultUserName = "gadgetbridge-user";
@ -167,6 +168,7 @@ public class ActivityUser {
activityUserCaloriesBurntGoal = prefs.getInt(PREF_USER_CALORIES_BURNT, defaultUserCaloriesBurntGoal);
activityUserDistanceGoalMeters = prefs.getInt(PREF_USER_DISTANCE_METERS, defaultUserDistanceGoalMeters);
activityUserActiveTimeGoalMinutes = prefs.getInt(PREF_USER_ACTIVETIME_MINUTES, defaultUserActiveTimeGoalMinutes);
activityUserStandingTimeGoalHours = prefs.getInt(PREF_USER_GOAL_STANDING_TIME_HOURS, defaultUserGoalStandingTimeHours);
activityUserStepLengthCm = prefs.getInt(PREF_USER_STEP_LENGTH_CM, defaultUserStepLengthCm);
}
@ -199,4 +201,12 @@ public class ActivityUser {
}
return activityUserActiveTimeGoalMinutes;
}
public int getStandingTimeGoalHours()
{
if (activityUserStandingTimeGoalHours < 1) {
activityUserStandingTimeGoalHours = defaultUserGoalStandingTimeHours;
}
return activityUserStandingTimeGoalHours;
}
}

View File

@ -33,6 +33,8 @@ public interface Alarm extends Serializable {
byte ALARM_SAT = 32;
byte ALARM_SUN = 64;
byte ALARM_DAILY = Alarm.ALARM_MON | Alarm.ALARM_TUE | Alarm.ALARM_WED | Alarm.ALARM_THU | Alarm.ALARM_FRI | Alarm.ALARM_SAT | Alarm.ALARM_SUN;
int getPosition();
boolean getEnabled();

View File

@ -140,6 +140,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveHrCoordin
import nodomain.freeyourgadget.gadgetbridge.devices.waspos.WaspOSCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsSteelHRDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.miband8.MiBand8Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xwatch.XWatchCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeCoordinator;
@ -192,6 +193,7 @@ public enum DeviceType {
AMAZFITPOP(AmazfitPopCoordinator.class),
AMAZFITPOPPRO(AmazfitPopProCoordinator.class),
MIBAND7(MiBand7Coordinator.class),
MIBAND8(MiBand8Coordinator.class),
AMAZFITGTS3(AmazfitGTS3Coordinator.class),
AMAZFITGTR3(AmazfitGTR3Coordinator.class),
AMAZFITGTR4(AmazfitGTR4Coordinator.class),

View File

@ -0,0 +1,307 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiConstants.UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE;
import android.content.SharedPreferences;
import android.os.Build;
import androidx.annotation.Nullable;
import com.google.protobuf.ByteString;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.modes.CCMBlockCipher;
import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Locale;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class XiaomiCipher {
private static final Logger LOG = LoggerFactory.getLogger(XiaomiCipher.class);
private final XiaomiSupport mSupport;
private final byte[] secretKey = new byte[16];
private final byte[] nonce = new byte[16];
private final byte[] encryptionKey = new byte[16];
private final byte[] decryptionKey = new byte[16];
private final byte[] encryptionNonce = new byte[4];
private final byte[] decryptionNonce = new byte[4];
public XiaomiCipher(final XiaomiSupport support) {
this.mSupport = support;
}
protected void startAuthentication(final TransactionBuilder builder) {
builder.add(new SetDeviceStateAction(mSupport.getDevice(), GBDevice.State.AUTHENTICATING, mSupport.getContext()));
System.arraycopy(getSecretKey(mSupport.getDevice()), 0, secretKey, 0, 16);
new SecureRandom().nextBytes(nonce);
builder.write(
mSupport.getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE),
ArrayUtils.addAll(XiaomiConstants.PAYLOAD_HEADER_AUTH, buildNonceCommand(nonce))
);
}
protected void handleAuthCommand(final XiaomiProto.Command cmd) {
if (cmd.getType() != XiaomiConstants.CMD_TYPE_AUTH) {
throw new IllegalArgumentException("Not an auth command");
}
switch (cmd.getSubtype()) {
case XiaomiConstants.CMD_AUTH_NONCE: {
LOG.debug("Got watch nonce");
// Watch nonce
final XiaomiProto.Command reply = handleWatchNonce(cmd.getAuth().getWatchNonce());
if (reply == null) {
mSupport.disconnect();
return;
}
final TransactionBuilder builder = mSupport.createTransactionBuilder("auth step 2");
// TODO maybe move these writes to support class?
builder.write(
mSupport.getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE),
ArrayUtils.addAll(XiaomiConstants.PAYLOAD_HEADER_AUTH, reply.toByteArray())
);
builder.queue(mSupport.getQueue());
break;
}
case XiaomiConstants.CMD_AUTH_AUTH: {
LOG.info("Authenticated!");
final TransactionBuilder builder = mSupport.createTransactionBuilder("phase 2 initialize");
builder.add(new SetDeviceStateAction(mSupport.getDevice(), GBDevice.State.INITIALIZED, mSupport.getContext()));
mSupport.phase2Initialize(builder);
builder.queue(mSupport.getQueue());
break;
}
default:
LOG.warn("Unknown auth payload subtype {}", cmd.getSubtype());
}
}
public byte[] encrypt(final byte[] arr, final short i) {
final ByteBuffer packetNonce = ByteBuffer.allocate(12).order(ByteOrder.LITTLE_ENDIAN)
.put(encryptionNonce)
.putInt(0)
.putShort(i) // TODO what happens once this overflows?
.putShort((short) 0);
try {
return encrypt(encryptionKey, packetNonce.array(), arr);
} catch (final CryptoException e) {
throw new RuntimeException("failed to encrypt", e);
}
}
public byte[] decrypt(final byte[] arr) {
final ByteBuffer packetNonce = ByteBuffer.allocate(12).order(ByteOrder.LITTLE_ENDIAN);
packetNonce.put(decryptionNonce);
packetNonce.putInt(0);
packetNonce.putInt(0);
try {
return decrypt(decryptionKey, packetNonce.array(), arr);
} catch (final CryptoException e) {
throw new RuntimeException("failed to decrypt", e);
}
}
@Nullable
private XiaomiProto.Command handleWatchNonce(final XiaomiProto.WatchNonce watchNonce) {
final byte[] step2hmac = computeAuthStep3Hmac(secretKey, nonce, watchNonce.getNonce().toByteArray());
System.arraycopy(step2hmac, 0, decryptionKey, 0, 16);
System.arraycopy(step2hmac, 16, encryptionKey, 0, 16);
System.arraycopy(step2hmac, 32, decryptionNonce, 0, 4);
System.arraycopy(step2hmac, 36, encryptionNonce, 0, 4);
if (BuildConfig.DEBUG) {
LOG.debug("decryptionKey: {}", GB.hexdump(decryptionKey));
LOG.debug("encryptionKey: {}", GB.hexdump(encryptionKey));
LOG.debug("decryptionNonce: {}", GB.hexdump(decryptionNonce));
LOG.debug("encryptionNonce: {}", GB.hexdump(encryptionNonce));
}
final byte[] decryptionConfirmation = hmacSHA256(decryptionKey, ArrayUtils.addAll(watchNonce.getNonce().toByteArray(), nonce));
if (!Arrays.equals(decryptionConfirmation, watchNonce.getHmac().toByteArray())) {
LOG.warn("Watch hmac mismatch");
return null;
}
final XiaomiProto.AuthDeviceInfo authDeviceInfo = XiaomiProto.AuthDeviceInfo.newBuilder()
.setUnknown1(0) // TODO ?
.setPhoneApiLevel(Build.VERSION.SDK_INT)
.setPhoneName(Build.MODEL)
.setUnknown3(224) // TODO ?
// TODO region should be actual device region?
.setRegion(Locale.getDefault().getLanguage().substring(0, 2).toUpperCase(Locale.ROOT))
.build();
final byte[] encryptedNonces = hmacSHA256(encryptionKey, ArrayUtils.addAll(nonce, watchNonce.getNonce().toByteArray()));
final byte[] encryptedDeviceInfo = encrypt(authDeviceInfo.toByteArray(), (short) 0);
final XiaomiProto.AuthStep3 authStep3 = XiaomiProto.AuthStep3.newBuilder()
.setEncryptedNonces(ByteString.copyFrom(encryptedNonces))
.setEncryptedDeviceInfo(ByteString.copyFrom(encryptedDeviceInfo))
.build();
final XiaomiProto.Command.Builder cmd = XiaomiProto.Command.newBuilder();
cmd.setType(XiaomiConstants.CMD_TYPE_AUTH);
cmd.setSubtype(XiaomiConstants.CMD_AUTH_AUTH);
final XiaomiProto.Auth.Builder auth = XiaomiProto.Auth.newBuilder();
auth.setAuthStep3(authStep3);
return cmd.setAuth(auth.build()).build();
}
public static byte[] buildNonceCommand(final byte[] nonce) {
final XiaomiProto.PhoneNonce.Builder phoneNonce = XiaomiProto.PhoneNonce.newBuilder();
phoneNonce.setNonce(ByteString.copyFrom(nonce));
final XiaomiProto.Auth.Builder auth = XiaomiProto.Auth.newBuilder();
auth.setPhoneNonce(phoneNonce.build());
final XiaomiProto.Command.Builder command = XiaomiProto.Command.newBuilder();
command.setType(XiaomiConstants.CMD_TYPE_AUTH);
command.setSubtype(XiaomiConstants.CMD_AUTH_NONCE);
command.setAuth(auth.build());
return command.build().toByteArray();
}
public static byte[] computeAuthStep3Hmac(final byte[] secretKey,
final byte[] phoneNonce,
final byte[] watchNonce) {
final byte[] miwearAuthBytes = "miwear-auth".getBytes();
final Mac mac;
try {
mac = Mac.getInstance("HmacSHA256");
// Compute the actual key and re-initialize the mac
mac.init(new SecretKeySpec(ArrayUtils.addAll(phoneNonce, watchNonce), "HmacSHA256"));
final byte[] hmacKeyBytes = mac.doFinal(secretKey);
final SecretKeySpec key = new SecretKeySpec(hmacKeyBytes, "HmacSHA256");
mac.init(key);
} catch (final NoSuchAlgorithmException | InvalidKeyException e) {
throw new IllegalStateException("Failed to initialize hmac for auth step 2", e);
}
final byte[] output = new byte[64];
byte[] tmp = new byte[0];
byte b = 1;
int i = 0;
while (i < output.length) {
mac.update(tmp);
mac.update(miwearAuthBytes);
mac.update(b);
tmp = mac.doFinal();
for (int j = 0; j < tmp.length && i < output.length; j++, i++) {
output[i] = tmp[j];
}
b++;
}
return output;
}
protected static byte[] getSecretKey(final GBDevice device) {
final byte[] authKeyBytes = new byte[16];
final SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress());
final String authKey = sharedPrefs.getString("authkey", null);
if (StringUtils.isNotBlank(authKey)) {
final byte[] srcBytes;
// Allow both with and without 0x, to avoid user mistakes
if (authKey.length() == 34 && authKey.startsWith("0x")) {
srcBytes = GB.hexStringToByteArray(authKey.trim().substring(2));
} else {
srcBytes = GB.hexStringToByteArray(authKey.trim());
}
System.arraycopy(srcBytes, 0, authKeyBytes, 0, Math.min(srcBytes.length, 16));
}
return authKeyBytes;
}
protected static byte[] hmacSHA256(final byte[] key, final byte[] input) {
try {
final Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(input);
} catch (final Exception e) {
throw new RuntimeException("Failed to hmac", e);
}
}
public static byte[] encrypt(final byte[] key, final byte[] nonce, final byte[] payload) throws
CryptoException {
final CCMBlockCipher cipher = createBlockCipher(true, new SecretKeySpec(key, "AES"), nonce);
final byte[] out = new byte[cipher.getOutputSize(payload.length)];
final int outBytes = cipher.processBytes(payload, 0, payload.length, out, 0);
cipher.doFinal(out, outBytes);
return out;
}
public static byte[] decrypt(final byte[] key,
final byte[] nonce,
final byte[] encryptedPayload) throws CryptoException {
final CCMBlockCipher cipher = createBlockCipher(false, new SecretKeySpec(key, "AES"), nonce);
final byte[] decrypted = new byte[cipher.getOutputSize(encryptedPayload.length)];
cipher.doFinal(decrypted, cipher.processBytes(encryptedPayload, 0, encryptedPayload.length, decrypted, 0));
return decrypted;
}
public static CCMBlockCipher createBlockCipher(final boolean forEncrypt,
final SecretKey secretKey,
final byte[] nonce) {
final AESEngine aesFastEngine = new AESEngine();
aesFastEngine.init(forEncrypt, new KeyParameter(secretKey.getEncoded()));
final CCMBlockCipher blockCipher = new CCMBlockCipher(aesFastEngine);
blockCipher.init(forEncrypt, new AEADParameters(new KeyParameter(secretKey.getEncoded()), 32, nonce, null));
return blockCipher;
}
}

View File

@ -0,0 +1,56 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi;
import java.util.UUID;
public class XiaomiConstants {
public static final String BASE_UUID = "0000%s-0000-1000-8000-00805f9b34fb";
public static final UUID UUID_SERVICE_XIAOMI_FE95 = UUID.fromString((String.format(BASE_UUID, "fe95")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0050 = UUID.fromString((String.format(BASE_UUID, "0050")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ = UUID.fromString((String.format(BASE_UUID, "0051")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE = UUID.fromString((String.format(BASE_UUID, "0052")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_ACTIVITY_DATA = UUID.fromString((String.format(BASE_UUID, "0053")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0054 = UUID.fromString((String.format(BASE_UUID, "0054")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_WATCHFACE = UUID.fromString((String.format(BASE_UUID, "0055")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0056 = UUID.fromString((String.format(BASE_UUID, "0056")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0057 = UUID.fromString((String.format(BASE_UUID, "0057")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0058 = UUID.fromString((String.format(BASE_UUID, "0058")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0059 = UUID.fromString((String.format(BASE_UUID, "0059")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_005A = UUID.fromString((String.format(BASE_UUID, "005a")));
public static final UUID UUID_SERVICE_XIAOMI_FDAB = UUID.fromString((String.format(BASE_UUID, "fdab")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0001 = UUID.fromString((String.format(BASE_UUID, "0001")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0002 = UUID.fromString((String.format(BASE_UUID, "0002")));
public static final UUID UUID_CHARACTERISTIC_XIAOMI_UNKNOWN_0003 = UUID.fromString((String.format(BASE_UUID, "0003")));
public static final int CMD_TYPE_AUTH = 1;
public static final int CMD_TYPE_SYSTEM = 2;
public static final int CMD_AUTH_NONCE = 26;
public static final int CMD_AUTH_AUTH = 27;
public static final int CMD_SYSTEM_BATTERY = 1;
public static final int CMD_SYSTEM_DEVICE_INFO = 2;
public static final int CMD_SYSTEM_CLOCK = 3;
public static final int CMD_SYSTEM_CHARGER = 79;
// TODO not like this
public static final byte[] PAYLOAD_ACK = new byte[]{0, 0, 3, 0};
public static final byte[] PAYLOAD_HEADER_AUTH = new byte[]{0, 0, 2, 2};
}

View File

@ -0,0 +1,565 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiConstants.*;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Context;
import android.location.Location;
import android.net.Uri;
import android.widget.Toast;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Reminder;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.AbstractXiaomiService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiHealthService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiMusicService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiNotificationService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiScheduleService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiSystemService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services.XiaomiWeatherService;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class XiaomiSupport extends AbstractBTLEDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(XiaomiSupport.class);
private final XiaomiCipher cipher = new XiaomiCipher(this);
private final XiaomiMusicService musicService = new XiaomiMusicService(this);
private final XiaomiHealthService healthService = new XiaomiHealthService(this);
private final XiaomiNotificationService notificationService = new XiaomiNotificationService(this);
private final XiaomiScheduleService scheduleService = new XiaomiScheduleService(this);
private final XiaomiWeatherService weatherService = new XiaomiWeatherService(this);
private final XiaomiSystemService systemService = new XiaomiSystemService(this);
private final Map<Integer, AbstractXiaomiService> mServiceMap = new LinkedHashMap<Integer, AbstractXiaomiService>() {{
put(XiaomiMusicService.COMMAND_TYPE, musicService);
put(XiaomiHealthService.COMMAND_TYPE, healthService);
put(XiaomiNotificationService.COMMAND_TYPE, notificationService);
put(XiaomiScheduleService.COMMAND_TYPE, scheduleService);
put(XiaomiWeatherService.COMMAND_TYPE, weatherService);
put(XiaomiSystemService.COMMAND_TYPE, systemService);
}};
public XiaomiSupport() {
super(LOG);
addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
addSupportedService(GattService.UUID_SERVICE_HUMAN_INTERFACE_DEVICE);
addSupportedService(UUID_SERVICE_XIAOMI_FE95);
addSupportedService(UUID_SERVICE_XIAOMI_FDAB);
}
@Override
public boolean useAutoConnect() {
return true;
}
@Override
public boolean getImplicitCallbackModify() {
return false;
}
@Override
public void setContext(final GBDevice gbDevice, final BluetoothAdapter btAdapter, final Context context) {
super.setContext(gbDevice, btAdapter, context);
for (final AbstractXiaomiService service : mServiceMap.values()) {
service.setContext(context);
}
}
@Override
protected TransactionBuilder initializeDevice(final TransactionBuilder builder) {
final BluetoothGattCharacteristic characteristicCommandWrite = getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE);
final BluetoothGattCharacteristic characteristicCommandRead = getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ);
if (characteristicCommandWrite == null || characteristicCommandRead == null) {
LOG.warn("Command characteristics are null, will attempt to reconnect");
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.WAITING_FOR_RECONNECT, getContext()));
return builder;
}
// FIXME why is this needed?
getDevice().setFirmwareVersion("...");
//getDevice().setFirmwareVersion2("...");
builder.notify(getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ), true);
builder.notify(getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE), true);
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
cipher.startAuthentication(builder);
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_XIAOMI_COMMAND_WRITE.equals(characteristicUUID)) {
if (Arrays.equals(value, PAYLOAD_ACK)) {
LOG.debug("Got command write ack");
} else {
LOG.warn("Unexpected notification from command write: {}", GB.hexdump(value));
}
return true;
}
if (UUID_CHARACTERISTIC_XIAOMI_COMMAND_READ.equals(characteristicUUID)) {
sendAck(characteristic);
final int header = BLETypeConversions.toUint16(value, 0);
final int type = BLETypeConversions.toUnsigned(value, 2);
final int encryption = BLETypeConversions.toUnsigned(value, 3);
if (header != 0) {
LOG.warn("Non-zero header not supported");
return true;
}
if (type != 2) {
LOG.warn("Unsupported type {}", type);
return true;
}
final byte[] plainValue;
if (encryption == 1) {
plainValue = cipher.decrypt(ArrayUtils.subarray(value, 4, value.length));
} else {
plainValue = ArrayUtils.subarray(value, 4, value.length);
}
LOG.debug("Got command: {}", GB.hexdump(plainValue));
final XiaomiProto.Command cmd;
try {
cmd = XiaomiProto.Command.parseFrom(plainValue);
} catch (final Exception e) {
LOG.error("Failed to parse bytes as protobuf command payload", e);
return true;
}
final AbstractXiaomiService service = mServiceMap.get(cmd.getType());
if (service != null) {
service.handleCommand(cmd);
return true;
}
if (cmd.getType() == CMD_TYPE_AUTH) {
cipher.handleAuthCommand(cmd);
return true;
}
LOG.warn("Unexpected watch command type {}", cmd.getType());
return true;
}
LOG.warn("Unhandled characteristic changed: {} {}", characteristicUUID, GB.hexdump(value));
return false;
}
@Override
public void onSendConfiguration(final String config) {
// TODO
// TODO user info
// TODO 24h
GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR);
}
@Override
public void onSetTime() {
final TransactionBuilder builder;
try {
builder = performInitialized("set time");
} catch (final IOException e) {
LOG.error("Failed to initialize transaction builder", e);
return;
}
setCurrentTime(builder);
builder.queue(getQueue());
}
public void setCurrentTime(final TransactionBuilder builder) {
final Calendar now = GregorianCalendar.getInstance();
final TimeZone tz = TimeZone.getDefault();
final GBPrefs gbPrefs = new GBPrefs(new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress())));
final String timeFormat = gbPrefs.getTimeFormat();
final boolean is24hour = DeviceSettingsPreferenceConst.PREF_TIMEFORMAT_24H.equals(timeFormat);
final XiaomiProto.Clock clock = XiaomiProto.Clock.newBuilder()
.setTime(XiaomiProto.Time.newBuilder()
.setHour(now.get(Calendar.HOUR_OF_DAY))
.setMinute(now.get(Calendar.MINUTE))
.setSecond(now.get(Calendar.SECOND))
.setMillisecond(now.get(Calendar.MILLISECOND))
.build())
.setDate(XiaomiProto.Date.newBuilder()
.setYear(now.get(Calendar.YEAR))
.setMonth(now.get(Calendar.MONTH) + 1)
.setDay(now.get(Calendar.DATE))
.build())
.setTimezone(XiaomiProto.TimeZone.newBuilder()
.setZoneOffset(((now.get(Calendar.ZONE_OFFSET) / 1000) / 60) / 15)
.setDstOffset(((now.get(Calendar.DST_OFFSET) / 1000) / 60) / 15)
.setName(tz.getID())
.build())
.setIsNot24Hour(!is24hour)
.build();
sendCommand(
builder,
XiaomiProto.Command.newBuilder()
.setType(CMD_TYPE_SYSTEM)
.setSubtype(CMD_SYSTEM_CLOCK)
.setSystem(XiaomiProto.System.newBuilder().setClock(clock).build())
.build()
);
}
@Override
public void onTestNewFunction() {
final TransactionBuilder builder = createTransactionBuilder("test new function");
sendCommand(
builder,
XiaomiProto.Command.newBuilder()
.setType(CMD_TYPE_SYSTEM)
.setSubtype(CMD_SYSTEM_DEVICE_INFO)
.build()
);
builder.queue(getQueue());
}
@Override
public void onFindPhone(final boolean start) {
// TODO possible to notify watch?
super.onFindPhone(start);
}
@Override
public void onFindDevice(final boolean start) {
// TODO onFindDevice
super.onFindDevice(start);
}
@Override
public void onSetPhoneVolume(final float volume) {
musicService.onSetPhoneVolume(volume);
}
@Override
public void onSetGpsLocation(final Location location) {
// TODO onSetGpsLocation
super.onSetGpsLocation(location);
}
@Override
public void onSetReminders(final ArrayList<? extends Reminder> reminders) {
scheduleService.onSetReminders(reminders);
}
@Override
public void onSetWorldClocks(final ArrayList<? extends WorldClock> clocks) {
scheduleService.onSetWorldClocks(clocks);
}
@Override
public void onNotification(final NotificationSpec notificationSpec) {
notificationService.onNotification(notificationSpec);
}
@Override
public void onDeleteNotification(final int id) {
notificationService.onDeleteNotification(id);
}
@Override
public void onSetAlarms(final ArrayList<? extends Alarm> alarms) {
scheduleService.onSetAlarms(alarms);
}
@Override
public void onSetCallState(final CallSpec callSpec) {
notificationService.onSetCallState(callSpec);
}
@Override
public void onSetCannedMessages(final CannedMessagesSpec cannedMessagesSpec) {
notificationService.onSetCannedMessages(cannedMessagesSpec);
}
@Override
public void onSetMusicState(final MusicStateSpec stateSpec) {
musicService.onSetMusicState(stateSpec);
}
@Override
public void onSetMusicInfo(final MusicSpec musicSpec) {
musicService.onSetMusicInfo(musicSpec);
}
@Override
public void onInstallApp(final Uri uri) {
// TODO
super.onInstallApp(uri);
}
@Override
public void onAppInfoReq() {
// TODO
super.onAppInfoReq();
}
@Override
public void onAppStart(final UUID uuid, boolean start) {
// TODO
super.onAppStart(uuid, start);
}
@Override
public void onAppDownload(final UUID uuid) {
// TODO
super.onAppDownload(uuid);
}
@Override
public void onAppDelete(final UUID uuid) {
// TODO
super.onAppDelete(uuid);
}
@Override
public void onAppConfiguration(final UUID appUuid, String config, Integer id) {
// TODO
super.onAppConfiguration(appUuid, config, id);
}
@Override
public void onAppReorder(final UUID[] uuids) {
// TODO
super.onAppReorder(uuids);
}
@Override
public void onFetchRecordedData(final int dataTypes) {
// TODO
super.onFetchRecordedData(dataTypes);
}
@Override
public void onReset(final int flags) {
// TODO
super.onReset(flags);
}
@Override
public void onHeartRateTest() {
healthService.onHeartRateTest();
}
@Override
public void onEnableRealtimeHeartRateMeasurement(final boolean enable) {
healthService.onEnableRealtimeHeartRateMeasurement(enable);
}
@Override
public void onEnableRealtimeSteps(final boolean enable) {
healthService.onEnableRealtimeSteps(enable);
}
@Override
public void onScreenshotReq() {
// TODO
super.onScreenshotReq();
}
@Override
public void onEnableHeartRateSleepSupport(final boolean enable) {
// TODO
super.onEnableHeartRateSleepSupport(enable);
}
@Override
public void onSetHeartRateMeasurementInterval(final int seconds) {
// TODO
super.onSetHeartRateMeasurementInterval(seconds);
}
@Override
public void onAddCalendarEvent(final CalendarEventSpec calendarEventSpec) {
scheduleService.onAddCalendarEvent(calendarEventSpec);
}
@Override
public void onDeleteCalendarEvent(final byte type, long id) {
scheduleService.onDeleteCalendarEvent(type, id);
}
@Override
public void onSendWeather(final WeatherSpec weatherSpec) {
weatherService.onSendWeather(weatherSpec);
}
protected void phase2Initialize(final TransactionBuilder builder) {
LOG.info("phase2Initialize");
encryptedIndex = 1; // TODO not here
if (GBApplication.getPrefs().getBoolean("datetime_synconconnect", true)) {
setCurrentTime(builder);
}
for (final AbstractXiaomiService service : mServiceMap.values()) {
service.initialize(builder);
}
// request device info
sendCommand(builder, CMD_TYPE_SYSTEM, CMD_SYSTEM_DEVICE_INFO);
// request battery status
sendCommand(builder, CMD_TYPE_SYSTEM, CMD_SYSTEM_BATTERY);
}
private void sendAck(final BluetoothGattCharacteristic characteristic) {
final TransactionBuilder builder = createTransactionBuilder("send ack");
builder.write(characteristic, PAYLOAD_ACK);
builder.queue(getQueue());
}
protected void handleConfigCommand(final XiaomiProto.Command cmd) {
switch (cmd.getSubtype()) {
case CMD_SYSTEM_DEVICE_INFO:
final XiaomiProto.DeviceInfo deviceInfo = cmd.getSystem().getDeviceInfo();
final GBDeviceEventVersionInfo gbDeviceEventVersionInfo = new GBDeviceEventVersionInfo();
gbDeviceEventVersionInfo.fwVersion = deviceInfo.getFirmware();
//gbDeviceEventVersionInfo.fwVersion2 = "N/A";
gbDeviceEventVersionInfo.hwVersion = deviceInfo.getModel();
final GBDeviceEventUpdateDeviceInfo gbDeviceEventUpdateDeviceInfo = new GBDeviceEventUpdateDeviceInfo("SERIAL: ", deviceInfo.getSerialNumber());
evaluateGBDeviceEvent(gbDeviceEventVersionInfo);
evaluateGBDeviceEvent(gbDeviceEventUpdateDeviceInfo);
return;
case CMD_SYSTEM_BATTERY:
final XiaomiProto.Battery battery = cmd.getSystem().getPower().getBattery();
final GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
batteryInfo.batteryIndex = 0;
batteryInfo.level = battery.getLevel();
switch (battery.getState()) {
case 1:
batteryInfo.state = BatteryState.BATTERY_CHARGING;
break;
case 2:
batteryInfo.state = BatteryState.BATTERY_NORMAL;
break;
default:
batteryInfo.state = BatteryState.UNKNOWN;
LOG.warn("Unknown battery state {}", battery.getState());
}
evaluateGBDeviceEvent(batteryInfo);
return;
case CMD_SYSTEM_CHARGER:
// charger event, request battery state
sendCommand(
"request battery state",
XiaomiProto.Command.newBuilder()
.setType(CMD_TYPE_SYSTEM)
.setSubtype(CMD_SYSTEM_BATTERY)
.build()
);
return;
default:
LOG.warn("Unknown config command {}", cmd.getSubtype());
}
}
private short encryptedIndex = 0;
public void sendCommand(final String taskName, final XiaomiProto.Command command) {
final TransactionBuilder builder = createTransactionBuilder(taskName);
sendCommand(builder, command);
builder.queue(getQueue());
}
public void sendCommand(final TransactionBuilder builder, final XiaomiProto.Command command) {
final byte[] commandBytes = command.toByteArray();
final byte[] encryptedCommandBytes = cipher.encrypt(commandBytes, encryptedIndex);
final ByteBuffer buf = ByteBuffer.allocate(6 + encryptedCommandBytes.length).order(ByteOrder.LITTLE_ENDIAN);
buf.putShort((short) 0);
buf.put((byte) 2); // 2 for command
buf.put((byte) 1); // 1 for encrypted
buf.putShort(encryptedIndex++);
buf.put(encryptedCommandBytes);
LOG.debug("Sending command {} as {}", GB.hexdump(commandBytes), GB.hexdump(buf.array()));
builder.write(getCharacteristic(UUID_CHARACTERISTIC_XIAOMI_COMMAND_WRITE), buf.array());
}
public void sendCommand(final TransactionBuilder builder, final int type, final int subtype) {
sendCommand(
builder,
XiaomiProto.Command.newBuilder()
.setType(type)
.setSubtype(subtype)
.build()
);
}
}

View File

@ -0,0 +1,56 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services;
import android.content.Context;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiCoordinator;
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public abstract class AbstractXiaomiService {
private final XiaomiSupport mSupport;
public AbstractXiaomiService(final XiaomiSupport support) {
this.mSupport = support;
}
public void setContext(final Context context) {
}
public abstract void handleCommand(final XiaomiProto.Command cmd);
public void initialize(final TransactionBuilder builder) {
}
protected XiaomiSupport getSupport() {
return mSupport;
}
protected XiaomiCoordinator getCoordinator() {
return (XiaomiCoordinator) getSupport().getDevice().getDeviceCoordinator();
}
protected Prefs getDevicePrefs() {
return new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getSupport().getDevice().getAddress()));
}
}

View File

@ -0,0 +1,110 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
public class XiaomiHealthService extends AbstractXiaomiService {
private static final Logger LOG = LoggerFactory.getLogger(XiaomiHealthService.class);
public static final int COMMAND_TYPE = 8;
private static final int CMD_SET_USER_INFO = 0;
private static final int GENDER_MALE = 1;
private static final int GENDER_FEMALE = 2;
public XiaomiHealthService(final XiaomiSupport support) {
super(support);
}
@Override
public void handleCommand(final XiaomiProto.Command cmd) {
// TODO
LOG.warn("Unhandled health command");
}
@Override
public void initialize(TransactionBuilder builder) {
setUserInfo(builder);
}
public void setUserInfo(final TransactionBuilder builder) {
final ActivityUser activityUser = new ActivityUser();
final int birthYear = activityUser.getYearOfBirth();
final byte birthMonth = 7; // not in user attributes
final byte birthDay = 1; // not in user attributes
final int genderInt = activityUser.getGender() != ActivityUser.GENDER_FEMALE ? GENDER_MALE : GENDER_FEMALE; // TODO other gender?
final Calendar now = GregorianCalendar.getInstance();
final int age = now.get(Calendar.YEAR) - birthYear;
// Compute the approximate max heart rate from the user age
// TODO max heart rate should be input by the user
int maxHeartRate = (int) Math.round(age <= 40 ? 220 - age : 207 - 0.7 * age);
if (maxHeartRate < 100 || maxHeartRate > 220) {
maxHeartRate = 175;
}
final XiaomiProto.UserInfo userInfo = XiaomiProto.UserInfo.newBuilder()
.setHeight(activityUser.getHeightCm())
.setWeight(activityUser.getWeightKg())
.setBirthday(Integer.parseInt(String.format(Locale.ROOT, "%02d%02d%02d", birthYear, birthMonth, birthDay)))
.setGender(genderInt)
.setMaxHeartRate(maxHeartRate)
.setGoalCalories(activityUser.getCaloriesBurntGoal())
.setGoalSteps(activityUser.getStepsGoal())
.setGoalStanding(activityUser.getStandingTimeGoalHours())
.setGoalMoving(activityUser.getActiveTimeGoalMinutes())
.build();
final XiaomiProto.Health health = XiaomiProto.Health.newBuilder()
.setUserInfo(userInfo)
.build();
getSupport().sendCommand(
builder,
XiaomiProto.Command.newBuilder()
.setType(COMMAND_TYPE)
.setSubtype(CMD_SET_USER_INFO)
.setHealth(health)
.build()
);
}
public void onHeartRateTest() {
// TODO
}
public void onEnableRealtimeHeartRateMeasurement(final boolean enable) {
// TODO
}
public void onEnableRealtimeSteps(final boolean enable) {
// TODO
}
}

View File

@ -0,0 +1,158 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services;
import android.content.Context;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
import nodomain.freeyourgadget.gadgetbridge.util.MediaManager;
public class XiaomiMusicService extends AbstractXiaomiService {
private static final Logger LOG = LoggerFactory.getLogger(XiaomiMusicService.class);
public static final int COMMAND_TYPE = 18;
private static final int CMD_MUSIC_GET = 0;
private static final int CMD_MUSIC_SEND = 1;
private static final int CMD_MUSIC_BUTTON = 1;
private static final byte BUTTON_PLAY = 0x00;
private static final byte BUTTON_PAUSE = 0x01;
private static final byte BUTTON_PREVIOUS = 0x03;
private static final byte BUTTON_NEXT = 0x04;
private static final byte BUTTON_VOLUME = 0x05;
private static final byte STATE_NOTHING = 0x00;
private static final byte STATE_PLAYING = 0x01;
private static final byte STATE_PAUSED = 0x02;
protected MediaManager mediaManager = null;
protected boolean isMusicAppStarted = false;
public XiaomiMusicService(final XiaomiSupport support) {
super(support);
}
@Override
public void setContext(Context context) {
super.setContext(context);
this.mediaManager = new MediaManager(context);
}
@Override
public void handleCommand(final XiaomiProto.Command cmd) {
final XiaomiProto.Music music = cmd.getMusic();
switch (cmd.getSubtype()) {
case CMD_MUSIC_GET:
mediaManager.refresh();
sendMusicStateToDevice();
break;
case CMD_MUSIC_BUTTON:
final GBDeviceEventMusicControl deviceEventMusicControl = new GBDeviceEventMusicControl();
switch (music.getMediaKey().getKey()) {
case BUTTON_PLAY:
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PLAY;
break;
case BUTTON_PAUSE:
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PAUSE;
break;
case BUTTON_PREVIOUS:
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.PREVIOUS;
break;
case BUTTON_NEXT:
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.NEXT;
break;
case BUTTON_VOLUME:
if (music.getMediaKey().getVolume() > 0) {
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.VOLUMEUP;
} else {
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.VOLUMEDOWN;
}
break;
default:
LOG.warn("Unexpected media button key {}", music.getMediaKey().getKey());
return;
}
// FIXME sometimes this is not triggering a device update?
getSupport().evaluateGBDeviceEvent(deviceEventMusicControl);
break;
default:
LOG.warn("Unhandled music command {}", cmd.getSubtype());
}
}
public void onSetMusicState(final MusicStateSpec stateSpec) {
if (mediaManager.onSetMusicState(stateSpec) && isMusicAppStarted) {
sendMusicStateToDevice();
}
}
public void onSetPhoneVolume(final float ignoredVolume) {
sendMusicStateToDevice();
}
public void onSetMusicInfo(final MusicSpec musicSpec) {
if (mediaManager.onSetMusicInfo(musicSpec) && isMusicAppStarted) {
sendMusicStateToDevice();
}
}
private void sendMusicStateToDevice() {
final MusicSpec musicSpec = mediaManager.getBufferMusicSpec();
final MusicStateSpec musicStateSpec = mediaManager.getBufferMusicStateSpec();
final XiaomiProto.MusicInfo.Builder musicInfo = XiaomiProto.MusicInfo.newBuilder()
.setVolume(mediaManager.getPhoneVolume());
if (musicSpec == null || musicStateSpec == null) {
musicInfo.setState(STATE_NOTHING);
} else {
if (musicStateSpec.state == MusicStateSpec.STATE_PLAYING) {
musicInfo.setState(STATE_PLAYING);
} else {
musicInfo.setState(STATE_PAUSED);
}
musicInfo.setVolume(mediaManager.getPhoneVolume())
.setTrack(musicSpec.track)
.setArtist(musicSpec.artist)
.setPosition(musicStateSpec.position)
.setDuration(musicSpec.duration);
}
final XiaomiProto.Music music = XiaomiProto.Music.newBuilder()
.setMusicInfo(musicInfo.build())
.build();
getSupport().sendCommand(
"send music",
XiaomiProto.Command.newBuilder()
.setType(COMMAND_TYPE)
.setSubtype(CMD_MUSIC_SEND)
.setMusic(music)
.build()
);
}
}

View File

@ -0,0 +1,57 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
public class XiaomiNotificationService extends AbstractXiaomiService {
private static final Logger LOG = LoggerFactory.getLogger(XiaomiNotificationService.class);
public static final int COMMAND_TYPE = 7;
public XiaomiNotificationService(final XiaomiSupport support) {
super(support);
}
@Override
public void handleCommand(final XiaomiProto.Command cmd) {
// TODO
}
public void onNotification(final NotificationSpec notificationSpec) {
// TODO
}
public void onDeleteNotification(final int id) {
// TODO
}
public void onSetCallState(final CallSpec callSpec) {
// TODO
}
public void onSetCannedMessages(final CannedMessagesSpec cannedMessagesSpec) {
// TODO
}
}

View File

@ -0,0 +1,248 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services;
import android.content.Intent;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.model.Reminder;
import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
public class XiaomiScheduleService extends AbstractXiaomiService {
private static final Logger LOG = LoggerFactory.getLogger(XiaomiScheduleService.class);
public static final int COMMAND_TYPE = 17;
private static final int CMD_ALARMS_GET = 0;
private static final int CMD_ALARMS_CREATE = 1;
private static final int CMD_ALARMS_EDIT = 3;
private static final int CMD_ALARMS_DELETE = 4;
private static final int REPETITION_ONCE = 0;
private static final int REPETITION_DAILY = 1;
private static final int REPETITION_WEEKLY = 5;
private static final int REPETITION_MONTHLY = 7;
private static final int REPETITION_YEARLY = 8;
private static final int ALARM_SMART = 1;
private static final int ALARM_NORMAL = 2;
// Map of alarm position to Alarm, as returned by the band
private final Map<Integer, Alarm> watchAlarms = new HashMap<>();
public XiaomiScheduleService(final XiaomiSupport support) {
super(support);
}
@Override
public void handleCommand(final XiaomiProto.Command cmd) {
switch (cmd.getSubtype()) {
case CMD_ALARMS_GET:
handleAlarms(cmd.getSchedule().getAlarms());
break;
}
}
@Override
public void initialize(final TransactionBuilder builder) {
requestAlarms(builder);
}
public void onSetReminders(final ArrayList<? extends Reminder> reminders) {
// TODO
}
public void onSetWorldClocks(final ArrayList<? extends WorldClock> clocks) {
// TODO
}
public void onSetAlarms(final ArrayList<? extends Alarm> alarms) {
final List<Integer> alarmsToDelete = new ArrayList<>();
// TODO this is flaky, since it's the watch that defines the IDs...
for (final Alarm alarm : alarms) {
final Alarm watchAlarm = watchAlarms.get(alarm.getPosition());
if (alarm.getUnused() && watchAlarm == null) {
// Disabled on both
continue;
}
if (alarm.getUnused() && watchAlarm != null) {
// Delete from watch
alarmsToDelete.add(watchAlarm.getPosition() + 1);
continue;
}
final XiaomiProto.HourMinute hourMinute = XiaomiProto.HourMinute.newBuilder()
.setHour(alarm.getHour())
.setMinute(alarm.getMinute())
.build();
final XiaomiProto.AlarmDetails.Builder alarmDetails = XiaomiProto.AlarmDetails.newBuilder()
.setTime(hourMinute)
.setEnabled(alarm.getEnabled())
.setSmart(alarm.getSmartWakeup() ? ALARM_SMART : ALARM_NORMAL);
switch (alarm.getRepetition()) {
case Alarm.ALARM_ONCE:
alarmDetails.setRepeatMode(REPETITION_ONCE);
break;
case Alarm.ALARM_DAILY:
alarmDetails.setRepeatMode(REPETITION_DAILY);
break;
default:
alarmDetails.setRepeatMode(REPETITION_WEEKLY);
alarmDetails.setRepeatFlags(alarm.getRepetition());
break;
}
final XiaomiProto.Schedule.Builder schedule = XiaomiProto.Schedule.newBuilder();
if (watchAlarm != null) {
// update existing alarm
schedule.setEditAlarm(
XiaomiProto.Alarm.newBuilder()
.setId(alarm.getPosition() + 1)
.setAlarmDetails(alarmDetails)
.build()
);
} else {
schedule.setCreateAlarm(alarmDetails);
}
getSupport().sendCommand(
(watchAlarm == null ? "create" : "update") + " alarm " + alarm.getPosition(),
XiaomiProto.Command.newBuilder()
.setType(COMMAND_TYPE)
.setSubtype(watchAlarm == null ? CMD_ALARMS_CREATE : CMD_ALARMS_EDIT)
.setSchedule(schedule)
.build()
);
}
if (!alarmsToDelete.isEmpty()) {
final XiaomiProto.AlarmDelete alarmDelete = XiaomiProto.AlarmDelete.newBuilder()
.addAllId(alarmsToDelete)
.build();
final XiaomiProto.Schedule schedule = XiaomiProto.Schedule.newBuilder()
.setDeleteAlarm(alarmDelete)
.build();
getSupport().sendCommand(
"delete unused alarms",
XiaomiProto.Command.newBuilder()
.setType(COMMAND_TYPE)
.setSubtype(CMD_ALARMS_DELETE)
.setSchedule(schedule)
.build()
);
}
}
public void requestAlarms(final TransactionBuilder builder) {
getSupport().sendCommand(builder, COMMAND_TYPE, CMD_ALARMS_GET);
}
public void handleAlarms(final XiaomiProto.Alarms alarms) {
LOG.debug("Got {} alarms", alarms.getAlarmCount());
watchAlarms.clear();
for (final XiaomiProto.Alarm alarm : alarms.getAlarmList()) {
final nodomain.freeyourgadget.gadgetbridge.entities.Alarm gbAlarm = new nodomain.freeyourgadget.gadgetbridge.entities.Alarm();
gbAlarm.setUnused(false); // If the band sent it, it's not unused
gbAlarm.setPosition(alarm.getId() - 1); // band id starts at 1
gbAlarm.setEnabled(alarm.getAlarmDetails().getEnabled());
gbAlarm.setSmartWakeup(alarm.getAlarmDetails().getSmart() == ALARM_SMART);
gbAlarm.setHour(alarm.getAlarmDetails().getTime().getHour());
gbAlarm.setMinute(alarm.getAlarmDetails().getTime().getMinute());
switch (alarm.getAlarmDetails().getRepeatMode()) {
case REPETITION_ONCE:
gbAlarm.setRepetition(Alarm.ALARM_ONCE);
break;
case REPETITION_DAILY:
gbAlarm.setRepetition(Alarm.ALARM_DAILY);
break;
case REPETITION_WEEKLY:
gbAlarm.setRepetition(alarm.getAlarmDetails().getRepeatFlags());
break;
}
watchAlarms.put(gbAlarm.getPosition(), gbAlarm);
}
final List<nodomain.freeyourgadget.gadgetbridge.entities.Alarm> dbAlarms = DBHelper.getAlarms(getSupport().getDevice());
int numUpdatedAlarms = 0;
for (nodomain.freeyourgadget.gadgetbridge.entities.Alarm alarm : dbAlarms) {
final int pos = alarm.getPosition();
final Alarm updatedAlarm = watchAlarms.get(pos);
final boolean alarmNeedsUpdate = updatedAlarm == null ||
alarm.getUnused() != updatedAlarm.getUnused() ||
alarm.getEnabled() != updatedAlarm.getEnabled() ||
alarm.getSmartWakeup() != updatedAlarm.getSmartWakeup() ||
alarm.getHour() != updatedAlarm.getHour() ||
alarm.getMinute() != updatedAlarm.getMinute() ||
alarm.getRepetition() != updatedAlarm.getRepetition();
if (alarmNeedsUpdate) {
numUpdatedAlarms++;
LOG.info("Updating alarm index={}, unused={}", pos, updatedAlarm == null);
alarm.setUnused(updatedAlarm == null);
if (updatedAlarm != null) {
alarm.setEnabled(updatedAlarm.getEnabled());
alarm.setSmartWakeup(updatedAlarm.getSmartWakeup());
alarm.setHour(updatedAlarm.getHour());
alarm.setMinute(updatedAlarm.getMinute());
alarm.setRepetition(updatedAlarm.getRepetition());
}
DBHelper.store(alarm);
}
}
if (numUpdatedAlarms > 0) {
final Intent intent = new Intent(DeviceService.ACTION_SAVE_ALARMS);
LocalBroadcastManager.getInstance(getSupport().getContext()).sendBroadcast(intent);
}
}
public void onAddCalendarEvent(final CalendarEventSpec calendarEventSpec) {
// TODO
}
public void onDeleteCalendarEvent(final byte type, long id) {
// TODO
}
}

View File

@ -0,0 +1,38 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
public class XiaomiSystemService extends AbstractXiaomiService {
private static final Logger LOG = LoggerFactory.getLogger(XiaomiSystemService.class);
public static final int COMMAND_TYPE = 2;
public XiaomiSystemService(final XiaomiSupport support) {
super(support);
}
@Override
public void handleCommand(final XiaomiProto.Command cmd) {
// TODO
}
}

View File

@ -0,0 +1,43 @@
/* Copyright (C) 2023 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
public class XiaomiWeatherService extends AbstractXiaomiService {
private static final Logger LOG = LoggerFactory.getLogger(XiaomiWeatherService.class);
public static final int COMMAND_TYPE = 10;
public XiaomiWeatherService(final XiaomiSupport support) {
super(support);
}
@Override
public void handleCommand(final XiaomiProto.Command cmd) {
// TODO
}
public void onSendWeather(final WeatherSpec weatherSpec) {
// TODO
}
}

View File

@ -0,0 +1,625 @@
syntax = "proto2"; // we must use proto2 to serialize default values on the wire
package xiaomi;
option java_package = "nodomain.freeyourgadget.gadgetbridge.proto.xiaomi";
option java_outer_classname = "XiaomiProto";
message Command {
required uint32 type = 1;
optional uint32 subtype = 2;
optional Auth auth = 3;
optional System system = 4;
optional Health health = 10;
optional Music music = 20;
optional Notification notification = 9;
optional Weather weather = 12;
optional Schedule schedule = 19;
optional uint32 status = 100; // 0 on success on some
}
//
// Auth
//
message Auth {
// 1, 26
optional PhoneNonce phoneNonce = 30;
optional WatchNonce watchNonce = 31;
// 1, 27
optional AuthStep3 authStep3 = 32;
optional AuthStep4 authStep4 = 33;
}
message PhoneNonce {
required bytes nonce = 1;
}
message WatchNonce {
required bytes nonce = 1;
required bytes hmac = 2;
}
message AuthStep3 {
required bytes encryptedNonces = 1;
required bytes encryptedDeviceInfo = 2; // AuthDeviceInfo
}
message AuthStep4 {
required uint32 unknown1 = 1;
required uint32 unknown2 = 2;
}
message AuthDeviceInfo {
required uint32 unknown1 = 1; // 0 - needs to be serialized explicitly
required float phoneApiLevel = 2;
required string phoneName = 3; // phone model
required uint32 unknown3 = 4; // 224
required string region = 5; // 2-letter, upper case
}
//
// System
//
message System {
// 2, 1
optional Power power = 2;
// 2, 2
optional DeviceInfo deviceInfo = 3;
// 2, 3
optional Clock clock = 4;
// 2, 18
optional uint32 findDevice = 5; // 0
// 2, 29
optional DisplayItems displayItems = 10;
// 2, 34
optional DoNotDisturb dndStatus = 11;
// 2, 9 get | 2, 21 set
optional Password password = 19;
// 2, 7 get | 2, 8 set
optional Camera camera = 15;
// 2, 51
optional Widgets widgets = 28;
// 2, 53
optional WidgetsSingle widgetsSingle = 29;
// 2, 14
optional DoNotDisturb dnd2 = 34;
// 2, 15
optional DndSync dndSync = 35;
// 2, 46
optional VibrationPatterns vibrationPatterns = 38;
// 2, 47
optional VibrationNotificationType vibrationSetPreset = 39;
// 2, 58
optional CustomVibrationPattern vibrationPatternCreate = 40;
// 2, 59
optional VibrationTest vibrationTestCustom = 41;
// 2, 47
optional VibrationPatternAck vibrationPatternAck = 43;
// 2, 79
optional Charger charger = 49;
}
message Power {
optional Battery battery = 1;
}
message Battery {
optional uint32 level = 1;
optional uint32 state = 2;
optional LastCharge lastCharge = 3;
}
message LastCharge {
optional uint32 state = 1; // 2
optional uint32 timestampSeconds = 2;
}
message DeviceInfo {
required string serialNumber = 1;
required string firmware = 2;
optional string unknown3 = 3; // "" ?
required string model = 4;
}
message Clock {
required Date date = 1;
required Time time = 2;
required TimeZone timezone = 3;
required bool isNot24hour = 4;
}
message Date {
required uint32 year = 1;
required uint32 month = 2;
required uint32 day = 3;
}
message Time {
required uint32 hour = 1;
required uint32 minute = 2;
optional uint32 second = 3;
optional uint32 millisecond = 4;
}
message TimeZone {
// offsets are in blocks of 15 min
optional sint32 zoneOffset = 1;
optional sint32 dstOffset = 2;
required string name = 3;
}
message DisplayItems {
repeated DisplayItem displayItem = 1;
}
message DisplayItem {
optional string code = 1;
optional string name = 2;
optional bool disabled = 3;
optional uint32 isSettings = 4;
optional uint32 unknown5 = 5; // 1
optional bool rarelyUsed = 6;
}
message Camera {
required bool enabled = 1;
}
message Widgets {
repeated Widget widget = 1;
}
message Widget {
optional uint32 unknown1 = 1;
optional uint32 unknown2 = 2;
repeated WidgetPart widgetPart = 3;
}
message WidgetPart {
optional uint32 unknown1 = 1;
optional uint32 unknown2 = 2;
optional uint32 unknown3 = 3;
}
message WidgetsSingle {
repeated SingleWidget widget = 1;
}
message SingleWidget {
optional uint32 unknown1 = 1;
optional uint32 unknown2 = 2;
optional uint32 unknown3 = 3;
optional string title = 4;
optional uint32 unknown5 = 5;
}
message DoNotDisturb {
optional uint32 status = 1; // 0 enabled, 2 disabled
}
message DoNotDisturb2 {
}
message DndSync {
}
message Password {
optional uint32 state = 1; // 1 disabled, 2 enabled
optional string password = 2;
}
message VibrationPatterns {
repeated VibrationNotificationType notificationType = 1;
optional uint32 unknown2 = 2; // 50, max patterns?
repeated CustomVibrationPattern customVibrationPattern = 3;
}
message CustomVibrationPattern {
optional uint32 id = 1;
optional string name = 2;
repeated Vibration vibration = 3;
optional uint32 unknown4 = 4; // 1 on creation
}
message VibrationNotificationType {
// 1 incoming calls
// 2 events
// 3 alarms
// 4 notifications
// 5 standing reminder
// 6 sms
// 7 goal
// 8 events
optional uint32 notificationType = 1;
optional uint32 preset = 2;
}
message VibrationTest {
repeated Vibration vibration = 1;
}
message VibrationPatternAck {
optional uint32 status = 1; // 0
}
message Vibration {
optional uint32 vibrate = 1; // 0/1
optional uint32 ms = 2;
}
message Charger {
optional uint32 state = 1; // 1 charging, 2 not charging
}
//
// Health
//
message Health {
optional UserInfo userInfo = 1;
optional SpO2 spo2 = 7;
optional HeartRate heartRate = 8;
// 8, 12 get | 8, 13 set
optional StandingReminder standingReminder = 9;
optional Stress stress = 10;
optional AchievementReminders achievementReminders = 13;
// 8, 35 get | 8, 36 set
optional VitalityScore vitalityScore = 14;
// 8, 26
optional WorkoutStatusWatch workoutStatusWatch = 20;
// 8, 30
optional WorkoutOpenWatch workoutOpenWatch = 25;
optional WorkoutOpenReply workoutOpenReply = 26;
// 7, 48
optional WorkoutLocation workoutLocation = 40;
// 8,45 enable | 8, 46 disable | 8, 47 periodic
optional RealTimeStats realTimeStats = 39;
}
message UserInfo {
optional uint32 height = 1; // cm
optional float weight = 2; // kg
optional uint32 birthday = 3; // YYYYMMDD
optional uint32 gender = 4; // 1 male, 2 female
optional uint32 maxHeartRate = 5;
optional uint32 goalCalories = 6;
optional uint32 goalSteps = 7;
optional uint32 goalStanding = 9; // hours
optional uint32 goalMoving = 11; // minutes
}
message SpO2 {
optional uint32 unknown1 = 1; // 1
optional bool allDayTracking = 2;
optional Spo2AlarmLow alarmLow = 4;
}
message Spo2AlarmLow {
optional bool alarmLowEnabled = 1;
optional uint32 alarmLowThreshold = 2; // 90, 85, 80
}
message HeartRate {
optional bool disabled = 1; // 0 enabled 1 disabled
optional uint32 interval = 2; // 0 smart 1 10 30
optional bool alarmHighEnabled = 3;
optional uint32 alarmHighThreshold = 4; // 100, 110, ... 150
optional AdvancedMonitoring advancedMonitoring = 5;
optional uint32 unknown7 = 7; // 1
optional HeartRateAlarmLow heartRateAlarmLow = 8;
required uint32 breathingScore = 9; // 1 on, 2 off
}
message AdvancedMonitoring {
required bool enabled = 1;
}
message HeartRateAlarmLow {
optional bool alarmLowEnabled = 1;
optional uint32 alarmLowThreshold = 2; // 40, 45, 50
}
message StandingReminder {
optional bool enabled = 1;
optional HourMinute start = 2;
optional HourMinute end = 3;
optional bool dnd = 4;
optional HourMinute dndStart = 6;
optional HourMinute dndEnd = 7;
}
message Stress {
optional bool allDayTracking = 1;
optional RelaxReminder relaxReminder = 2;
}
message AchievementReminders {
optional bool enabled = 1;
optional uint32 suggested = 2; // 0 moving, 1 standing
}
message RelaxReminder {
optional bool enabled = 1;
optional uint32 unknown2 = 2; // 0
}
message VitalityScore {
optional bool sevenDay = 1;
optional bool dailyProgress = 2;
}
message WorkoutStatusWatch {
optional uint32 timestamp = 1; // seconds
optional uint32 unknown2 = 2;
}
message WorkoutOpenWatch {
optional uint32 unknown1 = 1; // 2
optional uint32 unknown2 = 2; // 2
}
message WorkoutOpenReply {
// 3 2 10
// ...
// 0 2 10
// 0 2 2
optional uint32 unknown1 = 1;
optional uint32 unknown2 = 2;
optional uint32 unknown3 = 3;
}
message WorkoutLocation {
optional uint32 unknown1 = 1; // 10, sometimes 2
optional uint32 timestamp = 2; // seconds
optional double longitude = 3;
optional double latitude = 4;
optional float unknown6 = 6; // ?
optional float unknown7 = 7; // altitude?
optional float unknown8 = 8; // ?
optional float unknown9 = 9; // ?
}
message RealTimeStats {
optional uint32 steps = 1;
optional uint32 calories = 2;
optional uint32 unknown3 = 3; // increases during activity
optional uint32 heartRate = 4;
optional uint32 unknown5 = 5; // 0 probably moving time
optional uint32 standingHours = 6;
}
//
// Music
//
message Music {
// 18, 1
optional MusicInfo musicInfo = 1;
// 18, 2
optional MediaKey mediaKey = 2;
}
message MusicInfo {
required uint32 state = 1; // 0 not playing, 1 playing, 2 paused
optional uint32 volume = 2;
optional string track = 4;
optional string artist = 5;
optional uint32 position = 6;
optional uint32 duration = 7;
}
message MediaKey {
required uint32 key = 1; // 0 play, 1 pause, 3 prev, 4 next, 5 vol
optional uint32 volume = 2; // 100 vol+, 0 vol-
}
//
// Notification
//
message Notification {
optional Notification2 notification2 = 3;
optional Notification4 notification4 = 4;
// 7, 12
optional CannedReplies cannedReplies = 9;
}
message Notification2 {
optional Notification3 notification3 = 1;
}
message Notification3 {
optional string package = 1;
optional string appName = 2;
optional string title = 3;
optional string timestamp = 6;
optional string unknown4 = 4;
optional string body = 5;
optional uint32 id = 7;
optional string unknown12 = 12;
optional uint32 hasReply = 13;
}
message Notification4 {
optional Notification5 notification5 = 1;
}
message CannedReplies {
optional uint32 minReplies = 1;
repeated string reply = 2;
optional uint32 maxReplies = 3;
}
message Notification5 {
optional uint32 id = 1;
optional string package = 2;
optional string unknown4 = 4;
}
//
// Weather
//
message Weather {
optional WeatherCurrent current = 1;
optional WeatherDaily daily = 2;
// 10, 6 request without payload?
// 10, 5 set current | 10, 7 create | 10, 8 delete
optional WeatherCurrentLocation currentLocation = 4;
// 10, 7 create
optional WeatherLocation create = 5;
// 10, 10
optional WeatherTemperatureUnit temperatureUnit = 6;
}
message WeatherCurrent {
}
message WeatherDaily {
}
message WeatherCurrentLocation {
optional WeatherLocation location = 1;
}
message WeatherLocation {
optional string code = 1;
optional string name = 2;
}
message WeatherUnknown1 {
optional float unknown12 = 12;
}
message WeatherTemperatureUnit {
optional uint32 unit = 1; // 1 celsius 2 fahrenheit
}
//
// Schedule
//
message Schedule {
// 17, 0 get
optional Alarms alarms = 1;
// 17, 1
optional AlarmDetails createAlarm = 2;
// 17, 3 -> returns 17, 5
optional Alarm editAlarm = 3;
optional uint32 ackId = 4; // id of created or edited alarm and event
// 17, 4
optional AlarmDelete deleteAlarm = 5;
// 17, 8 get | 17, 9 set
optional SleepMode sleepMode = 9;
// 17, 14 get: 10 -> 2: 50 // max events?
optional Events events = 10;
// 17,10 get/ret | 17,11 create | 17,13 delete
optional WorldClocks worldClocks = 11;
optional uint32 worldClockStatus = 13; // 0 on edit and create
// 17, 15
optional EventDetails createEvent = 14;
// 17, 17
optional Event editEvent = 15;
// 17, 18
optional EventDelete deleteEvent = 17;
}
message Alarms {
optional uint32 maxAlarms = 2; // 10
optional uint32 unknown3 = 3; // 0
optional uint32 unknown4 = 4; // 1
repeated Alarm alarm = 1;
}
message Alarm {
optional uint32 id = 1; // starts at 1
optional AlarmDetails alarmDetails = 2;
}
message AlarmDetails {
optional HourMinute time = 2;
optional uint32 repeatMode = 3; // 0 once, 1 daily, 5 weekly
optional uint32 repeatFlags = 4; // only if weekly: 31 during week, 1 monday, 2 tuesday, 3 mon tue
optional bool enabled = 5;
optional uint32 smart = 7; // 1 smart, 2 normal
}
message AlarmDelete {
repeated uint32 id = 1;
}
message SleepMode {
required bool enabled = 1;
optional SleepModeSchedule schedule = 2;
}
message SleepModeSchedule {
optional HourMinute start = 1;
optional HourMinute end = 2;
optional uint32 unknown3 = 3; // 0
}
message Events {
repeated Event event = 1;
optional uint32 unknown2 = 2; // 50, max events?
}
message Event {
optional uint32 id = 1;
optional EventDetails eventDetails = 2;
}
message EventDetails {
optional Date date = 1;
optional Time time = 2;
optional uint32 repeatMode = 3; // 0 once, 1 daily, weekly (every monday), 7 monthly, 8 yearly
optional uint32 repeatFlags = 4; // 64 for unset, day flags on weekly
optional string title = 5;
}
message EventDelete {
repeated uint32 id = 1;
}
message WorldClocks {
repeated string worldClock = 1;
}
message HourMinute {
required uint32 hour = 1;
required uint32 minute = 2;
}

View File

@ -1296,6 +1296,7 @@
<string name="devicetype_miband5">Mi Band 5</string>
<string name="devicetype_miband6">Mi Band 6</string>
<string name="devicetype_miband7">Xiaomi Smart Band 7</string>
<string name="devicetype_miband8">Xiaomi Smart Band 8</string>
<string name="devicetype_amazfit_balance">Amazfit Balance</string>
<string name="devicetype_amazfit_active">Amazfit Active</string>
<string name="devicetype_amazfit_active_edge">Amazfit Active Edge</string>
@ -2390,6 +2391,7 @@
<string name="temperature_scale_cf_summary">Select whether device uses Celsius or Fahrenheit scale.</string>
<string name="temperature_scale_celsius">Celsius</string>
<string name="temperature_scale_fahrenheit">Fahrenheit</string>
<string name="fossil_hr_nav_app_not_installed_notify_title">Navigation app not installed on watch</string>
<string name="fossil_hr_nav_app_not_installed_notify_text">Navigation started but navigationApp not installed on watch. Please install it from the App Manager.</string>
<string name="call_rejection_method_reject">Reject</string>
@ -2404,4 +2406,5 @@
<string name="pref_title_navigation_apps">Navigation apps</string>
<string name="pref_navigation_app_osmand">OsmAnd(+)</string>
<string name="pref_navigation_app_gmaps">Google Maps</string>
<string name="serial_number">Serial Number</string>
</resources>