1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-28 21:06:50 +01:00

Huami/Xiaomi: centralize handling of device state events

Gadgetbridge can be configured to perform an action when a
Huami device is taken off or the user was detected to fall asleep or
wake up. This functionality was specific to Huami devices, but this
changeset moves this upstream to the AbstractDeviceSupport class in
combination with new GBDeviceEvents.

Now that the ADS has centralized support for this functionality, the
same logic can be used for other devices. In this case, an
implementation is added for supported Xiaomi devices.
This commit is contained in:
MrYoranimo 2023-12-07 01:50:14 +01:00
parent 2ff92c73f8
commit 435d41aca0
14 changed files with 504 additions and 114 deletions

View File

@ -382,4 +382,12 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_FEMOMETER_MEASUREMENT_MODE = "femometer_measurement_mode";
public static final String PREF_PREFIX_NOTIFICATION_WITH_APP = "pref_prefix_notification_with_app";
public static final String PREF_DEVICE_ACTION_SELECTION_OFF = "UNKNOWN";
public static final String PREF_DEVICE_ACTION_SELECTION_BROADCAST = "BROADCAST";
public static final String PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION = "events_forwarding_fellsleep_action_selection";
public static final String PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST = "prefs_events_forwarding_fellsleep_broadcast";
public static final String PREF_DEVICE_ACTION_WOKE_UP_SELECTION = "events_forwarding_wokeup_action_selection";
public static final String PREF_DEVICE_ACTION_WOKE_UP_BROADCAST = "prefs_events_forwarding_wokeup_broadcast";
public static final String PREF_DEVICE_ACTION_START_NON_WEAR_SELECTION = "events_forwarding_startnonwear_action_selection";
public static final String PREF_DEVICE_ACTION_START_NON_WEAR_BROADCAST = "prefs_events_forwarding_startnonwear_broadcast";
}

View File

@ -59,14 +59,14 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_CONTROL_CENTER_SORTABLE;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_SELECTION_BROADCAST;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_SELECTION_OFF;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_START_NON_WEAR_BROADCAST;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_START_NON_WEAR_SELECTION;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_WOKE_UP_BROADCAST;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_WOKE_UP_SELECTION;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_ACTION_SELECTION_BROADCAST;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_ACTION_SELECTION_OFF;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_ACTION_START_NON_WEAR_BROADCAST;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_ACTION_START_NON_WEAR_SELECTION;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_ACTION_WOKE_UP_BROADCAST;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_ACTION_WOKE_UP_SELECTION;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISPLAY_ITEMS;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DISPLAY_ITEMS_SORTABLE;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_EXPOSE_HR_THIRDPARTY;

View File

@ -0,0 +1,30 @@
/* Copyright (C) 2023 Yoran Vulker
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.deviceevents;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.model.SleepState;
public class GBDeviceEventSleepStateDetection extends GBDeviceEvent {
public SleepState sleepState;
@Override
public String toString() {
return super.toString() + String.format(Locale.ROOT, "sleepState=%s", sleepState);
}
}

View File

@ -0,0 +1,30 @@
/* Copyright (C) 2023 Yoran Vulker
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.deviceevents;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.model.WearingState;
public class GBDeviceEventWearState extends GBDeviceEvent {
public WearingState wearingState = WearingState.UNKNOWN;
@Override
public String toString() {
return super.toString() + String.format(Locale.ROOT, "wearingState=%s", wearingState);
}
}

View File

@ -98,15 +98,6 @@ public class HuamiConst {
public static final String PREF_BUTTON_ACTION_SELECTION_FITNESS_APP_STOP = "FITNESS_CONTROL_STOP";
public static final String PREF_BUTTON_ACTION_SELECTION_FITNESS_APP_TOGGLE = "FITNESS_CONTROL_TOGGLE";
public static final String PREF_DEVICE_ACTION_SELECTION_OFF = "UNKNOWN";
public static final String PREF_DEVICE_ACTION_SELECTION_BROADCAST = "BROADCAST";
public static final String PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION = "events_forwarding_fellsleep_action_selection";
public static final String PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST = "prefs_events_forwarding_fellsleep_broadcast";
public static final String PREF_DEVICE_ACTION_WOKE_UP_SELECTION = "events_forwarding_wokeup_action_selection";
public static final String PREF_DEVICE_ACTION_WOKE_UP_BROADCAST = "prefs_events_forwarding_wokeup_broadcast";
public static final String PREF_DEVICE_ACTION_START_NON_WEAR_SELECTION = "events_forwarding_startnonwear_action_selection";
public static final String PREF_DEVICE_ACTION_START_NON_WEAR_BROADCAST = "prefs_events_forwarding_startnonwear_broadcast";
/**
* The suffixes match the enum {@link nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiVibrationPatternNotificationType}.
*/

