From b51328e4f232a25dee894f527702b23e7645dc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Thu, 15 Dec 2022 21:35:54 +0000 Subject: [PATCH] Huami: Fix payload when setting the time Fixes #2999 --- .../service/btle/actions/WriteAction.java | 2 +- .../devices/huami/Huami2021Support.java | 4 +- .../service/devices/huami/HuamiSupport.java | 64 ++++++++++++++--- .../devices/huami/Huami2021SupportTest.java | 71 +++++++++++++++++++ .../devices/huami/HuamiSupportTest.java | 71 +++++++++++++++++++ 5 files changed, 198 insertions(+), 14 deletions(-) create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021SupportTest.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupportTest.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java index d78a9025a..18d0ea1ce 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/actions/WriteAction.java @@ -63,7 +63,7 @@ public class WriteAction extends BtLEAction { return false; } - protected final byte[] getValue() { + public final byte[] getValue() { return value; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java index bf04a3e8a..6ffda8b6d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021Support.java @@ -745,7 +745,7 @@ public abstract class Huami2021Support extends HuamiSupport { buf.put(REMINDERS_CMD_UPDATE); buf.put((byte) (position & 0xFF)); - final Calendar cal = Calendar.getInstance(); + final Calendar cal = createCalendar(); cal.setTime(reminder.getDate()); int reminderFlags = REMINDER_FLAG_ENABLED | REMINDER_FLAG_TEXT; @@ -916,7 +916,7 @@ public abstract class Huami2021Support extends HuamiSupport { // - Day of week starts at 0 // Otherwise, the command gets rejected with an "Out of Range" error and init fails. - final Calendar timestamp = Calendar.getInstance(); + final Calendar timestamp = createCalendar(); final byte[] year = fromUint16(timestamp.get(Calendar.YEAR)); final byte[] cmd = { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index 018cd3b25..1c24cafe1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -276,7 +276,6 @@ import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_ import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_NAME; import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_WEIGHT_KG; import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_YEAR_OF_BIRTH; -import static nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions.fromUint8; import static nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic.UUID_CHARACTERISTIC_ALERT_LEVEL; public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements Huami2021Handler { @@ -404,20 +403,56 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements return weatherSpec.windSpeedAsBeaufort() + ""; // cast to string } + /** + * Returns the given date/time (calendar) as a byte sequence, suitable for sending to the + * Mi Band 2 (or derivative). The band appears to not handle DST offsets, so we simply add this + * to the timezone. + * + * @param calendar + * @param precision + * @return + */ public byte[] getTimeBytes(Calendar calendar, TimeUnit precision) { + byte[] bytes; if (precision == TimeUnit.MINUTES) { - final byte[] bytes = BLETypeConversions.shortCalendarToRawBytes(calendar); - //add nonstandard extension - final byte[] tail = new byte[] { 0, BLETypeConversions.mapTimeZone(calendar, BLETypeConversions.TZ_FLAG_INCLUDE_DST_IN_TZ) }; - return BLETypeConversions.join(bytes, tail); + bytes = BLETypeConversions.shortCalendarToRawBytes(calendar); } else if (precision == TimeUnit.SECONDS) { - final byte[] bytes = BLETypeConversions.calendarToCurrentTime(calendar); - //add nonstandard extension, only one byte needed, as format is different from above - final byte[] tail = new byte[] { BLETypeConversions.mapTimeZone(calendar, BLETypeConversions.TZ_FLAG_INCLUDE_DST_IN_TZ) }; - return BLETypeConversions.join(bytes, tail); + bytes = calendarToRawBytes(calendar); } else { throw new IllegalArgumentException("Unsupported precision, only MINUTES and SECONDS are supported till now"); } + byte[] tail = new byte[] { 0, BLETypeConversions.mapTimeZone(calendar, BLETypeConversions.TZ_FLAG_INCLUDE_DST_IN_TZ) }; + // 0 = adjust reason bitflags? or DST offset?? , timezone +// byte[] tail = new byte[] { 0x2 }; // reason + byte[] all = BLETypeConversions.join(bytes, tail); + return all; + } + + /** + * Converts a timestamp to the byte sequence to be sent to the current time characteristic + * + * @param timestamp + * @return + * @see GattCharacteristic#UUID_CHARACTERISTIC_CURRENT_TIME + */ + public static byte[] calendarToRawBytes(Calendar timestamp) { + // MiBand2: + // year,year,month,dayofmonth,hour,minute,second,dayofweek,0,0,tz + + byte[] year = BLETypeConversions.fromUint16(timestamp.get(Calendar.YEAR)); + return new byte[] { + year[0], + year[1], + BLETypeConversions.fromUint8(timestamp.get(Calendar.MONTH) + 1), + BLETypeConversions.fromUint8(timestamp.get(Calendar.DATE)), + BLETypeConversions.fromUint8(timestamp.get(Calendar.HOUR_OF_DAY)), + BLETypeConversions.fromUint8(timestamp.get(Calendar.MINUTE)), + BLETypeConversions.fromUint8(timestamp.get(Calendar.SECOND)), + BLETypeConversions.dayOfWeekToRawBytes(timestamp), + 0, // fractions256 (not set) + // 0 (DST offset?) Mi2 + // k (tz) Mi2 + }; } public Calendar fromTimeBytes(byte[] bytes) { @@ -426,12 +461,19 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements } public HuamiSupport setCurrentTimeWithService(TransactionBuilder builder) { - GregorianCalendar now = BLETypeConversions.createCalendar(); - final byte[] bytes = getTimeBytes(now, TimeUnit.MINUTES); + final Calendar now = createCalendar(); + byte[] bytes = getTimeBytes(now, TimeUnit.SECONDS); builder.write(getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_CURRENT_TIME), bytes); return this; } + /** + * Allow for the calendar to be overridden to a fixed date, for tests. + */ + protected Calendar createCalendar() { + return BLETypeConversions.createCalendar(); + } + /** * Last action of initialization sequence. Sets the device to initialized. * It is only invoked if all other actions were successfully run, so the device diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021SupportTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021SupportTest.java new file mode 100644 index 000000000..4850c4db5 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/Huami2021SupportTest.java @@ -0,0 +1,71 @@ +/* Copyright (C) 2022 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 . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huami; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Context; +import android.net.Uri; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction; + +public class Huami2021SupportTest { + @Test + public void testSetCurrentTimeWithService() { + final Huami2021Support support = createSupport(); + + final TransactionBuilder testTransactionBuilder = new TransactionBuilder("test"); + support.setCurrentTimeWithService(testTransactionBuilder); + final WriteAction action = (WriteAction) testTransactionBuilder.getTransaction().getActions().get(0); + + Assert.assertArrayEquals(new byte[]{-26, 7, 12, 15, 20, 38, 53, 4, 0, 8, 4}, action.getValue()); + } + + private Huami2021Support createSupport() { + return new Huami2021Support() { + @Override + public BluetoothGattCharacteristic getCharacteristic(final UUID uuid) { + return new BluetoothGattCharacteristic(null, 0, 0); + } + + @Override + public HuamiFWHelper createFWHelper(final Uri uri, final Context context) throws IOException { + return null; + } + + @Override + public Calendar createCalendar() { + // 2022-12-15 20:38:53 GMT+1 + final Instant instant = Instant.ofEpochMilli(1671133133000L); + final ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, ZoneId.of("Europe/Paris")); + return GregorianCalendar.from(zdt); + } + }; + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupportTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupportTest.java new file mode 100644 index 000000000..1ac81f107 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupportTest.java @@ -0,0 +1,71 @@ +/* Copyright (C) 2022 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 . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.huami; + +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.Context; +import android.net.Uri; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction; + +public class HuamiSupportTest { + @Test + public void testSetCurrentTimeWithService() { + final TransactionBuilder testTransactionBuilder = new TransactionBuilder("test"); + final HuamiSupport huamiSupport = createSupport(); + + huamiSupport.setCurrentTimeWithService(testTransactionBuilder); + final WriteAction action = (WriteAction) testTransactionBuilder.getTransaction().getActions().get(0); + + Assert.assertArrayEquals(new byte[]{-26, 7, 12, 15, 20, 38, 53, 4, 0, 0, 4}, action.getValue()); + } + + private HuamiSupport createSupport() { + return new HuamiSupport() { + @Override + public BluetoothGattCharacteristic getCharacteristic(final UUID uuid) { + return new BluetoothGattCharacteristic(null, 0, 0); + } + + @Override + public HuamiFWHelper createFWHelper(final Uri uri, final Context context) throws IOException { + return null; + } + + @Override + public Calendar createCalendar() { + // 2022-12-15 20:38:53 GMT+1 + final Instant instant = Instant.ofEpochMilli(1671133133000L); + final ZonedDateTime zdt = ZonedDateTime.ofInstant(instant, ZoneId.of("Europe/Paris")); + return GregorianCalendar.from(zdt); + } + }; + } +}