mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2025-02-18 05:17:08 +01:00
Zepp OS: Refactor config, display items, reminders and http to standalone services
This commit is contained in:
parent
ecb71a6dc5
commit
9d3c480414
@ -52,6 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuami2
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAlexaService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsContactsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLogsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsRemindersService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsShortcutCardsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiLanguageType;
|
||||
@ -241,7 +242,7 @@ public abstract class Huami2021Coordinator extends HuamiCoordinator {
|
||||
|
||||
@Override
|
||||
public int getReminderSlotCount(final GBDevice device) {
|
||||
return getPrefs(device).getInt(Huami2021Service.REMINDERS_PREF_CAPABILITY, 0);
|
||||
return ZeppOsRemindersService.getSlotCount(getPrefs(device));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -20,7 +20,6 @@ public class Huami2021Service {
|
||||
/**
|
||||
* Endpoints for 2021 chunked protocol
|
||||
*/
|
||||
public static final short CHUNKED2021_ENDPOINT_HTTP = 0x0001;
|
||||
public static final short CHUNKED2021_ENDPOINT_WEATHER = 0x000e;
|
||||
public static final short CHUNKED2021_ENDPOINT_CONNECTION = 0x0015;
|
||||
public static final short CHUNKED2021_ENDPOINT_USER_INFO = 0x0017;
|
||||
@ -30,37 +29,11 @@ public class Huami2021Service {
|
||||
public static final short CHUNKED2021_ENDPOINT_FIND_DEVICE = 0x001a;
|
||||
public static final short CHUNKED2021_ENDPOINT_MUSIC = 0x001b;
|
||||
public static final short CHUNKED2021_ENDPOINT_HEARTRATE = 0x001d;
|
||||
public static final short CHUNKED2021_ENDPOINT_DISPLAY_ITEMS = 0x0026;
|
||||
public static final short CHUNKED2021_ENDPOINT_BATTERY = 0x0029;
|
||||
public static final short CHUNKED2021_ENDPOINT_REMINDERS = 0x0038;
|
||||
public static final short CHUNKED2021_ENDPOINT_SILENT_MODE = 0x003b;
|
||||
public static final short CHUNKED2021_ENDPOINT_AUTH = 0x0082;
|
||||
public static final short CHUNKED2021_ENDPOINT_COMPAT = 0x0090;
|
||||
|
||||
/**
|
||||
* HTTP, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_HTTP}.
|
||||
*/
|
||||
public static final byte HTTP_CMD_REQUEST = 0x01;
|
||||
public static final byte HTTP_CMD_RESPONSE = 0x02;
|
||||
public static final byte HTTP_RESPONSE_SUCCESS = 0x01;
|
||||
public static final byte HTTP_RESPONSE_NO_INTERNET = 0x02;
|
||||
|
||||
/**
|
||||
* Display Items, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_DISPLAY_ITEMS}.
|
||||
*/
|
||||
public static final byte DISPLAY_ITEMS_CMD_CAPABILITIES_REQUEST = 0x01;
|
||||
public static final byte DISPLAY_ITEMS_CMD_CAPABILITIES_RESPONSE = 0x02;
|
||||
public static final byte DISPLAY_ITEMS_CMD_REQUEST = 0x03;
|
||||
public static final byte DISPLAY_ITEMS_CMD_RESPONSE = 0x04;
|
||||
public static final byte DISPLAY_ITEMS_CMD_CREATE = 0x05;
|
||||
public static final byte DISPLAY_ITEMS_CMD_CREATE_ACK = 0x06;
|
||||
public static final byte DISPLAY_ITEMS_MENU = 0x01;
|
||||
public static final byte DISPLAY_ITEMS_SHORTCUTS = 0x02;
|
||||
public static final byte DISPLAY_ITEMS_CONTROL_CENTER = 0x03;
|
||||
public static final byte DISPLAY_ITEMS_SECTION_MAIN = 0x01;
|
||||
public static final byte DISPLAY_ITEMS_SECTION_MORE = 0x02;
|
||||
public static final byte DISPLAY_ITEMS_SECTION_DISABLED = 0x03;
|
||||
|
||||
/**
|
||||
* Find Device, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_FIND_DEVICE}.
|
||||
*/
|
||||
@ -157,25 +130,6 @@ public class Huami2021Service {
|
||||
public static final byte MUSIC_BUTTON_VOLUME_UP = 0x05;
|
||||
public static final byte MUSIC_BUTTON_VOLUME_DOWN = 0x06;
|
||||
|
||||
/**
|
||||
* Reminders, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_REMINDERS}.
|
||||
*/
|
||||
public static final byte REMINDERS_CMD_CAPABILITIES_REQUEST = 0x01;
|
||||
public static final byte REMINDERS_CMD_CAPABILITIES_RESPONSE = 0x02;
|
||||
public static final byte REMINDERS_CMD_REQUEST = 0x03;
|
||||
public static final byte REMINDERS_CMD_RESPONSE = 0x04;
|
||||
public static final byte REMINDERS_CMD_CREATE = 0x05;
|
||||
public static final byte REMINDERS_CMD_CREATE_ACK = 0x06;
|
||||
public static final byte REMINDERS_CMD_UPDATE = 0x07;
|
||||
public static final byte REMINDERS_CMD_UPDATE_ACK = 0x08;
|
||||
public static final byte REMINDERS_CMD_DELETE = 0x09;
|
||||
public static final byte REMINDERS_CMD_DELETE_ACK = 0x0a;
|
||||
public static final int REMINDER_FLAG_ENABLED = 0x0001;
|
||||
public static final int REMINDER_FLAG_TEXT = 0x0008;
|
||||
public static final int REMINDER_FLAG_REPEAT_MONTH = 0x1000;
|
||||
public static final int REMINDER_FLAG_REPEAT_YEAR = 0x2000;
|
||||
public static final String REMINDERS_PREF_CAPABILITY = "huami_2021_capability_reminders";
|
||||
|
||||
/**
|
||||
* Weather, for {@link Huami2021Service#CHUNKED2021_ENDPOINT_WEATHER}.
|
||||
*/
|
||||
|
@ -118,8 +118,11 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.service
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsAppsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCalendarService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsCannedMessagesService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsDisplayItemsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsHttpService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsLogsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsNotificationService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsRemindersService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsServicesService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsShortcutCardsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.ZeppOsConfigService;
|
||||
@ -164,6 +167,9 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
private final ZeppOsAlexaService alexaService = new ZeppOsAlexaService(this);
|
||||
private final ZeppOsAppsService appsService = new ZeppOsAppsService(this);
|
||||
private final ZeppOsLogsService logsService = new ZeppOsLogsService(this);
|
||||
private final ZeppOsDisplayItemsService displayItemsService = new ZeppOsDisplayItemsService(this);
|
||||
private final ZeppOsHttpService httpService = new ZeppOsHttpService(this);
|
||||
private final ZeppOsRemindersService remindersService = new ZeppOsRemindersService(this);
|
||||
|
||||
private final Map<Short, AbstractZeppOsService> mServiceMap = new LinkedHashMap<Short, AbstractZeppOsService>() {{
|
||||
put(servicesService.getEndpoint(), servicesService);
|
||||
@ -184,6 +190,9 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
put(alexaService.getEndpoint(), alexaService);
|
||||
put(appsService.getEndpoint(), appsService);
|
||||
put(logsService.getEndpoint(), logsService);
|
||||
put(displayItemsService.getEndpoint(), displayItemsService);
|
||||
put(httpService.getEndpoint(), httpService);
|
||||
put(remindersService.getEndpoint(), remindersService);
|
||||
}};
|
||||
|
||||
public Huami2021Support() {
|
||||
@ -215,7 +224,6 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(final String config) {
|
||||
final ZeppOsConfigService.ConfigSetter configSetter = configService.newSetter();
|
||||
final Prefs prefs = getDevicePrefs();
|
||||
|
||||
// Check if any of the services handles this config
|
||||
@ -225,28 +233,6 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
}
|
||||
}
|
||||
|
||||
// Other preferences
|
||||
switch (config) {
|
||||
case HuamiConst.PREF_CONTROL_CENTER_SORTABLE:
|
||||
setControlCenter();
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer everything else to the configService
|
||||
try {
|
||||
if (configService.setConfig(prefs, config, configSetter)) {
|
||||
// If the ConfigSetter was able to set the config, just write it and return
|
||||
final TransactionBuilder builder;
|
||||
builder = performInitialized("Sending configuration for option: " + config);
|
||||
configSetter.write(builder);
|
||||
builder.queue(getQueue());
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
|
||||
super.onSendConfiguration(config);
|
||||
}
|
||||
|
||||
@ -516,83 +502,16 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
notificationService.sendNotification(notificationSpec);
|
||||
}
|
||||
|
||||
protected Huami2021Support requestReminders(final TransactionBuilder builder) {
|
||||
LOG.info("Requesting reminders");
|
||||
|
||||
writeToChunked2021(builder, CHUNKED2021_ENDPOINT_REMINDERS, REMINDERS_CMD_REQUEST, false);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void sendReminderToDevice(final TransactionBuilder builder, int position, final Reminder reminder) {
|
||||
final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
|
||||
final int reminderSlotCount = coordinator.getReminderSlotCount(getDevice());
|
||||
if (position + 1 > reminderSlotCount) {
|
||||
LOG.error("Reminder for position {} is over the limit of {} reminders", position, reminderSlotCount);
|
||||
return;
|
||||
public void onSetReminders(final ArrayList<? extends Reminder> reminders) {
|
||||
final TransactionBuilder builder;
|
||||
try {
|
||||
builder = performInitialized("onSetReminders");
|
||||
remindersService.sendReminders(builder, reminders);
|
||||
builder.queue(getQueue());
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Unable to send reminders to device", e);
|
||||
}
|
||||
|
||||
if (reminder == null) {
|
||||
// Delete reminder
|
||||
writeToChunked2021(builder, CHUNKED2021_ENDPOINT_REMINDERS, new byte[]{REMINDERS_CMD_DELETE, (byte) (position & 0xFF)}, false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
final String message;
|
||||
if (reminder.getMessage().length() > coordinator.getMaximumReminderMessageLength()) {
|
||||
LOG.warn("The reminder message length {} is longer than {}, will be truncated",
|
||||
reminder.getMessage().length(),
|
||||
coordinator.getMaximumReminderMessageLength()
|
||||
);
|
||||
message = StringUtils.truncate(reminder.getMessage(), coordinator.getMaximumReminderMessageLength());
|
||||
} else {
|
||||
message = reminder.getMessage();
|
||||
}
|
||||
|
||||
final ByteBuffer buf = ByteBuffer.allocate(1 + 10 + message.getBytes(StandardCharsets.UTF_8).length + 1);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
// Update does an upsert, so let's use it. If we call create twice on the same ID, it becomes weird
|
||||
buf.put(REMINDERS_CMD_UPDATE);
|
||||
buf.put((byte) (position & 0xFF));
|
||||
|
||||
final Calendar cal = createCalendar();
|
||||
cal.setTime(reminder.getDate());
|
||||
|
||||
int reminderFlags = REMINDER_FLAG_ENABLED | REMINDER_FLAG_TEXT;
|
||||
|
||||
switch (reminder.getRepetition()) {
|
||||
case Reminder.ONCE:
|
||||
// Default is once, nothing to do
|
||||
break;
|
||||
case Reminder.EVERY_DAY:
|
||||
reminderFlags |= 0x0fe0; // all week day bits set
|
||||
break;
|
||||
case Reminder.EVERY_WEEK:
|
||||
int dayOfWeek = BLETypeConversions.dayOfWeekToRawBytes(cal) - 1; // Monday = 0
|
||||
reminderFlags |= 0x20 << dayOfWeek;
|
||||
break;
|
||||
case Reminder.EVERY_MONTH:
|
||||
reminderFlags |= REMINDER_FLAG_REPEAT_MONTH;
|
||||
break;
|
||||
case Reminder.EVERY_YEAR:
|
||||
reminderFlags |= REMINDER_FLAG_REPEAT_YEAR;
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unknown repetition for reminder in position {}, defaulting to once", position);
|
||||
}
|
||||
|
||||
buf.putInt(reminderFlags);
|
||||
|
||||
buf.putInt((int) (cal.getTimeInMillis() / 1000L));
|
||||
buf.put((byte) 0x00);
|
||||
|
||||
buf.put(message.getBytes(StandardCharsets.UTF_8));
|
||||
buf.put((byte) 0x00);
|
||||
|
||||
writeToChunked2021(builder, CHUNKED2021_ENDPOINT_REMINDERS, buf.array(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -898,173 +817,6 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Huami2021Support setDisplayItems(final TransactionBuilder builder) {
|
||||
final Prefs prefs = getDevicePrefs();
|
||||
|
||||
setDisplayItems2021(
|
||||
builder,
|
||||
DISPLAY_ITEMS_MENU,
|
||||
new ArrayList<>(prefs.getList(Huami2021Coordinator.getPrefPossibleValuesKey(HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE), Collections.emptyList())),
|
||||
new ArrayList<>(prefs.getList(HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE, Collections.emptyList()))
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Huami2021Support setShortcuts(final TransactionBuilder builder) {
|
||||
final Prefs prefs = getDevicePrefs();
|
||||
|
||||
setDisplayItems2021(
|
||||
builder,
|
||||
DISPLAY_ITEMS_SHORTCUTS,
|
||||
new ArrayList<>(prefs.getList(Huami2021Coordinator.getPrefPossibleValuesKey(HuamiConst.PREF_SHORTCUTS_SORTABLE), Collections.emptyList())),
|
||||
new ArrayList<>(prefs.getList(HuamiConst.PREF_SHORTCUTS_SORTABLE, Collections.emptyList()))
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
protected void setControlCenter() {
|
||||
try {
|
||||
final TransactionBuilder builder = performInitialized("set control center");
|
||||
|
||||
final Prefs prefs = getDevicePrefs();
|
||||
|
||||
setDisplayItems2021(
|
||||
builder,
|
||||
DISPLAY_ITEMS_CONTROL_CENTER,
|
||||
new ArrayList<>(prefs.getList(Huami2021Coordinator.getPrefPossibleValuesKey(HuamiConst.PREF_CONTROL_CENTER_SORTABLE), Collections.emptyList())),
|
||||
new ArrayList<>(prefs.getList(HuamiConst.PREF_CONTROL_CENTER_SORTABLE, Collections.emptyList()))
|
||||
);
|
||||
|
||||
builder.queue(getQueue());
|
||||
} catch (final Exception e) {
|
||||
GB.toast("Error setting control center", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void setDisplayItems2021(final TransactionBuilder builder,
|
||||
final byte menuType,
|
||||
final List<String> allSettings,
|
||||
List<String> enabledList) {
|
||||
final boolean isMainMenu = menuType == DISPLAY_ITEMS_MENU;
|
||||
final boolean isShortcuts = menuType == DISPLAY_ITEMS_SHORTCUTS;
|
||||
final boolean hasMoreSection;
|
||||
final Map<String, String> idMap;
|
||||
|
||||
switch (menuType) {
|
||||
case DISPLAY_ITEMS_MENU:
|
||||
LOG.info("Setting menu items");
|
||||
hasMoreSection = getCoordinator().mainMenuHasMoreSection();
|
||||
idMap = MapUtils.reverse(Huami2021MenuType.displayItemNameLookup);
|
||||
break;
|
||||
case DISPLAY_ITEMS_SHORTCUTS:
|
||||
LOG.info("Setting shortcuts");
|
||||
hasMoreSection = false;
|
||||
idMap = MapUtils.reverse(Huami2021MenuType.shortcutsNameLookup);
|
||||
break;
|
||||
case DISPLAY_ITEMS_CONTROL_CENTER:
|
||||
LOG.info("Setting control center");
|
||||
hasMoreSection = false;
|
||||
idMap = MapUtils.reverse(Huami2021MenuType.controlCenterNameLookup);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unknown menu type {}", menuType);
|
||||
return;
|
||||
}
|
||||
|
||||
if (allSettings.isEmpty()) {
|
||||
LOG.warn("List of all display items is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMainMenu && !enabledList.contains("settings")) {
|
||||
// Settings can't be disabled
|
||||
enabledList.add("settings");
|
||||
}
|
||||
|
||||
if (isShortcuts && enabledList.size() > 10) {
|
||||
// Enforced by official app
|
||||
LOG.warn("Truncating shortcuts list to 10");
|
||||
enabledList = enabledList.subList(0, 10);
|
||||
}
|
||||
|
||||
LOG.info("Setting display items (shortcuts={}): {}", isShortcuts, enabledList);
|
||||
|
||||
int numItems = allSettings.size();
|
||||
if (hasMoreSection) {
|
||||
// Exclude the "more" item from the main menu, since it's not a real item
|
||||
numItems--;
|
||||
}
|
||||
|
||||
final ByteBuffer buf = ByteBuffer.allocate(4 + numItems * 12);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
buf.put((byte) 0x05);
|
||||
buf.put(menuType);
|
||||
buf.put((byte) numItems);
|
||||
buf.put((byte) 0x00);
|
||||
|
||||
byte pos = 0;
|
||||
boolean inMoreSection = false;
|
||||
|
||||
// IDs are 8-char hex strings, in upper case
|
||||
final Pattern ID_REGEX = Pattern.compile("^[0-9A-F]{8}$");
|
||||
|
||||
for (final String name : enabledList) {
|
||||
if (name.equals("more")) {
|
||||
inMoreSection = true;
|
||||
pos = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
final String id = idMap.containsKey(name) ? idMap.get(name) : name;
|
||||
if (!ID_REGEX.matcher(id).find()) {
|
||||
LOG.error("Screen item id '{}' is not 8-char hex string", id);
|
||||
continue;
|
||||
}
|
||||
|
||||
final byte sectionKey;
|
||||
if (inMoreSection) {
|
||||
// In more section
|
||||
sectionKey = DISPLAY_ITEMS_SECTION_MORE;
|
||||
} else {
|
||||
// In main section
|
||||
sectionKey = DISPLAY_ITEMS_SECTION_MAIN;
|
||||
}
|
||||
|
||||
// Screen IDs are sent as literal hex strings
|
||||
buf.put(id.getBytes(StandardCharsets.UTF_8));
|
||||
buf.put((byte) 0);
|
||||
buf.put(sectionKey);
|
||||
buf.put(pos++);
|
||||
buf.put((byte) (id.equals("00000013") ? 1 : 0));
|
||||
}
|
||||
|
||||
// Set all disabled items
|
||||
pos = 0;
|
||||
for (final String name : allSettings) {
|
||||
if (enabledList.contains(name) || name.equals("more")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String id = idMap.containsKey(name) ? idMap.get(name) : name;
|
||||
if (!ID_REGEX.matcher(id).find()) {
|
||||
LOG.error("Screen item id '{}' is not 8-char hex string", id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Screen IDs are sent as literal hex strings
|
||||
buf.put(id.getBytes(StandardCharsets.UTF_8));
|
||||
buf.put((byte) 0);
|
||||
buf.put(DISPLAY_ITEMS_SECTION_DISABLED);
|
||||
buf.put(pos++);
|
||||
buf.put((byte) (id.equals("00000013") ? 1 : 0));
|
||||
}
|
||||
|
||||
writeToChunked2021(builder, CHUNKED2021_ENDPOINT_DISPLAY_ITEMS, buf.array(), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Huami2021Support setDistanceUnit(final TransactionBuilder builder) {
|
||||
final MiBandConst.DistanceUnit unit = HuamiCoordinator.getDistanceUnit();
|
||||
@ -1126,18 +878,8 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Huami2021Support requestDisplayItems(final TransactionBuilder builder) {
|
||||
LOG.info("Requesting display items");
|
||||
|
||||
writeToChunked2021(
|
||||
builder,
|
||||
CHUNKED2021_ENDPOINT_DISPLAY_ITEMS,
|
||||
new byte[]{DISPLAY_ITEMS_CMD_REQUEST, DISPLAY_ITEMS_MENU},
|
||||
true
|
||||
);
|
||||
|
||||
return this;
|
||||
public void requestDisplayItems(final TransactionBuilder builder) {
|
||||
displayItemsService.requestItems(builder, ZeppOsDisplayItemsService.DISPLAY_ITEMS_MENU);
|
||||
}
|
||||
|
||||
public void requestApps(final TransactionBuilder builder) {
|
||||
@ -1149,32 +891,6 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
watchfaceService.requestCurrentWatchface(builder);
|
||||
}
|
||||
|
||||
protected Huami2021Support requestShortcuts(final TransactionBuilder builder) {
|
||||
LOG.info("Requesting shortcuts");
|
||||
|
||||
writeToChunked2021(
|
||||
builder,
|
||||
CHUNKED2021_ENDPOINT_DISPLAY_ITEMS,
|
||||
new byte[]{DISPLAY_ITEMS_CMD_REQUEST, DISPLAY_ITEMS_SHORTCUTS},
|
||||
true
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
protected Huami2021Support requestControlCenter(final TransactionBuilder builder) {
|
||||
LOG.info("Requesting shortcuts");
|
||||
|
||||
writeToChunked2021(
|
||||
builder,
|
||||
CHUNKED2021_ENDPOINT_DISPLAY_ITEMS,
|
||||
new byte[]{DISPLAY_ITEMS_CMD_REQUEST, DISPLAY_ITEMS_CONTROL_CENTER},
|
||||
true
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
protected void requestMTU(final TransactionBuilder builder) {
|
||||
writeToChunked2021(
|
||||
builder,
|
||||
@ -1184,15 +900,6 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
);
|
||||
}
|
||||
|
||||
protected void requestCapabilityReminders(final TransactionBuilder builder) {
|
||||
writeToChunked2021(
|
||||
builder,
|
||||
CHUNKED2021_ENDPOINT_REMINDERS,
|
||||
REMINDERS_CMD_CAPABILITIES_REQUEST,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void phase2Initialize(final TransactionBuilder builder) {
|
||||
LOG.info("2021 phase2Initialize...");
|
||||
@ -1219,9 +926,6 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
LOG.info("2021 phase3Initialize...");
|
||||
setUserInfo(builder);
|
||||
|
||||
configService.requestAllConfigs(builder);
|
||||
requestCapabilityReminders(builder);
|
||||
|
||||
for (final HuamiVibrationPatternNotificationType type : coordinator.getVibrationPatternNotificationTypes(gbDevice)) {
|
||||
// FIXME: Can we read these from the band?
|
||||
final String typeKey = type.name().toLowerCase(Locale.ROOT);
|
||||
@ -1229,13 +933,7 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
}
|
||||
|
||||
cannedMessagesService.requestCannedMessages(builder);
|
||||
requestDisplayItems(builder);
|
||||
requestShortcuts(builder);
|
||||
if (coordinator.supportsControlCenter()) {
|
||||
requestControlCenter(builder);
|
||||
}
|
||||
alarmsService.requestAlarms(builder);
|
||||
//requestReminders(builder);
|
||||
|
||||
for (AbstractZeppOsService service : mServiceMap.values()) {
|
||||
service.initialize(builder);
|
||||
@ -1298,8 +996,9 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
return;
|
||||
}
|
||||
|
||||
if (mServiceMap.containsKey(type)) {
|
||||
mServiceMap.get(type).handlePayload(payload);
|
||||
final AbstractZeppOsService service = mServiceMap.get(type);
|
||||
if (service != null) {
|
||||
service.handlePayload(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1316,21 +1015,12 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
case CHUNKED2021_ENDPOINT_WORKOUT:
|
||||
handle2021Workout(payload);
|
||||
return;
|
||||
case CHUNKED2021_ENDPOINT_DISPLAY_ITEMS:
|
||||
handle2021DisplayItems(payload);
|
||||
return;
|
||||
case CHUNKED2021_ENDPOINT_FIND_DEVICE:
|
||||
handle2021FindDevice(payload);
|
||||
return;
|
||||
case CHUNKED2021_ENDPOINT_HTTP:
|
||||
handle2021Http(payload);
|
||||
return;
|
||||
case CHUNKED2021_ENDPOINT_HEARTRATE:
|
||||
handle2021HeartRate(payload);
|
||||
return;
|
||||
case CHUNKED2021_ENDPOINT_REMINDERS:
|
||||
handle2021Reminders(payload);
|
||||
return;
|
||||
case CHUNKED2021_ENDPOINT_CONNECTION:
|
||||
handle2021Connection(payload);
|
||||
return;
|
||||
@ -1395,122 +1085,6 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
}
|
||||
}
|
||||
|
||||
protected void handle2021DisplayItems(final byte[] payload) {
|
||||
switch (payload[0]) {
|
||||
case DISPLAY_ITEMS_CMD_RESPONSE:
|
||||
LOG.info("Got display items from band");
|
||||
decodeAndUpdateDisplayItems(payload);
|
||||
break;
|
||||
case DISPLAY_ITEMS_CMD_CREATE_ACK:
|
||||
LOG.info("Display items set ACK, type = {}, status = {}", payload[1], payload[2]);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unexpected display items payload byte {}", String.format("0x%02x", payload[0]));
|
||||
}
|
||||
}
|
||||
|
||||
private void decodeAndUpdateDisplayItems(final byte[] payload) {
|
||||
final int numberScreens = payload[2];
|
||||
final int expectedLength = 4 + numberScreens * 12;
|
||||
if (payload.length != 4 + numberScreens * 12) {
|
||||
LOG.error("Unexpected display items payload length {}, expected {}", payload.length, expectedLength);
|
||||
return;
|
||||
}
|
||||
|
||||
final String prefKey;
|
||||
final Map<String, String> idMap;
|
||||
switch (payload[1]) {
|
||||
case DISPLAY_ITEMS_MENU:
|
||||
LOG.info("Got {} display items", numberScreens);
|
||||
prefKey = HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE;
|
||||
idMap = Huami2021MenuType.displayItemNameLookup;
|
||||
break;
|
||||
case DISPLAY_ITEMS_SHORTCUTS:
|
||||
LOG.info("Got {} shortcuts", numberScreens);
|
||||
prefKey = HuamiConst.PREF_SHORTCUTS_SORTABLE;
|
||||
idMap = Huami2021MenuType.shortcutsNameLookup;
|
||||
break;
|
||||
case DISPLAY_ITEMS_CONTROL_CENTER:
|
||||
LOG.info("Got {} control center", numberScreens);
|
||||
prefKey = HuamiConst.PREF_CONTROL_CENTER_SORTABLE;
|
||||
idMap = Huami2021MenuType.controlCenterNameLookup;
|
||||
break;
|
||||
default:
|
||||
LOG.error("Unknown display items type {}", String.format("0x%x", payload[1]));
|
||||
return;
|
||||
}
|
||||
final String allScreensPrefKey = Huami2021Coordinator.getPrefPossibleValuesKey(prefKey);
|
||||
|
||||
final boolean menuHasMoreSection;
|
||||
|
||||
if (payload[1] == DISPLAY_ITEMS_MENU) {
|
||||
menuHasMoreSection = getCoordinator().mainMenuHasMoreSection();
|
||||
} else {
|
||||
menuHasMoreSection = false;
|
||||
}
|
||||
|
||||
final String[] mainScreensArr = new String[numberScreens];
|
||||
final String[] moreScreensArr = new String[numberScreens];
|
||||
final List<String> allScreens = new LinkedList<>();
|
||||
if (menuHasMoreSection) {
|
||||
// The band doesn't report the "more" screen, so we add it
|
||||
allScreens.add("more");
|
||||
}
|
||||
|
||||
for (int i = 0; i < numberScreens; i++) {
|
||||
// Screen IDs are sent as literal hex strings
|
||||
final String screenId = new String(subarray(payload, 4 + i * 12, 4 + i * 12 + 8));
|
||||
final String screenNameOrId = idMap.containsKey(screenId) ? idMap.get(screenId) : screenId;
|
||||
allScreens.add(screenNameOrId);
|
||||
|
||||
final int screenSectionVal = payload[4 + i * 12 + 9];
|
||||
final int screenPosition = payload[4 + i * 12 + 10];
|
||||
|
||||
if (screenPosition >= numberScreens) {
|
||||
LOG.warn("Invalid screen position {}, ignoring", screenPosition);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (screenSectionVal) {
|
||||
case DISPLAY_ITEMS_SECTION_MAIN:
|
||||
if (mainScreensArr[screenPosition] != null) {
|
||||
LOG.warn("Duplicate position {} for main section", screenPosition);
|
||||
}
|
||||
//LOG.debug("mainScreensArr[{}] = {}", screenPosition, screenKey);
|
||||
mainScreensArr[screenPosition] = screenNameOrId;
|
||||
break;
|
||||
case DISPLAY_ITEMS_SECTION_MORE:
|
||||
if (moreScreensArr[screenPosition] != null) {
|
||||
LOG.warn("Duplicate position {} for more section", screenPosition);
|
||||
}
|
||||
//LOG.debug("moreScreensArr[{}] = {}", screenPosition, screenKey);
|
||||
moreScreensArr[screenPosition] = screenNameOrId;
|
||||
break;
|
||||
case DISPLAY_ITEMS_SECTION_DISABLED:
|
||||
// Ignore disabled screens
|
||||
//LOG.debug("Ignoring disabled screen {} {}", screenPosition, screenKey);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unknown screen section {}, ignoring", String.format("0x%02x", screenSectionVal));
|
||||
}
|
||||
}
|
||||
|
||||
final List<String> screens = new ArrayList<>(Arrays.asList(mainScreensArr));
|
||||
if (menuHasMoreSection) {
|
||||
screens.add("more");
|
||||
screens.addAll(Arrays.asList(moreScreensArr));
|
||||
}
|
||||
screens.removeAll(Collections.singleton(null));
|
||||
|
||||
final String allScreensPrefValue = StringUtils.join(",", allScreens.toArray(new String[0])).toString();
|
||||
final String prefValue = StringUtils.join(",", screens.toArray(new String[0])).toString();
|
||||
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences()
|
||||
.withPreference(allScreensPrefKey, allScreensPrefValue)
|
||||
.withPreference(prefKey, prefValue);
|
||||
|
||||
evaluateGBDeviceEvent(eventUpdatePreferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* A handler to schedule the find phone event.
|
||||
*/
|
||||
@ -1561,107 +1135,6 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
}
|
||||
}
|
||||
|
||||
protected void handle2021Http(final byte[] payload) {
|
||||
switch (payload[0]) {
|
||||
case HTTP_CMD_REQUEST:
|
||||
int pos = 1;
|
||||
final byte requestId = payload[pos++];
|
||||
final String method = StringUtils.untilNullTerminator(payload, pos);
|
||||
if (method == null) {
|
||||
LOG.error("Failed to decode method from payload");
|
||||
return;
|
||||
}
|
||||
pos += method.length() + 1;
|
||||
final String url = StringUtils.untilNullTerminator(payload, pos);
|
||||
if (url == null) {
|
||||
LOG.error("Failed to decode method from payload");
|
||||
return;
|
||||
}
|
||||
// headers after pos += url.length() + 1;
|
||||
|
||||
LOG.info("Got HTTP {} request: {}", method, url);
|
||||
|
||||
handleUrlRequest(requestId, method, url);
|
||||
return;
|
||||
default:
|
||||
LOG.warn("Unexpected HTTP payload byte {}", String.format("0x%02x", payload[0]));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUrlRequest(final byte requestId, final String method, final String urlString) {
|
||||
if (!"GET".equals(method)) {
|
||||
LOG.error("Unable to handle HTTP method {}", method);
|
||||
// TODO: There's probably a "BAD REQUEST" response or similar
|
||||
replyHttpNoInternet(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
final URL url;
|
||||
try {
|
||||
url = new URL(urlString);
|
||||
} catch (final MalformedURLException e) {
|
||||
LOG.error("Failed to parse url", e);
|
||||
replyHttpNoInternet(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
final String path = url.getPath();
|
||||
final Map<String, String> query = urlQueryParameters(url);
|
||||
|
||||
if (path.startsWith("/weather/")) {
|
||||
final Huami2021Weather.Response response = Huami2021Weather.handleHttpRequest(path, query);
|
||||
replyHttpSuccess(requestId, response.getHttpStatusCode(), response.toJson());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.error("Unhandled URL {}", url);
|
||||
replyHttpNoInternet(requestId);
|
||||
}
|
||||
|
||||
private Map<String, String> urlQueryParameters(final URL url) {
|
||||
final Map<String, String> queryParameters = new HashMap<>();
|
||||
final String[] pairs = url.getQuery().split("&");
|
||||
for (final String pair : pairs) {
|
||||
final String[] parts = pair.split("=", 2);
|
||||
try {
|
||||
final String key = URLDecoder.decode(parts[0], "UTF-8");
|
||||
if (parts.length == 2) {
|
||||
queryParameters.put(key, URLDecoder.decode(parts[1], "UTF-8"));
|
||||
} else {
|
||||
queryParameters.put(key, "");
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to decode query", e);
|
||||
}
|
||||
}
|
||||
return queryParameters;
|
||||
}
|
||||
|
||||
private void replyHttpNoInternet(final byte requestId) {
|
||||
LOG.info("Replying with no internet to http request {}", requestId);
|
||||
|
||||
final byte[] cmd = new byte[]{HTTP_CMD_RESPONSE, requestId, HTTP_RESPONSE_NO_INTERNET, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
writeToChunked2021("http reply no internet", Huami2021Service.CHUNKED2021_ENDPOINT_HTTP, cmd, true);
|
||||
}
|
||||
|
||||
private void replyHttpSuccess(final byte requestId, final int status, final String content) {
|
||||
LOG.debug("Replying with http {} request {} with {}", status, requestId, content);
|
||||
|
||||
final byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
|
||||
final ByteBuffer buf = ByteBuffer.allocate(8 + contentBytes.length);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
buf.put((byte) 0x02);
|
||||
buf.put(requestId);
|
||||
buf.put(HTTP_RESPONSE_SUCCESS);
|
||||
buf.put((byte) status);
|
||||
buf.putInt(contentBytes.length);
|
||||
buf.put(contentBytes);
|
||||
|
||||
writeToChunked2021("http reply success", Huami2021Service.CHUNKED2021_ENDPOINT_HTTP, buf.array(), true);
|
||||
}
|
||||
|
||||
protected void handle2021HeartRate(final byte[] payload) {
|
||||
switch (payload[0]) {
|
||||
case HEART_RATE_CMD_REALTIME_ACK:
|
||||
@ -1698,84 +1171,6 @@ public abstract class Huami2021Support extends HuamiSupport implements ZeppOsFil
|
||||
}
|
||||
}
|
||||
|
||||
protected void handle2021Reminders(final byte[] payload) {
|
||||
switch (payload[0]) {
|
||||
case REMINDERS_CMD_CAPABILITIES_RESPONSE:
|
||||
LOG.info("Reminder capability, status = {}", payload[1]);
|
||||
if (payload[1] != 1) {
|
||||
LOG.warn("Reminder capability unexpected status");
|
||||
return;
|
||||
}
|
||||
final int numReminders = payload[2] & 0xff;
|
||||
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences(
|
||||
REMINDERS_PREF_CAPABILITY,
|
||||
numReminders
|
||||
);
|
||||
evaluateGBDeviceEvent(eventUpdatePreferences);
|
||||
return;
|
||||
case REMINDERS_CMD_CREATE_ACK:
|
||||
LOG.info("Reminder create ACK, status = {}", payload[1]);
|
||||
return;
|
||||
case REMINDERS_CMD_DELETE_ACK:
|
||||
LOG.info("Reminder delete ACK, status = {}", payload[1]);
|
||||
// status 1 = success
|
||||
// status 2 = reminder not found
|
||||
return;
|
||||
case REMINDERS_CMD_UPDATE_ACK:
|
||||
LOG.info("Reminder update ACK, status = {}", payload[1]);
|
||||
return;
|
||||
case REMINDERS_CMD_RESPONSE:
|
||||
LOG.info("Got reminders from band");
|
||||
decodeAndUpdateReminders(payload);
|
||||
return;
|
||||
default:
|
||||
LOG.warn("Unexpected reminders payload byte {}", String.format("0x%02x", payload[0]));
|
||||
}
|
||||
}
|
||||
|
||||
private void decodeAndUpdateReminders(final byte[] payload) {
|
||||
final int numReminders = payload[1];
|
||||
|
||||
if (payload.length < 3 + numReminders * 11) {
|
||||
LOG.warn("Unexpected payload length of {} for {} reminders", payload.length, numReminders);
|
||||
return;
|
||||
}
|
||||
|
||||
// Map of alarm position to Reminder, as returned by the band
|
||||
final Map<Integer, Reminder> payloadReminders = new HashMap<>();
|
||||
|
||||
int i = 3;
|
||||
while (i < payload.length) {
|
||||
if (payload.length - i < 11) {
|
||||
LOG.error("Not enough bytes remaining to parse a reminder ({})", payload.length - i);
|
||||
return;
|
||||
}
|
||||
|
||||
final int reminderPosition = payload[i++] & 0xff;
|
||||
final int reminderFlags = BLETypeConversions.toUint32(payload, i);
|
||||
i += 4;
|
||||
final int reminderTimestamp = BLETypeConversions.toUint32(payload, i);
|
||||
i += 4;
|
||||
i++; // 0 ?
|
||||
final Date reminderDate = new Date(reminderTimestamp * 1000L);
|
||||
final String reminderText = StringUtils.untilNullTerminator(payload, i);
|
||||
if (reminderText == null) {
|
||||
LOG.error("Failed to parse reminder text at pos {}", i);
|
||||
return;
|
||||
}
|
||||
|
||||
i += reminderText.length() + 1;
|
||||
|
||||
LOG.info("Reminder {}, {}, {}, {}", reminderPosition, String.format("0x%04x", reminderFlags), reminderDate, reminderText);
|
||||
}
|
||||
if (i != payload.length) {
|
||||
LOG.error("Unexpected reminders payload trailer, {} bytes were not consumed", payload.length - i);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO persist in database. Probably not trivial, because reminderPosition != reminderId
|
||||
}
|
||||
|
||||
protected void handle2021Connection(final byte[] payload) {
|
||||
switch (payload[0]) {
|
||||
case CONNECTION_CMD_MTU_RESPONSE:
|
||||
|
@ -1009,7 +1009,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
|
||||
sendReminders(builder, reminders);
|
||||
}
|
||||
|
||||
protected void sendReminders(final TransactionBuilder builder, final List<? extends Reminder> reminders) {
|
||||
private void sendReminders(final TransactionBuilder builder, final List<? extends Reminder> reminders) {
|
||||
final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(gbDevice);
|
||||
|
||||
final Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
|
||||
@ -1032,7 +1032,7 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
|
||||
}
|
||||
}
|
||||
|
||||
protected void sendReminderToDevice(final TransactionBuilder builder, int position, final Reminder reminder) {
|
||||
private void sendReminderToDevice(final TransactionBuilder builder, int position, final Reminder reminder) {
|
||||
if (characteristicChunked == null) {
|
||||
LOG.warn("characteristicChunked is null, not sending reminder");
|
||||
return;
|
||||
@ -4085,11 +4085,6 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
|
||||
return this;
|
||||
}
|
||||
|
||||
public HuamiSupport requestDisplayItems(TransactionBuilder builder) {
|
||||
LOG.warn("Function not implemented");
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String customStringFilter(String inputString) {
|
||||
if (HuamiCoordinator.getUseCustomFont(gbDevice.getAddress())) {
|
||||
|
@ -18,9 +18,13 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public abstract class AbstractZeppOsService {
|
||||
@ -54,6 +58,15 @@ public abstract class AbstractZeppOsService {
|
||||
return mSupport;
|
||||
}
|
||||
|
||||
protected Huami2021Coordinator getCoordinator() {
|
||||
final DeviceCoordinator coordinator = DeviceHelper.getInstance().getCoordinator(getSupport().getDevice());
|
||||
return (Huami2021Coordinator) coordinator;
|
||||
}
|
||||
|
||||
protected Prefs getDevicePrefs() {
|
||||
return new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getSupport().getDevice().getAddress()));
|
||||
}
|
||||
|
||||
protected void write(final String taskName, final byte b) {
|
||||
this.write(taskName, new byte[]{b});
|
||||
}
|
||||
|
@ -25,6 +25,8 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PR
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_NIGHT_MODE_END;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst.PREF_NIGHT_MODE_START;
|
||||
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
@ -126,6 +128,34 @@ public class ZeppOsConfigService extends AbstractZeppOsService {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(TransactionBuilder builder) {
|
||||
requestAllConfigs(builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSendConfiguration(final String prefKey, Prefs prefs) {
|
||||
if (!PREF_TO_CONFIG.containsKey(prefKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final ConfigSetter configSetter = new ConfigSetter();
|
||||
if (setConfig(prefs, prefKey, configSetter)) {
|
||||
try {
|
||||
// If the ConfigSetter was able to set the config, just write it and return
|
||||
final TransactionBuilder builder = new TransactionBuilder("Sending configuration for " + prefKey);
|
||||
configSetter.write(builder);
|
||||
builder.queue(getSupport().getQueue());
|
||||
} catch (final Exception e) {
|
||||
GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean sentFitnessGoal = false;
|
||||
|
||||
private void handle2021ConfigResponse(final byte[] payload) {
|
||||
|
@ -0,0 +1,377 @@
|
||||
/* 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.huami.zeppos.services;
|
||||
|
||||
import static org.apache.commons.lang3.ArrayUtils.subarray;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021MenuType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.MapUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
public class ZeppOsDisplayItemsService extends AbstractZeppOsService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsDisplayItemsService.class);
|
||||
|
||||
private static final short ENDPOINT = 0x0026;
|
||||
|
||||
public static final byte CMD_CAPABILITIES_REQUEST = 0x01;
|
||||
public static final byte CMD_CAPABILITIES_RESPONSE = 0x02;
|
||||
public static final byte CMD_REQUEST = 0x03;
|
||||
public static final byte CMD_RESPONSE = 0x04;
|
||||
public static final byte CMD_CREATE = 0x05;
|
||||
public static final byte CMD_CREATE_ACK = 0x06;
|
||||
|
||||
public static final byte DISPLAY_ITEMS_MENU = 0x01;
|
||||
public static final byte DISPLAY_ITEMS_SHORTCUTS = 0x02;
|
||||
public static final byte DISPLAY_ITEMS_CONTROL_CENTER = 0x03;
|
||||
|
||||
public static final byte DISPLAY_ITEMS_SECTION_MAIN = 0x01;
|
||||
public static final byte DISPLAY_ITEMS_SECTION_MORE = 0x02;
|
||||
public static final byte DISPLAY_ITEMS_SECTION_DISABLED = 0x03;
|
||||
|
||||
public ZeppOsDisplayItemsService(final Huami2021Support support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getEndpoint() {
|
||||
return ENDPOINT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(final byte[] payload) {
|
||||
switch (payload[0]) {
|
||||
case CMD_RESPONSE:
|
||||
decodeAndUpdateDisplayItems(payload);
|
||||
break;
|
||||
case CMD_CREATE_ACK:
|
||||
LOG.info("Display items set ACK, type = {}, status = {}", payload[1], payload[2]);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unexpected display items payload byte {}", String.format("0x%02x", payload[0]));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSendConfiguration(final String config, final Prefs prefs) {
|
||||
switch (config) {
|
||||
case HuamiConst.PREF_DISPLAY_ITEMS:
|
||||
case HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE:
|
||||
setDisplayItems(
|
||||
DISPLAY_ITEMS_MENU,
|
||||
new ArrayList<>(prefs.getList(Huami2021Coordinator.getPrefPossibleValuesKey(HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE), Collections.emptyList())),
|
||||
new ArrayList<>(prefs.getList(HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE, Collections.emptyList()))
|
||||
);
|
||||
return true;
|
||||
case HuamiConst.PREF_SHORTCUTS:
|
||||
case HuamiConst.PREF_SHORTCUTS_SORTABLE:
|
||||
setDisplayItems(
|
||||
DISPLAY_ITEMS_SHORTCUTS,
|
||||
new ArrayList<>(prefs.getList(Huami2021Coordinator.getPrefPossibleValuesKey(HuamiConst.PREF_SHORTCUTS_SORTABLE), Collections.emptyList())),
|
||||
new ArrayList<>(prefs.getList(HuamiConst.PREF_SHORTCUTS_SORTABLE, Collections.emptyList()))
|
||||
);
|
||||
return true;
|
||||
case HuamiConst.PREF_CONTROL_CENTER_SORTABLE:
|
||||
setDisplayItems(
|
||||
DISPLAY_ITEMS_CONTROL_CENTER,
|
||||
new ArrayList<>(prefs.getList(Huami2021Coordinator.getPrefPossibleValuesKey(HuamiConst.PREF_CONTROL_CENTER_SORTABLE), Collections.emptyList())),
|
||||
new ArrayList<>(prefs.getList(HuamiConst.PREF_CONTROL_CENTER_SORTABLE, Collections.emptyList()))
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(final TransactionBuilder builder) {
|
||||
requestItems(builder, DISPLAY_ITEMS_MENU);
|
||||
requestItems(builder, DISPLAY_ITEMS_SHORTCUTS);
|
||||
if (getCoordinator().supportsControlCenter()) {
|
||||
requestItems(builder, DISPLAY_ITEMS_CONTROL_CENTER);
|
||||
}
|
||||
}
|
||||
|
||||
public void requestItems(final TransactionBuilder builder, final byte type) {
|
||||
LOG.info("Requesting display items type={}", type);
|
||||
|
||||
write(builder, new byte[]{CMD_REQUEST, type});
|
||||
}
|
||||
|
||||
private void decodeAndUpdateDisplayItems(final byte[] payload) {
|
||||
LOG.info("Got display items from band, type={}", payload[1]);
|
||||
|
||||
final int numberScreens = payload[2];
|
||||
final int expectedLength = 4 + numberScreens * 12;
|
||||
if (payload.length != 4 + numberScreens * 12) {
|
||||
LOG.error("Unexpected display items payload length {}, expected {}", payload.length, expectedLength);
|
||||
return;
|
||||
}
|
||||
|
||||
final String prefKey;
|
||||
final Map<String, String> idMap;
|
||||
switch (payload[1]) {
|
||||
case DISPLAY_ITEMS_MENU:
|
||||
LOG.info("Got {} display items", numberScreens);
|
||||
prefKey = HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE;
|
||||
idMap = Huami2021MenuType.displayItemNameLookup;
|
||||
break;
|
||||
case DISPLAY_ITEMS_SHORTCUTS:
|
||||
LOG.info("Got {} shortcuts", numberScreens);
|
||||
prefKey = HuamiConst.PREF_SHORTCUTS_SORTABLE;
|
||||
idMap = Huami2021MenuType.shortcutsNameLookup;
|
||||
break;
|
||||
case DISPLAY_ITEMS_CONTROL_CENTER:
|
||||
LOG.info("Got {} control center", numberScreens);
|
||||
prefKey = HuamiConst.PREF_CONTROL_CENTER_SORTABLE;
|
||||
idMap = Huami2021MenuType.controlCenterNameLookup;
|
||||
break;
|
||||
default:
|
||||
LOG.error("Unknown display items type {}", String.format("0x%x", payload[1]));
|
||||
return;
|
||||
}
|
||||
final String allScreensPrefKey = Huami2021Coordinator.getPrefPossibleValuesKey(prefKey);
|
||||
|
||||
final boolean menuHasMoreSection;
|
||||
|
||||
if (payload[1] == DISPLAY_ITEMS_MENU) {
|
||||
menuHasMoreSection = getCoordinator().mainMenuHasMoreSection();
|
||||
} else {
|
||||
menuHasMoreSection = false;
|
||||
}
|
||||
|
||||
final String[] mainScreensArr = new String[numberScreens];
|
||||
final String[] moreScreensArr = new String[numberScreens];
|
||||
final List<String> allScreens = new LinkedList<>();
|
||||
if (menuHasMoreSection) {
|
||||
// The band doesn't report the "more" screen, so we add it
|
||||
allScreens.add("more");
|
||||
}
|
||||
|
||||
for (int i = 0; i < numberScreens; i++) {
|
||||
// Screen IDs are sent as literal hex strings
|
||||
final String screenId = new String(subarray(payload, 4 + i * 12, 4 + i * 12 + 8));
|
||||
final String screenNameOrId = idMap.containsKey(screenId) ? idMap.get(screenId) : screenId;
|
||||
allScreens.add(screenNameOrId);
|
||||
|
||||
final int screenSectionVal = payload[4 + i * 12 + 9];
|
||||
final int screenPosition = payload[4 + i * 12 + 10];
|
||||
|
||||
if (screenPosition >= numberScreens) {
|
||||
LOG.warn("Invalid screen position {}, ignoring", screenPosition);
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (screenSectionVal) {
|
||||
case DISPLAY_ITEMS_SECTION_MAIN:
|
||||
if (mainScreensArr[screenPosition] != null) {
|
||||
LOG.warn("Duplicate position {} for main section", screenPosition);
|
||||
}
|
||||
//LOG.debug("mainScreensArr[{}] = {}", screenPosition, screenKey);
|
||||
mainScreensArr[screenPosition] = screenNameOrId;
|
||||
break;
|
||||
case DISPLAY_ITEMS_SECTION_MORE:
|
||||
if (moreScreensArr[screenPosition] != null) {
|
||||
LOG.warn("Duplicate position {} for more section", screenPosition);
|
||||
}
|
||||
//LOG.debug("moreScreensArr[{}] = {}", screenPosition, screenKey);
|
||||
moreScreensArr[screenPosition] = screenNameOrId;
|
||||
break;
|
||||
case DISPLAY_ITEMS_SECTION_DISABLED:
|
||||
// Ignore disabled screens
|
||||
//LOG.debug("Ignoring disabled screen {} {}", screenPosition, screenKey);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unknown screen section {}, ignoring", String.format("0x%02x", screenSectionVal));
|
||||
}
|
||||
}
|
||||
|
||||
final List<String> screens = new ArrayList<>(Arrays.asList(mainScreensArr));
|
||||
if (menuHasMoreSection) {
|
||||
screens.add("more");
|
||||
screens.addAll(Arrays.asList(moreScreensArr));
|
||||
}
|
||||
screens.removeAll(Collections.singleton(null));
|
||||
|
||||
final String allScreensPrefValue = StringUtils.join(",", allScreens.toArray(new String[0])).toString();
|
||||
final String prefValue = StringUtils.join(",", screens.toArray(new String[0])).toString();
|
||||
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences()
|
||||
.withPreference(allScreensPrefKey, allScreensPrefValue)
|
||||
.withPreference(prefKey, prefValue);
|
||||
|
||||
evaluateGBDeviceEvent(eventUpdatePreferences);
|
||||
}
|
||||
|
||||
private void setDisplayItems(final byte menuType,
|
||||
final List<String> allSettings,
|
||||
List<String> enabledList) {
|
||||
try {
|
||||
final TransactionBuilder builder = new TransactionBuilder("set display items type " + menuType);
|
||||
setDisplayItems(builder, menuType, allSettings, enabledList);
|
||||
builder.queue(getSupport().getQueue());
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to set display items", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void setDisplayItems(final TransactionBuilder builder,
|
||||
final byte menuType,
|
||||
final List<String> allSettings,
|
||||
List<String> enabledList) {
|
||||
final boolean isMainMenu = menuType == DISPLAY_ITEMS_MENU;
|
||||
final boolean isShortcuts = menuType == DISPLAY_ITEMS_SHORTCUTS;
|
||||
final boolean hasMoreSection;
|
||||
final Map<String, String> idMap;
|
||||
|
||||
switch (menuType) {
|
||||
case DISPLAY_ITEMS_MENU:
|
||||
LOG.info("Setting menu items");
|
||||
hasMoreSection = getCoordinator().mainMenuHasMoreSection();
|
||||
idMap = MapUtils.reverse(Huami2021MenuType.displayItemNameLookup);
|
||||
break;
|
||||
case DISPLAY_ITEMS_SHORTCUTS:
|
||||
LOG.info("Setting shortcuts");
|
||||
hasMoreSection = false;
|
||||
idMap = MapUtils.reverse(Huami2021MenuType.shortcutsNameLookup);
|
||||
break;
|
||||
case DISPLAY_ITEMS_CONTROL_CENTER:
|
||||
LOG.info("Setting control center");
|
||||
hasMoreSection = false;
|
||||
idMap = MapUtils.reverse(Huami2021MenuType.controlCenterNameLookup);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unknown menu type {}", menuType);
|
||||
return;
|
||||
}
|
||||
|
||||
if (allSettings.isEmpty()) {
|
||||
LOG.warn("List of all display items is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMainMenu && !enabledList.contains("settings")) {
|
||||
// Settings can't be disabled
|
||||
enabledList.add("settings");
|
||||
}
|
||||
|
||||
if (isShortcuts && enabledList.size() > 10) {
|
||||
// Enforced by official app
|
||||
LOG.warn("Truncating shortcuts list to 10");
|
||||
enabledList = enabledList.subList(0, 10);
|
||||
}
|
||||
|
||||
LOG.info("Setting display items (shortcuts={}): {}", isShortcuts, enabledList);
|
||||
|
||||
int numItems = allSettings.size();
|
||||
if (hasMoreSection) {
|
||||
// Exclude the "more" item from the main menu, since it's not a real item
|
||||
numItems--;
|
||||
}
|
||||
|
||||
final ByteBuffer buf = ByteBuffer.allocate(4 + numItems * 12);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
buf.put(CMD_CREATE);
|
||||
buf.put(menuType);
|
||||
buf.put((byte) numItems);
|
||||
buf.put((byte) 0x00);
|
||||
|
||||
byte pos = 0;
|
||||
boolean inMoreSection = false;
|
||||
|
||||
// IDs are 8-char hex strings, in upper case
|
||||
final Pattern ID_REGEX = Pattern.compile("^[0-9A-F]{8}$");
|
||||
|
||||
for (final String name : enabledList) {
|
||||
if (name.equals("more")) {
|
||||
inMoreSection = true;
|
||||
pos = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
final String id = idMap.containsKey(name) ? idMap.get(name) : name;
|
||||
if (!ID_REGEX.matcher(id).find()) {
|
||||
LOG.error("Screen item id '{}' is not 8-char hex string", id);
|
||||
continue;
|
||||
}
|
||||
|
||||
final byte sectionKey;
|
||||
if (inMoreSection) {
|
||||
// In more section
|
||||
sectionKey = DISPLAY_ITEMS_SECTION_MORE;
|
||||
} else {
|
||||
// In main section
|
||||
sectionKey = DISPLAY_ITEMS_SECTION_MAIN;
|
||||
}
|
||||
|
||||
// Screen IDs are sent as literal hex strings
|
||||
buf.put(id.getBytes(StandardCharsets.UTF_8));
|
||||
buf.put((byte) 0);
|
||||
buf.put(sectionKey);
|
||||
buf.put(pos++);
|
||||
buf.put((byte) (id.equals("00000013") ? 1 : 0));
|
||||
}
|
||||
|
||||
// Set all disabled items
|
||||
pos = 0;
|
||||
for (final String name : allSettings) {
|
||||
if (enabledList.contains(name) || name.equals("more")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String id = idMap.containsKey(name) ? idMap.get(name) : name;
|
||||
if (!ID_REGEX.matcher(id).find()) {
|
||||
LOG.error("Screen item id '{}' is not 8-char hex string", id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Screen IDs are sent as literal hex strings
|
||||
buf.put(id.getBytes(StandardCharsets.UTF_8));
|
||||
buf.put((byte) 0);
|
||||
buf.put(DISPLAY_ITEMS_SECTION_DISABLED);
|
||||
buf.put(pos++);
|
||||
buf.put((byte) (id.equals("00000013") ? 1 : 0));
|
||||
}
|
||||
|
||||
write(builder, buf.array());
|
||||
}
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
/* 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.huami.zeppos.services;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
public class ZeppOsHttpService extends AbstractZeppOsService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsHttpService.class);
|
||||
|
||||
private static final short ENDPOINT = 0x0001;
|
||||
|
||||
public static final byte CMD_REQUEST = 0x01;
|
||||
public static final byte CMD_RESPONSE = 0x02;
|
||||
|
||||
public static final byte RESPONSE_SUCCESS = 0x01;
|
||||
public static final byte RESPONSE_NO_INTERNET = 0x02;
|
||||
|
||||
public ZeppOsHttpService(final Huami2021Support support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getEndpoint() {
|
||||
return ENDPOINT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(final byte[] payload) {
|
||||
switch (payload[0]) {
|
||||
case CMD_REQUEST:
|
||||
int pos = 1;
|
||||
final byte requestId = payload[pos++];
|
||||
final String method = StringUtils.untilNullTerminator(payload, pos);
|
||||
if (method == null) {
|
||||
LOG.error("Failed to decode method from payload");
|
||||
return;
|
||||
}
|
||||
pos += method.length() + 1;
|
||||
final String url = StringUtils.untilNullTerminator(payload, pos);
|
||||
if (url == null) {
|
||||
LOG.error("Failed to decode method from payload");
|
||||
return;
|
||||
}
|
||||
// headers after pos += url.length() + 1;
|
||||
|
||||
LOG.info("Got HTTP {} request: {}", method, url);
|
||||
|
||||
handleUrlRequest(requestId, method, url);
|
||||
return;
|
||||
default:
|
||||
LOG.warn("Unexpected HTTP payload byte {}", String.format("0x%02x", payload[0]));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUrlRequest(final byte requestId, final String method, final String urlString) {
|
||||
if (!"GET".equals(method)) {
|
||||
LOG.error("Unable to handle HTTP method {}", method);
|
||||
// TODO: There's probably a "BAD REQUEST" response or similar
|
||||
replyHttpNoInternet(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
final URL url;
|
||||
try {
|
||||
url = new URL(urlString);
|
||||
} catch (final MalformedURLException e) {
|
||||
LOG.error("Failed to parse url", e);
|
||||
replyHttpNoInternet(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
final String path = url.getPath();
|
||||
final Map<String, String> query = urlQueryParameters(url);
|
||||
|
||||
if (path.startsWith("/weather/")) {
|
||||
final Huami2021Weather.Response response = Huami2021Weather.handleHttpRequest(path, query);
|
||||
replyHttpSuccess(requestId, response.getHttpStatusCode(), response.toJson());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.error("Unhandled URL {}", url);
|
||||
replyHttpNoInternet(requestId);
|
||||
}
|
||||
|
||||
private Map<String, String> urlQueryParameters(final URL url) {
|
||||
final Map<String, String> queryParameters = new HashMap<>();
|
||||
final String[] pairs = url.getQuery().split("&");
|
||||
for (final String pair : pairs) {
|
||||
final String[] parts = pair.split("=", 2);
|
||||
try {
|
||||
final String key = URLDecoder.decode(parts[0], "UTF-8");
|
||||
if (parts.length == 2) {
|
||||
queryParameters.put(key, URLDecoder.decode(parts[1], "UTF-8"));
|
||||
} else {
|
||||
queryParameters.put(key, "");
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to decode query", e);
|
||||
}
|
||||
}
|
||||
return queryParameters;
|
||||
}
|
||||
|
||||
private void replyHttpNoInternet(final byte requestId) {
|
||||
LOG.info("Replying with no internet to http request {}", requestId);
|
||||
|
||||
final byte[] cmd = new byte[]{CMD_RESPONSE, requestId, RESPONSE_NO_INTERNET, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
write("http reply no internet", cmd);
|
||||
}
|
||||
|
||||
private void replyHttpSuccess(final byte requestId, final int status, final String content) {
|
||||
LOG.debug("Replying with http {} request {} with {}", status, requestId, content);
|
||||
|
||||
final byte[] contentBytes = content.getBytes(StandardCharsets.UTF_8);
|
||||
final ByteBuffer buf = ByteBuffer.allocate(8 + contentBytes.length);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
buf.put(CMD_RESPONSE);
|
||||
buf.put(requestId);
|
||||
buf.put(RESPONSE_SUCCESS);
|
||||
buf.put((byte) status);
|
||||
buf.putInt(contentBytes.length);
|
||||
buf.put(contentBytes);
|
||||
|
||||
write("http reply success", buf.array());
|
||||
}
|
||||
}
|
@ -0,0 +1,267 @@
|
||||
/* 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.huami.zeppos.services;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Reminder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.Huami2021Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
public class ZeppOsRemindersService extends AbstractZeppOsService {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsRemindersService.class);
|
||||
|
||||
private static final short ENDPOINT = 0x0038;
|
||||
|
||||
private static final byte CMD_CAPABILITIES_REQUEST = 0x01;
|
||||
private static final byte CMD_CAPABILITIES_RESPONSE = 0x02;
|
||||
private static final byte CMD_REQUEST = 0x03;
|
||||
private static final byte CMD_RESPONSE = 0x04;
|
||||
private static final byte CMD_CREATE = 0x05;
|
||||
private static final byte CMD_CREATE_ACK = 0x06;
|
||||
private static final byte CMD_UPDATE = 0x07;
|
||||
private static final byte CMD_UPDATE_ACK = 0x08;
|
||||
private static final byte CMD_DELETE = 0x09;
|
||||
private static final byte CMD_DELETE_ACK = 0x0a;
|
||||
|
||||
private static final int FLAG_ENABLED = 0x0001;
|
||||
private static final int FLAG_TEXT = 0x0008;
|
||||
private static final int FLAG_REPEAT_MONTH = 0x1000;
|
||||
private static final int FLAG_REPEAT_YEAR = 0x2000;
|
||||
|
||||
private static final String PREF_CAPABILITY = "huami_2021_capability_reminders";
|
||||
|
||||
public ZeppOsRemindersService(final Huami2021Support support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getEndpoint() {
|
||||
return ENDPOINT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePayload(final byte[] payload) {
|
||||
switch (payload[0]) {
|
||||
case CMD_CAPABILITIES_RESPONSE:
|
||||
LOG.info("Reminder capability, version = {}", payload[1]);
|
||||
if (payload[1] != 1) {
|
||||
LOG.warn("Reminder unsupported version {}", payload[1]);
|
||||
return;
|
||||
}
|
||||
final int numReminders = payload[2] & 0xff;
|
||||
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences(
|
||||
PREF_CAPABILITY,
|
||||
numReminders
|
||||
);
|
||||
evaluateGBDeviceEvent(eventUpdatePreferences);
|
||||
return;
|
||||
case CMD_CREATE_ACK:
|
||||
LOG.info("Reminder create ACK, status = {}", payload[1]);
|
||||
return;
|
||||
case CMD_DELETE_ACK:
|
||||
LOG.info("Reminder delete ACK, status = {}", payload[1]);
|
||||
// status 1 = success
|
||||
// status 2 = reminder not found
|
||||
return;
|
||||
case CMD_UPDATE_ACK:
|
||||
LOG.info("Reminder update ACK, status = {}", payload[1]);
|
||||
return;
|
||||
case CMD_RESPONSE:
|
||||
LOG.info("Got reminders from band");
|
||||
decodeAndUpdateReminders(payload);
|
||||
return;
|
||||
default:
|
||||
LOG.warn("Unexpected reminders payload byte {}", String.format("0x%02x", payload[0]));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(final TransactionBuilder builder) {
|
||||
requestCapabilities(builder);
|
||||
//requestReminders(builder);
|
||||
sendReminders(builder);
|
||||
}
|
||||
|
||||
private void requestCapabilities(final TransactionBuilder builder) {
|
||||
write(builder, CMD_CAPABILITIES_REQUEST);
|
||||
}
|
||||
|
||||
private void requestReminders(final TransactionBuilder builder) {
|
||||
write(builder, CMD_REQUEST);
|
||||
}
|
||||
|
||||
public void sendReminders(final TransactionBuilder builder) {
|
||||
final List<? extends Reminder> reminders = DBHelper.getReminders(getSupport().getDevice());
|
||||
sendReminders(builder, reminders);
|
||||
}
|
||||
|
||||
public void sendReminders(final TransactionBuilder builder, final List<? extends Reminder> reminders) {
|
||||
LOG.info("On Set Reminders: {}", reminders.size());
|
||||
|
||||
final int reminderSlotCount = getCoordinator().getReminderSlotCount(getSupport().getDevice());
|
||||
if (reminderSlotCount <= 0) {
|
||||
LOG.warn("Reminders not yet initialized");
|
||||
return;
|
||||
}
|
||||
|
||||
// Send the reminders, skipping the reserved slots for calendar events
|
||||
for (int i = 0; i < reminders.size(); i++) {
|
||||
LOG.debug("Sending reminder at position {}", i);
|
||||
}
|
||||
|
||||
// Delete the remaining slots, skipping the sent reminders
|
||||
for (int i = reminders.size(); i < reminderSlotCount; i++) {
|
||||
LOG.debug("Deleting reminder at position {}", i);
|
||||
|
||||
sendReminderToDevice(builder, i, null);
|
||||
}
|
||||
}
|
||||
|
||||
protected void sendReminderToDevice(final TransactionBuilder builder, int position, final Reminder reminder) {
|
||||
final DeviceCoordinator coordinator = getCoordinator();
|
||||
final int reminderSlotCount = coordinator.getReminderSlotCount(getSupport().getDevice());
|
||||
if (position + 1 > reminderSlotCount) {
|
||||
LOG.error("Reminder for position {} is over the limit of {} reminders", position, reminderSlotCount);
|
||||
return;
|
||||
}
|
||||
|
||||
if (reminder == null) {
|
||||
// Delete reminder
|
||||
write(builder, new byte[]{CMD_DELETE, (byte) (position & 0xFF)});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
final String message;
|
||||
if (reminder.getMessage().length() > coordinator.getMaximumReminderMessageLength()) {
|
||||
LOG.warn("The reminder message length {} is longer than {}, will be truncated",
|
||||
reminder.getMessage().length(),
|
||||
coordinator.getMaximumReminderMessageLength()
|
||||
);
|
||||
message = StringUtils.truncate(reminder.getMessage(), coordinator.getMaximumReminderMessageLength());
|
||||
} else {
|
||||
message = reminder.getMessage();
|
||||
}
|
||||
|
||||
final ByteBuffer buf = ByteBuffer.allocate(1 + 10 + message.getBytes(StandardCharsets.UTF_8).length + 1);
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
// Update does an upsert, so let's use it. If we call create twice on the same ID, it becomes weird
|
||||
buf.put(CMD_UPDATE);
|
||||
buf.put((byte) (position & 0xFF));
|
||||
|
||||
final Calendar cal = BLETypeConversions.createCalendar();
|
||||
cal.setTime(reminder.getDate());
|
||||
|
||||
int reminderFlags = FLAG_ENABLED | FLAG_TEXT;
|
||||
|
||||
switch (reminder.getRepetition()) {
|
||||
case Reminder.ONCE:
|
||||
// Default is once, nothing to do
|
||||
break;
|
||||
case Reminder.EVERY_DAY:
|
||||
reminderFlags |= 0x0fe0; // all week day bits set
|
||||
break;
|
||||
case Reminder.EVERY_WEEK:
|
||||
int dayOfWeek = BLETypeConversions.dayOfWeekToRawBytes(cal) - 1; // Monday = 0
|
||||
reminderFlags |= 0x20 << dayOfWeek;
|
||||
break;
|
||||
case Reminder.EVERY_MONTH:
|
||||
reminderFlags |= FLAG_REPEAT_MONTH;
|
||||
break;
|
||||
case Reminder.EVERY_YEAR:
|
||||
reminderFlags |= FLAG_REPEAT_YEAR;
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unknown repetition for reminder in position {}, defaulting to once", position);
|
||||
}
|
||||
|
||||
buf.putInt(reminderFlags);
|
||||
|
||||
buf.putInt((int) (cal.getTimeInMillis() / 1000L));
|
||||
buf.put((byte) 0x00);
|
||||
|
||||
buf.put(message.getBytes(StandardCharsets.UTF_8));
|
||||
buf.put((byte) 0x00);
|
||||
|
||||
write(builder, buf.array());
|
||||
}
|
||||
|
||||
private void decodeAndUpdateReminders(final byte[] payload) {
|
||||
final int numReminders = payload[1];
|
||||
|
||||
if (payload.length < 3 + numReminders * 11) {
|
||||
LOG.warn("Unexpected payload length of {} for {} reminders", payload.length, numReminders);
|
||||
return;
|
||||
}
|
||||
|
||||
int i = 3;
|
||||
while (i < payload.length) {
|
||||
if (payload.length - i < 11) {
|
||||
LOG.error("Not enough bytes remaining to parse a reminder ({})", payload.length - i);
|
||||
return;
|
||||
}
|
||||
|
||||
final int reminderPosition = payload[i++] & 0xff;
|
||||
final int reminderFlags = BLETypeConversions.toUint32(payload, i);
|
||||
i += 4;
|
||||
final int reminderTimestamp = BLETypeConversions.toUint32(payload, i);
|
||||
i += 4;
|
||||
i++; // 0 ?
|
||||
final Date reminderDate = new Date(reminderTimestamp * 1000L);
|
||||
final String reminderText = StringUtils.untilNullTerminator(payload, i);
|
||||
if (reminderText == null) {
|
||||
LOG.error("Failed to parse reminder text at pos {}", i);
|
||||
return;
|
||||
}
|
||||
|
||||
i += reminderText.length() + 1;
|
||||
|
||||
LOG.info("Reminder {}, {}, {}, {}", reminderPosition, String.format("0x%04x", reminderFlags), reminderDate, reminderText);
|
||||
}
|
||||
if (i != payload.length) {
|
||||
LOG.error("Unexpected reminders payload trailer, {} bytes were not consumed", payload.length - i);
|
||||
}
|
||||
|
||||
// TODO persist in database. Probably not trivial, because reminderPosition != reminderId
|
||||
}
|
||||
|
||||
public static int getSlotCount(final Prefs devicePrefs) {
|
||||
return devicePrefs.getInt(PREF_CAPABILITY, 0);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user