View File

@ -410,6 +410,9 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
settings.add(R.xml.devicesettings_contacts);
}
settings.add(R.xml.devicesettings_camera_remote);
if (supportsWearingAndSleepingDataThroughDeviceState()) {
settings.add(R.xml.devicesettings_device_actions);
}
//
// Developer
@ -487,4 +490,8 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
public boolean supportsMultipleWeatherLocations() {
return false;
}
public boolean supportsWearingAndSleepingDataThroughDeviceState() {
return false;
}
}

View File

@ -67,4 +67,9 @@ public class XiaomiWatchS1ActiveCoordinator extends XiaomiCoordinator {
public boolean supportsMultipleWeatherLocations() {
return true;
}
@Override
public boolean supportsWearingAndSleepingDataThroughDeviceState() {
return true;
}
}

View File

@ -0,0 +1,23 @@
/* Copyright (C) 2023 Yoran Vulker
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.model;
public enum SleepState {
UNKNOWN,
ASLEEP,
AWAKE,
}

View File

@ -0,0 +1,23 @@
/* Copyright (C) 2023 Yoran Vulker
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.model;
public enum WearingState {
UNKNOWN,
WEARING,
NOT_WEARING,
}

View File

@ -1,6 +1,6 @@
/* Copyright (C) 2015-2021 Andreas Böhler, Andreas Shimokawa, Carsten
/* Copyright (C) 2015-2023 Andreas Böhler, Andreas Shimokawa, Carsten
Pfeiffer, Daniele Gobbetti, José Rebelo, Pauli Salmenrinne, Sebastian Kranz,
Taavi Eomäe
Taavi Eomäe, Yoran Vulker
This file is part of Gadgetbridge.
@ -56,6 +56,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.FindPhoneActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AbstractAppManagerFragment;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.capabilities.loyaltycards.LoyaltyCard;
import nodomain.freeyourgadget.gadgetbridge.database.DBAccess;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
@ -67,6 +68,7 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallContro
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventDisplayMessage;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFmFrequency;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSleepStateDetection;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventLEDColor;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
@ -75,10 +77,12 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDevi
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventScreenshot;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventWearState;
import nodomain.freeyourgadget.gadgetbridge.entities.BatteryLevel;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
@ -90,6 +94,8 @@ 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.SleepState;
import nodomain.freeyourgadget.gadgetbridge.model.WearingState;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.model.WorldClock;
import nodomain.freeyourgadget.gadgetbridge.model.NavigationInfoSpec;
@ -208,6 +214,10 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
handleGBDeviceEvent((GBDeviceEventUpdateDeviceState) deviceEvent);
} else if (deviceEvent instanceof GBDeviceEventFmFrequency) {
handleGBDeviceEvent((GBDeviceEventFmFrequency) deviceEvent);
} else if (deviceEvent instanceof GBDeviceEventWearState) {
handleGBDeviceEvent((GBDeviceEventWearState) deviceEvent);
} else if (deviceEvent instanceof GBDeviceEventSleepStateDetection) {
handleGBDeviceEvent((GBDeviceEventSleepStateDetection) deviceEvent);
}
}
@ -521,6 +531,133 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
gbDevice.sendDeviceUpdateIntent(context);
}
/**
* Helper method to run specific actions configured in the device preferences, upon wear state
* or awake/asleep events.
*
* @param action
* @param message
*/
private void handleDeviceAction(String action, String message) {
String actionDisabled = getContext().getString(R.string.pref_button_action_disabled_value);
if (actionDisabled.equals(action)) {
return;
}
final String actionBroadcast = getContext().getString(R.string.pref_device_action_broadcast_value);
final String actionFitnessControlStart = getContext().getString(R.string.pref_device_action_fitness_app_control_start_value);
final String actionFitnessControlStop = getContext().getString(R.string.pref_device_action_fitness_app_control_stop_value);
final String actionFitnessControlToggle = getContext().getString(R.string.pref_device_action_fitness_app_control_toggle_value);
final String actionMediaPlay = getContext().getString(R.string.pref_media_play_value);
final String actionMediaPause = getContext().getString(R.string.pref_media_pause_value);
final String actionMediaPlayPause = getContext().getString(R.string.pref_media_playpause_value);
if (actionBroadcast.equals(action)) {
if (message != null) {
Intent in = new Intent();
in.setAction(message);
LOG.info("Sending broadcast " + message);
getContext().getApplicationContext().sendBroadcast(in);
return;
}
}
if (actionFitnessControlStart.equals(action)) {
OpenTracksController.startRecording(getContext());
return;
}
if (actionFitnessControlStop.equals(action)) {
OpenTracksController.stopRecording(getContext());
return;
}
if (actionFitnessControlToggle.equals(action)) {
OpenTracksController.toggleRecording(getContext());
return;
}
if (actionMediaPlay.equals(action) ||
actionMediaPause.equals(action) ||
actionMediaPlayPause.equals(action)
) {
GBDeviceEventMusicControl deviceEventMusicControl = new GBDeviceEventMusicControl();
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.valueOf(action);
evaluateGBDeviceEvent(deviceEventMusicControl);
return;
}
LOG.warn("Unhandled device state change action (action: {}, message: {})", action, message);
}
private void handleGBDeviceEvent(GBDeviceEventSleepStateDetection event) {
LOG.debug("Got SLEEP_STATE_DETECTION device event, detected sleep state = {}", event.sleepState);
if (event.sleepState == SleepState.UNKNOWN) {
return;
}
String actionDisabled = getContext().getString(R.string.pref_button_action_disabled_value);
String actionPreferenceKey, messagePreferenceKey;
int defaultBroadcastMessageResource;
switch (event.sleepState) {
case AWAKE:
actionPreferenceKey = DeviceSettingsPreferenceConst.PREF_DEVICE_ACTION_WOKE_UP_SELECTION;
messagePreferenceKey = DeviceSettingsPreferenceConst.PREF_DEVICE_ACTION_WOKE_UP_BROADCAST;
defaultBroadcastMessageResource = R.string.prefs_events_forwarding_wokeup_broadcast_default_value;
break;
case ASLEEP:
actionPreferenceKey = DeviceSettingsPreferenceConst.PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION;
messagePreferenceKey = DeviceSettingsPreferenceConst.PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST;
defaultBroadcastMessageResource = R.string.prefs_events_forwarding_fellsleep_broadcast_default_value;
break;
default:
LOG.warn("Unable to deduce action and broadcast message preference key for sleep state {}", event.sleepState);
return;
}
String action = getDevicePrefs().getString(actionPreferenceKey, actionDisabled);
if (actionDisabled.equals(action)) {
return;
}
String broadcastMessage = getDevicePrefs().getString(messagePreferenceKey, context.getString(defaultBroadcastMessageResource));
handleDeviceAction(action, broadcastMessage);
}
private void handleGBDeviceEvent(GBDeviceEventWearState event) {
LOG.debug("Got WEAR_STATE device event, wearingState = {}", event.wearingState);
if (event.wearingState == WearingState.UNKNOWN) {
LOG.warn("WEAR_STATE state is UNKNOWN, aborting further evaluation");
return;
}
if (event.wearingState != WearingState.NOT_WEARING) {
LOG.debug("WEAR_STATE state is not NOT_WEARING, aborting further evaluation");
}
String valueDisabled = getContext().getString(R.string.pref_button_action_disabled_value);
String actionOnUnwear = getDevicePrefs().getString(
DeviceSettingsPreferenceConst.PREF_DEVICE_ACTION_START_NON_WEAR_SELECTION,
valueDisabled
);
// check if an action is set
if (actionOnUnwear.equals(valueDisabled)) {
return;
}
String broadcastMessage = getDevicePrefs().getString(
DeviceSettingsPreferenceConst.PREF_DEVICE_ACTION_START_NON_WEAR_BROADCAST,
getContext().getString(R.string.prefs_events_forwarding_startnonwear_broadcast_default_value)
);
handleDeviceAction(actionOnUnwear, broadcastMessage);
}
private StoreDataTask createStoreTask(String task, Context context, GBDeviceEventBatteryInfo deviceEvent) {
return new StoreDataTask(task, context, deviceEvent);

View File

@ -1,8 +1,8 @@
/* Copyright (C) 2015-2021 Andreas Böhler, Andreas Shimokawa, Avamander,
/* Copyright (C) 2015-2023 Andreas Böhler, Andreas Shimokawa, Avamander,
Carsten Pfeiffer, Daniel Dakhno, Daniele Gobbetti, Daniel Hauck, Dikay900,
Frank Slezak, ivanovlev, João Paulo Barraca, José Rebelo, Julien Pivotto,
Kasha, keeshii, mamucho, Martin, Matthieu Baerts, Nephiel, Sebastian Kranz,
Sergey Trofimov, Steffen Liebergeld, Taavi Eomäe, Uwe Hermann
Sergey Trofimov, Steffen Liebergeld, Taavi Eomäe, Uwe Hermann, Yoran Vulker
This file is part of Gadgetbridge.

View File

@ -1,7 +1,7 @@
/* Copyright (C) 2015-2021 Andreas Shimokawa, Carsten Pfeiffer, Christian
/* Copyright (C) 2015-2023 Andreas Shimokawa, Carsten Pfeiffer, Christian
Fischer, Daniele Gobbetti, Dmitry Markin, JohnnySun, José Rebelo, Julien
Pivotto, Kasha, Michal Novotny, Petr Vaněk, Sebastian Kranz, Sergey Trofimov,
Steffen Liebergeld, Taavi Eomäe, Zhong Jianxin
Steffen Liebergeld, Taavi Eomäe, Yoran Vulker, Zhong Jianxin
This file is part of Gadgetbridge.
@ -80,12 +80,15 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSett
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSleepStateDetection;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventWearState;
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.ActivateDisplayOnLift;
@ -121,6 +124,8 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
import nodomain.freeyourgadget.gadgetbridge.model.SleepState;
import nodomain.freeyourgadget.gadgetbridge.model.WearingState;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.AbstractFetchOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchStatisticsOperation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.operations.FetchTemperatureOperation;
@ -220,6 +225,7 @@ import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.Dev
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_TIMEFORMAT;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_USER_FITNESS_GOAL_NOTIFICATION;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_WEARLOCATION;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DEVICE_ACTION_SELECTION_OFF;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Service.WORKOUT_GPS_FLAG_POSITION;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.Huami2021Service.WORKOUT_GPS_FLAG_STATUS;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_BUTTON_ACTION_SELECTION_BROADCAST;
@ -227,13 +233,6 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_BUTTON_ACTION_SELECTION_FITNESS_APP_STOP;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_BUTTON_ACTION_SELECTION_FITNESS_APP_TOGGLE;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_BUTTON_ACTION_SELECTION_OFF;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_SELECTION_OFF;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_START_NON_WEAR_BROADCAST;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_START_NON_WEAR_SELECTION;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_WOKE_UP_BROADCAST;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_WOKE_UP_SELECTION;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_HUAMI_VIBRATION_COUNT_ALARM;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_HUAMI_VIBRATION_COUNT_APP_ALERTS;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_HUAMI_VIBRATION_COUNT_EVENT_REMINDER;
@ -1788,13 +1787,12 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
}
private void executeButtonAction(String buttonKey) {
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
String buttonPreference = prefs.getString(buttonKey, PREF_BUTTON_ACTION_SELECTION_OFF);
String buttonPreference = getDevicePrefs().getString(buttonKey, PREF_BUTTON_ACTION_SELECTION_OFF);
if (buttonPreference.equals(PREF_BUTTON_ACTION_SELECTION_OFF)) {
return;
}
if (prefs.getBoolean(HuamiConst.PREF_BUTTON_ACTION_VIBRATE, false)) {
if (getDevicePrefs().getBoolean(HuamiConst.PREF_BUTTON_ACTION_VIBRATE, false)) {
vibrateOnce();
}
switch (buttonPreference) {
@ -1815,37 +1813,6 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
}
}
protected void handleDeviceAction(String deviceAction, String message) {
if (deviceAction.equals(PREF_DEVICE_ACTION_SELECTION_OFF)) {
return;
}
switch (deviceAction) {
case PREF_BUTTON_ACTION_SELECTION_BROADCAST:
sendSystemBroadcast(message);
break;
case PREF_BUTTON_ACTION_SELECTION_FITNESS_APP_START:
OpenTracksController.startRecording(this.getContext());
break;
case PREF_BUTTON_ACTION_SELECTION_FITNESS_APP_STOP:
OpenTracksController.stopRecording(this.getContext());
break;
case PREF_BUTTON_ACTION_SELECTION_FITNESS_APP_TOGGLE:
OpenTracksController.toggleRecording(this.getContext());
break;
default:
handleMediaButton(deviceAction);
}
}
private void sendSystemBroadcast(String message){
if (message !=null) {
Intent in = new Intent();
in.setAction(message);
LOG.info("Sending broadcast " + message);
this.getContext().getApplicationContext().sendBroadcast(in);
}
}
private void sendSystemBroadcastWithButtonId() {
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
String requiredButtonPressMessage = prefs.getString(HuamiConst.PREF_BUTTON_ACTION_BROADCAST,
@ -1857,12 +1824,12 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
this.getContext().getApplicationContext().sendBroadcast(in);
}
private void handleMediaButton(String MediaAction) {
if (MediaAction.equals(PREF_DEVICE_ACTION_SELECTION_OFF)) {
private void handleMediaButton(String mediaAction) {
if (mediaAction.equals(PREF_DEVICE_ACTION_SELECTION_OFF) || mediaAction.equals(PREF_BUTTON_ACTION_SELECTION_OFF)) {
return;
}
GBDeviceEventMusicControl deviceEventMusicControl = new GBDeviceEventMusicControl();
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.valueOf(MediaAction);
deviceEventMusicControl.event = GBDeviceEventMusicControl.Event.valueOf(mediaAction);
evaluateGBDeviceEvent(deviceEventMusicControl);
}
@ -2190,34 +2157,28 @@ public abstract class HuamiSupport extends AbstractBTLEDeviceSupport implements
}
}
protected void processDeviceEvent(int event){
LOG.debug("Handling device event: " + event);
Prefs prefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()));
String deviceActionBroadcastMessage=null;
switch (event) {
protected void processDeviceEvent(int deviceEvent){
LOG.debug("Handling device event: " + deviceEvent);
GBDeviceEvent event;
switch (deviceEvent) {
case HuamiDeviceEvent.WOKE_UP:
String wakeupAction = prefs.getString(PREF_DEVICE_ACTION_WOKE_UP_SELECTION,PREF_DEVICE_ACTION_SELECTION_OFF);
if (wakeupAction.equals(PREF_DEVICE_ACTION_SELECTION_OFF)) return;
deviceActionBroadcastMessage= prefs.getString(PREF_DEVICE_ACTION_WOKE_UP_BROADCAST,
this.getContext().getString(R.string.prefs_events_forwarding_wokeup_broadcast_default_value));
handleDeviceAction(wakeupAction, deviceActionBroadcastMessage);
event = new GBDeviceEventSleepStateDetection();
((GBDeviceEventSleepStateDetection) event).sleepState = SleepState.AWAKE;
break;
case HuamiDeviceEvent.FELL_ASLEEP:
String fellsleepAction = prefs.getString(PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION,PREF_DEVICE_ACTION_SELECTION_OFF);
if (fellsleepAction.equals(PREF_DEVICE_ACTION_SELECTION_OFF)) return;
deviceActionBroadcastMessage= prefs.getString(PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST,
this.getContext().getString(R.string.prefs_events_forwarding_fellsleep_broadcast_default_value));
handleDeviceAction(fellsleepAction, deviceActionBroadcastMessage);
event = new GBDeviceEventSleepStateDetection();
((GBDeviceEventSleepStateDetection) event).sleepState = SleepState.ASLEEP;
break;
case HuamiDeviceEvent.START_NONWEAR:
String nonwearAction = prefs.getString(PREF_DEVICE_ACTION_START_NON_WEAR_SELECTION,PREF_DEVICE_ACTION_SELECTION_OFF);
if (nonwearAction.equals(PREF_DEVICE_ACTION_SELECTION_OFF)) return;
deviceActionBroadcastMessage= prefs.getString(PREF_DEVICE_ACTION_START_NON_WEAR_BROADCAST,
this.getContext().getString(R.string.prefs_events_forwarding_startnonwear_broadcast_default_value));
handleDeviceAction(nonwearAction, deviceActionBroadcastMessage);
event = new GBDeviceEventWearState();
((GBDeviceEventWearState) event).wearingState = WearingState.NOT_WEARING;
break;
default:
LOG.warn("Unhandled device event {}", deviceEvent);
return;
}
evaluateGBDeviceEvent(event);
}
private void handleLongButtonEvent(){

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2023 José Rebelo
/* Copyright (C) 2023 José Rebelo, Yoran Vulker
This file is part of Gadgetbridge.
@ -36,13 +36,17 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSett
import nodomain.freeyourgadget.gadgetbridge.capabilities.password.PasswordCapabilityImpl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSleepStateDetection;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventWearState;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiFWHelper;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.model.SleepState;
import nodomain.freeyourgadget.gadgetbridge.model.WearingState;
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetProgressAction;
@ -73,11 +77,14 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
public static final int CMD_PASSWORD_SET = 21;
public static final int CMD_DISPLAY_ITEMS_GET = 29;
public static final int CMD_DISPLAY_ITEMS_SET = 30;
public static final int CMD_DEVICE_STATE_GET = 78;
public static final int CMD_DEVICE_STATE = 79;
// Not null if we're installing a firmware
private XiaomiFWHelper fwHelper = null;
private XiaomiProto.DeviceState cachedDeviceState = null;
private WearingState currentWearingState = WearingState.UNKNOWN;
private BatteryState currentBatteryState = BatteryState.UNKNOWN;
private SleepState currentSleepDetectionState = SleepState.UNKNOWN;
public XiaomiSystemService(final XiaomiSupport support) {
super(support);
@ -87,7 +94,10 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
public void initialize() {
// Request device info and configs
getSupport().sendCommand("get device info", COMMAND_TYPE, CMD_DEVICE_INFO);
getSupport().sendCommand("get battery", COMMAND_TYPE, CMD_BATTERY);
getSupport().sendCommand("get device status", COMMAND_TYPE, CMD_DEVICE_STATE_GET);
// device status request may initialize wearing, charger, sleeping, and activity state, so
// get battery level as a failsafe for devices that don't support CMD_DEVICE_STATE_SET command
getSupport().sendCommand("get battery state", COMMAND_TYPE, CMD_BATTERY);
getSupport().sendCommand("get password", COMMAND_TYPE, CMD_PASSWORD_GET);
getSupport().sendCommand("get display items", COMMAND_TYPE, CMD_DISPLAY_ITEMS_GET);
}
@ -129,14 +139,15 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
case CMD_DISPLAY_ITEMS_GET:
handleDisplayItems(cmd.getSystem().getDisplayItems());
return;
case CMD_DEVICE_STATE_GET:
handleBasicDeviceState(cmd.getSystem().hasBasicDeviceState()
? cmd.getSystem().getBasicDeviceState()
: null);
return;
case CMD_DEVICE_STATE:
// some devices (e.g. Xiaomi Watch S1 Active) only broadcast the charger state through
// this message, so this will need to be kept cached to process when the battery levels
// get requested
cachedDeviceState = cmd.getSystem().getDeviceState();
// request battery state to request battery level and charger state on supported models
getSupport().sendCommand("request battery state", COMMAND_TYPE, CMD_BATTERY);
handleDeviceState(cmd.getSystem().hasDeviceState()
? cmd.getSystem().getDeviceState()
: null);
return;
}
@ -246,6 +257,17 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
getSupport().evaluateGBDeviceEvent(gbDeviceEventUpdateDeviceInfo);
}
private BatteryState convertBatteryStateFromRawValue(int chargerState) {
switch (chargerState) {
case 1:
return BatteryState.BATTERY_CHARGING;
case 2:
return BatteryState.BATTERY_NORMAL;
}
return BatteryState.UNKNOWN;
}
private void handleBattery(final XiaomiProto.Battery battery) {
LOG.debug("Got battery: {}", battery.getLevel());
@ -253,25 +275,18 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
batteryInfo.batteryIndex = 0;
batteryInfo.level = battery.getLevel();
int chargerState = battery.getState();
// currentBatteryState may already be set if the DeviceState message contained the field,
// but since some models report their charger state through this message, we will update it
// from here
if (battery.hasState()) {
currentBatteryState = convertBatteryStateFromRawValue(battery.getState());
// if device state is cached and the charging state there is set, take the charger status
// from there
if (cachedDeviceState != null && cachedDeviceState.hasChargingState()) {
chargerState = cachedDeviceState.getChargingState();
}
switch (chargerState) {
case 1:
batteryInfo.state = BatteryState.BATTERY_CHARGING;
break;
case 2:
batteryInfo.state = BatteryState.BATTERY_NORMAL;
break;
default:
batteryInfo.state = BatteryState.UNKNOWN;
if (currentBatteryState == BatteryState.UNKNOWN) {
LOG.warn("Unknown battery state {}", battery.getState());
}
}
batteryInfo.state = currentBatteryState;
getSupport().evaluateGBDeviceEvent(batteryInfo);
}
@ -446,6 +461,155 @@ public class XiaomiSystemService extends AbstractXiaomiService implements Xiaomi
getSupport().evaluateGBDeviceEvent(eventUpdatePreferences);
}
private void handleWearingState(int newStateValue) {
WearingState newState;
switch (newStateValue) {
case 1:
newState = WearingState.WEARING;
break;
case 2:
newState = WearingState.NOT_WEARING;
break;
default:
LOG.warn("Unknown wearing state {}", newStateValue);
return;
}
LOG.debug("Current wearing state = {}, new wearing state = {}", currentWearingState, newState);
if (currentWearingState != WearingState.UNKNOWN && currentWearingState != newState) {
GBDeviceEventWearState event = new GBDeviceEventWearState();
event.wearingState = newState;
getSupport().evaluateGBDeviceEvent(event);
}
currentWearingState = newState;
}
private void handleSleepDetectionState(int newStateValue) {
SleepState newState;
switch (newStateValue) {
case 1:
newState = SleepState.ASLEEP;
break;
case 2:
newState = SleepState.AWAKE;
break;
default:
LOG.warn("Unknown sleep detection state {}", newStateValue);
return;
}
LOG.debug("Current sleep detection state = {}, new sleep detection state = {}", currentSleepDetectionState, newState);
if (currentSleepDetectionState != SleepState.UNKNOWN && currentSleepDetectionState != newState) {
GBDeviceEventSleepStateDetection event = new GBDeviceEventSleepStateDetection();
event.sleepState = newState;
getSupport().evaluateGBDeviceEvent(event);
}
currentSleepDetectionState = newState;
}
public void handleBasicDeviceState(XiaomiProto.BasicDeviceState deviceState) {
LOG.debug("Got basic device state: {}", deviceState);
if (null == deviceState) {
LOG.warn("Got null for BasicDeviceState, requesting battery state and returning");
getSupport().sendCommand("request battery state", COMMAND_TYPE, CMD_BATTERY);
return;
}
// handle battery info from message
{
BatteryState newBatteryState = deviceState.getIsCharging() ? BatteryState.BATTERY_CHARGING : BatteryState.BATTERY_NORMAL;
LOG.debug("Previous charging state: {}, new charging state: {}", currentBatteryState, newBatteryState);
currentBatteryState = newBatteryState;
// if the device state did not have a battery level, request it from the device through other means.
// the battery state is now cached, so that it can be used when another response with battery level is received.
if (!deviceState.hasBatteryLevel()) {
getSupport().sendCommand("request battery state", COMMAND_TYPE, CMD_BATTERY);
} else {
GBDeviceEventBatteryInfo event = new GBDeviceEventBatteryInfo();
event.batteryIndex = 0;
event.state = newBatteryState;
event.level = deviceState.getBatteryLevel();
getSupport().evaluateGBDeviceEvent(event);
}
}
// handle sleep state from message
{
SleepState newSleepState = deviceState.getIsUserAsleep() ? SleepState.ASLEEP : SleepState.AWAKE;
LOG.debug("Previous sleep state: {}, new sleep state: {}", currentSleepDetectionState, newSleepState);
// send event if the previous state is known and the new state is different from cached
if (currentSleepDetectionState != SleepState.UNKNOWN && currentSleepDetectionState != newSleepState) {
GBDeviceEventSleepStateDetection event = new GBDeviceEventSleepStateDetection();
event.sleepState = newSleepState;
getSupport().evaluateGBDeviceEvent(event);
}
currentSleepDetectionState = newSleepState;
}
// handle wearing state from message
{
WearingState newWearingState = deviceState.getIsWorn() ? WearingState.WEARING : WearingState.NOT_WEARING;
LOG.debug("Previous wearing state: {}, new wearing state: {}", currentWearingState, newWearingState);
if (currentWearingState != WearingState.UNKNOWN && currentWearingState != newWearingState) {
GBDeviceEventWearState event = new GBDeviceEventWearState();
event.wearingState = newWearingState;
getSupport().evaluateGBDeviceEvent(event);
}
currentWearingState = newWearingState;
}
// TODO: handle activity state
}
public void handleDeviceState(XiaomiProto.DeviceState deviceState) {
LOG.debug("Got device state: {}", deviceState);
if (null == deviceState) {
LOG.warn("Got null for DeviceState, requesting battery state and returning");
getSupport().sendCommand("request battery state", COMMAND_TYPE, CMD_BATTERY);
return;
}
if (deviceState.hasWearingState()) {
handleWearingState(deviceState.getWearingState());
}
// The charger state of some devices can only be known when listening for device status
// updates. If available, this state will be cached here and updated in the GBDevice upon
// the next retrieval of the battery level
if (deviceState.hasChargingState()) {
BatteryState newBatteryState = convertBatteryStateFromRawValue(deviceState.getChargingState());
LOG.debug("Current battery state = {}, new battery state = {}", currentBatteryState, newBatteryState);
if (currentBatteryState != newBatteryState) {
currentBatteryState = newBatteryState;
}
}
if (deviceState.hasSleepState()) {
handleSleepDetectionState(deviceState.getSleepState());
}
// TODO process warning (unknown possible values) and activity information
// request battery state to request battery level and charger state on supported models
getSupport().sendCommand("request battery state", COMMAND_TYPE, CMD_BATTERY);
}
public void onFindPhone(final boolean start) {
LOG.debug("Find phone: {}", start);

View File

@ -129,6 +129,9 @@ message System {
// 2, 47
optional VibrationPatternAck vibrationPatternAck = 43;
// 2, 78
optional BasicDeviceState basicDeviceState = 48;
// 2, 79
optional DeviceState deviceState = 49;
}
@ -304,6 +307,14 @@ message DeviceActivityState {
optional uint32 currentActivityState = 2;
}
message BasicDeviceState {
required bool isCharging = 1; // true when connected to charger
optional uint32 batteryLevel = 2;
required bool isWorn = 3; // true when the device detects it's being worn
required bool isUserAsleep = 4; // true when the device detected its user is asleep
optional DeviceActivityState activityState = 5;
}
message DeviceState {
optional uint32 chargingState = 1; // 1 charging, 2 not charging
optional uint32 wearingState = 2; // 1 wearing, 2 not wearing