mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-10 17:11:56 +01:00
Compare commits
29 Commits
64c024a5ce
...
a54f953b6f
Author | SHA1 | Date | |
---|---|---|---|
|
a54f953b6f | ||
|
9a388ca386 | ||
|
c66051f580 | ||
|
1c2516186d | ||
|
5ebfe9b39f | ||
|
b231104a88 | ||
|
242f3c6dbc | ||
|
043f839a4d | ||
|
6a2186919c | ||
|
4b70f3fcdb | ||
|
d379b11535 | ||
|
ee14b5d821 | ||
|
65aa5faec5 | ||
|
f970b7482b | ||
|
287b720350 | ||
|
14eaba858c | ||
|
0b64408b33 | ||
|
2b1c5b5819 | ||
|
b2b176b65c | ||
|
3af5a412fe | ||
|
fa1f99642a | ||
|
c1f2f5bb4b | ||
|
7a79a1d0f5 | ||
|
cc5078332b | ||
|
31fc266f94 | ||
|
22e51f841b | ||
|
275b662188 | ||
|
89b6ae9f24 | ||
|
01ef422812 |
@ -1,5 +1,14 @@
|
||||
### Changelog
|
||||
|
||||
#### Next version (WIP)
|
||||
* Initial support for Huawei Watch Fit
|
||||
* Initial support for Xiaomi Redmi Watch 3
|
||||
* Fossil/Skagen Hybrids: Fix crash on multi-byte unicode characters in menu
|
||||
* Huawei: Add weather support
|
||||
* Test Device: Add fake features and data
|
||||
* Periodically (around every 2 days) synchronize time on connected devices
|
||||
* Set alarm as used and enabled if time has changed
|
||||
|
||||
#### 0.79.0
|
||||
* Initial support for Honor Magic Watch 2
|
||||
* Initial support for Mijia MHO-C303
|
||||
|
@ -67,7 +67,7 @@ def getGitHashShort = { ->
|
||||
standardOutput = stdout
|
||||
}
|
||||
return stdout.toString().trim()
|
||||
} catch (ignored){
|
||||
} catch (ignored) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@ -84,9 +84,10 @@ android {
|
||||
defaultConfig {
|
||||
applicationId "nodomain.freeyourgadget.gadgetbridge"
|
||||
|
||||
minSdkVersion 21
|
||||
//noinspection OldTargetApi
|
||||
targetSdkVersion 33
|
||||
compileSdk 33
|
||||
minSdkVersion 21
|
||||
|
||||
// Note: always bump BOTH versionCode and versionName!
|
||||
versionName "0.79.0"
|
||||
@ -94,13 +95,8 @@ android {
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
buildConfigField "String", "GIT_HASH_SHORT", "\"${getGitHashShort()}\""
|
||||
buildConfigField "boolean", "INTERNET_ACCESS", "false"
|
||||
resValue "string", "pebble_content_provider", "com.getpebble.android.provider"
|
||||
resValue "string", "app_name", "@string/application_name_generic"
|
||||
resValue "string", "title_activity_controlcenter", "@string/title_activity_controlcenter_generic"
|
||||
resValue "string", "about_activity_title", "@string/about_activity_title_generic"
|
||||
resValue "string", "about_description", "@string/about_description_generic"
|
||||
resValue "string", "gadgetbridge_running", "@string/gadgetbridge_running_generic"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
nightly {
|
||||
if (System.getProperty("nightly_store_file") != null) {
|
||||
@ -122,19 +118,13 @@ android {
|
||||
//applicationIdSuffix ""
|
||||
//versionNameSuffix ""
|
||||
}
|
||||
|
||||
banglejs {
|
||||
dimension "device_type"
|
||||
applicationId "com.espruino.gadgetbridge"
|
||||
applicationIdSuffix ".banglejs"
|
||||
versionNameSuffix "-banglejs"
|
||||
buildConfigField "boolean", "INTERNET_ACCESS", "true"
|
||||
// Disable pebble provider to allow Bangle.js Gadgetbridge to coexist with Gadgetbridge
|
||||
resValue "string", "pebble_content_provider", "com.getpebble.android.nopebble.bangle.provider"
|
||||
resValue "string", "app_name", "@string/application_name_banglejs_main"
|
||||
resValue "string", "title_activity_controlcenter", "@string/title_activity_controlcenter_banglejs_main"
|
||||
resValue "string", "about_activity_title", "@string/about_activity_title_banglejs_main"
|
||||
resValue "string", "about_description", "@string/about_description_banglejs_main"
|
||||
resValue "string", "gadgetbridge_running", "@string/gadgetbridge_running_banglejs_main"
|
||||
targetSdkVersion 33
|
||||
// Note: app/src/banglejs/AndroidManifest.xml contains some extra permissions
|
||||
}
|
||||
@ -150,62 +140,34 @@ android {
|
||||
release {
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
resValue "string", "pebble_content_provider", "com.getpebble.android.provider"
|
||||
}
|
||||
|
||||
nightly {
|
||||
applicationIdSuffix ".nightly"
|
||||
versionNameSuffix "-${getGitHashShort}"
|
||||
minifyEnabled true
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
resValue "string", "pebble_content_provider", "com.getpebble.android.provider"
|
||||
resValue "string", "app_name", "@string/application_name_main_nightly"
|
||||
resValue "string", "title_activity_controlcenter", "@string/title_activity_controlcenter_main_nightly"
|
||||
resValue "string", "about_activity_title", "@string/about_activity_title_main_nightly"
|
||||
resValue "string", "about_description", "@string/about_description_main_nightly"
|
||||
resValue "string", "gadgetbridge_running", "@string/gadgetbridge_running_main_nightly"
|
||||
|
||||
minifyEnabled true
|
||||
debuggable true
|
||||
|
||||
if (System.getProperty("nightly_store_file") != null) {
|
||||
signingConfig signingConfigs.nightly
|
||||
|
||||
//this was conflicting with regular debug type (it should not), so it is only available for CI builds:
|
||||
productFlavors.mainline.resValue "string", "pebble_content_provider", "com.getpebble.android.provider"
|
||||
productFlavors.mainline.resValue "string", "app_name", "@string/application_name_main_nightly"
|
||||
productFlavors.mainline.resValue "string", "title_activity_controlcenter", "@string/title_activity_controlcenter_main_nightly"
|
||||
productFlavors.mainline.resValue "string", "about_activity_title", "@string/about_activity_title_main_nightly"
|
||||
productFlavors.mainline.resValue "string", "about_description", "@string/about_description_main_nightly"
|
||||
productFlavors.mainline.resValue "string", "gadgetbridge_running", "@string/gadgetbridge_running_main_nightly"
|
||||
|
||||
//keep the pebble provider for people who want this. In case of need we can create Banglejs Nopebble
|
||||
productFlavors.banglejs.resValue "string", "pebble_content_provider", "com.getpebble.android.provider"
|
||||
productFlavors.banglejs.resValue "string", "app_name", "@string/application_name_banglejs_nightly"
|
||||
productFlavors.banglejs.resValue "string", "title_activity_controlcenter", "@string/title_activity_controlcenter_banglejs_nightly"
|
||||
productFlavors.banglejs.resValue "string", "about_activity_title", "@string/about_activity_title_banglejs_nightly"
|
||||
productFlavors.banglejs.resValue "string", "about_description", "@string/about_description_banglejs_nightly"
|
||||
productFlavors.banglejs.resValue "string", "gadgetbridge_running", "@string/gadgetbridge_running_banglejs_nightly"
|
||||
} else {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
}
|
||||
|
||||
nopebble {
|
||||
applicationIdSuffix ".nightly_nopebble"
|
||||
versionNameSuffix "-${getGitHashShort}"
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
minifyEnabled true
|
||||
debuggable true
|
||||
|
||||
if (System.getProperty("nightly_store_file") != null) {
|
||||
signingConfig signingConfigs.nightly
|
||||
} else {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
resValue "string", "pebble_content_provider", "com.getpebble.android.nopebble.provider"
|
||||
resValue "string", "app_name", "@string/application_name_main_nopebble"
|
||||
resValue "string", "title_activity_controlcenter", "@string/title_activity_controlcenter_main_nopebble"
|
||||
resValue "string", "about_activity_title", "@string/about_activity_title_main_nopebble"
|
||||
resValue "string", "about_description", "@string/about_description_main_nopebble"
|
||||
resValue "string", "gadgetbridge_running", "@string/gadgetbridge_running_main_nopebble"
|
||||
debuggable true
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
@ -221,7 +183,6 @@ android {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
lint {
|
||||
@ -367,8 +328,8 @@ task pmd(type: Pmd) {
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
main.java.srcDirs += "${protobuf.generatedFilesBaseDir}"
|
||||
main.java.srcDirs += "build/generated/source/buildConfig"
|
||||
java.srcDirs += "${protobuf.generatedFilesBaseDir}"
|
||||
java.srcDirs += "build/generated/source/buildConfig"
|
||||
}
|
||||
}
|
||||
|
||||
@ -387,10 +348,10 @@ tasks.withType(SpotBugsTask) {
|
||||
xml.enabled = false
|
||||
html.enabled = true
|
||||
xml {
|
||||
destination file ("$project.buildDir/reports/spotbugs/spotbugs-output.xml")
|
||||
destination file("$project.buildDir/reports/spotbugs/spotbugs-output.xml")
|
||||
}
|
||||
html {
|
||||
destination file ("$project.buildDir/reports/spotbugs/spotbugs-output.html")
|
||||
destination file("$project.buildDir/reports/spotbugs/spotbugs-output.html")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
9
app/src/banglejs/res/values/strings.xml
Normal file
9
app/src/banglejs/res/values/strings.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string translatable="false" name="pebble_content_provider">com.getpebble.android.nopebble.bangle.provider</string>
|
||||
<string translatable="false" name="app_name">@string/application_name_banglejs_main</string>
|
||||
<string translatable="false" name="title_activity_controlcenter">@string/title_activity_controlcenter_banglejs_main</string>
|
||||
<string translatable="false" name="about_activity_title">@string/about_activity_title_banglejs_main</string>
|
||||
<string translatable="false" name="about_description">@string/about_description_banglejs_main</string>
|
||||
<string translatable="false" name="gadgetbridge_running">@string/gadgetbridge_running_banglejs_main</string>
|
||||
</resources>
|
10
app/src/banglejsNightly/res/values/strings.xml
Normal file
10
app/src/banglejsNightly/res/values/strings.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- enable Pebble content provider for Bangle.js nightly, so can that it can be installed besides regular Bangle.js release -->
|
||||
<string translatable="false" name="pebble_content_provider">com.getpebble.android.provider</string>
|
||||
<string translatable="false" name="app_name">@string/application_name_banglejs_nightly</string>
|
||||
<string translatable="false" name="title_activity_controlcenter">@string/title_activity_controlcenter_banglejs_nightly</string>
|
||||
<string translatable="false" name="about_activity_title">@string/about_activity_title_banglejs_nightly</string>
|
||||
<string translatable="false" name="about_description">@string/about_description_banglejs_nightly</string>
|
||||
<string translatable="false" name="gadgetbridge_running">@string/gadgetbridge_running_banglejs_nightly</string>
|
||||
</resources>
|
@ -35,6 +35,9 @@
|
||||
<!-- Schedule exact alarms (eg. for DST changes) -->
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
|
||||
<!-- Take wake locks (e.g. for time sync) -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!-- Read loyalty cards from Catima -->
|
||||
<uses-permission android:name="me.hackerchick.catima.READ_CARDS"/>
|
||||
<uses-permission android:name="me.hackerchick.catima.debug.READ_CARDS"/>
|
||||
|
@ -246,7 +246,6 @@ public class GBApplication extends Application {
|
||||
loadAppsPebbleBlackList();
|
||||
|
||||
PeriodicExporter.enablePeriodicExport(context);
|
||||
TimeChangeReceiver.scheduleNextDstChange(context);
|
||||
|
||||
if (isRunningMarshmallowOrLater()) {
|
||||
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
@ -125,9 +125,11 @@ public class AlarmDetails extends AbstractGBActivity {
|
||||
timePicker.setCurrentHour(alarm.getHour());
|
||||
timePicker.setCurrentMinute(alarm.getMinute());
|
||||
|
||||
cbSmartWakeup.setChecked(alarm.getSmartWakeup());
|
||||
int smartAlarmVisibility = supportsSmartWakeup() ? View.VISIBLE : View.GONE;
|
||||
boolean smartAlarmForced = forcedSmartWakeup(alarm.getPosition());
|
||||
cbSmartWakeup.setChecked(alarm.getSmartWakeup() || smartAlarmForced);
|
||||
int smartAlarmVisibility = supportsSmartWakeup(alarm.getPosition()) ? View.VISIBLE : View.GONE;
|
||||
cbSmartWakeup.setVisibility(smartAlarmVisibility);
|
||||
cbSmartWakeup.setEnabled(!smartAlarmForced);
|
||||
|
||||
cbSnooze.setChecked(alarm.getSnooze());
|
||||
int snoozeVisibility = supportsSnoozing() ? View.VISIBLE : View.GONE;
|
||||
@ -153,10 +155,21 @@ public class AlarmDetails extends AbstractGBActivity {
|
||||
cbSunday.setChecked(alarm.getRepetition(Alarm.ALARM_SUN));
|
||||
}
|
||||
|
||||
private boolean supportsSmartWakeup() {
|
||||
private boolean supportsSmartWakeup(int position) {
|
||||
if (device != null) {
|
||||
DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||
return coordinator.supportsSmartWakeup(device);
|
||||
return coordinator.supportsSmartWakeup(device, position);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The alarm at this position *must* be a smart alarm
|
||||
*/
|
||||
private boolean forcedSmartWakeup(int position) {
|
||||
if (device != null) {
|
||||
DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||
return coordinator.forcedSmartWakeup(device, position);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -205,7 +218,12 @@ public class AlarmDetails extends AbstractGBActivity {
|
||||
}
|
||||
|
||||
private void updateAlarm() {
|
||||
alarm.setSmartWakeup(supportsSmartWakeup() && cbSmartWakeup.isChecked());
|
||||
// Set alarm as used and enabled if time has changed
|
||||
if (alarm.getUnused() && alarm.getHour() != timePicker.getCurrentHour() || alarm.getMinute() != timePicker.getCurrentMinute()) {
|
||||
alarm.setUnused(false);
|
||||
alarm.setEnabled(true);
|
||||
}
|
||||
alarm.setSmartWakeup(supportsSmartWakeup(alarm.getPosition()) && cbSmartWakeup.isChecked());
|
||||
alarm.setSnooze(supportsSnoozing() && cbSnooze.isChecked());
|
||||
int repetitionMask = AlarmUtils.createRepetitionMask(cbMonday.isChecked(), cbTuesday.isChecked(), cbWednesday.isChecked(), cbThursday.isChecked(), cbFriday.isChecked(), cbSaturday.isChecked(), cbSunday.isChecked());
|
||||
alarm.setRepetition(repetitionMask);
|
||||
|
@ -71,6 +71,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandPreferencesActi
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleSettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.ConfigActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimePreferenceActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.externalevents.TimeChangeReceiver;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
@ -173,6 +174,17 @@ public class SettingsActivity extends AbstractSettingsActivityV2 {
|
||||
});
|
||||
}
|
||||
|
||||
pref = findPreference("datetime_synconconnect");
|
||||
if (pref != null) {
|
||||
pref.setOnPreferenceChangeListener((preference, newVal) -> {
|
||||
if (Boolean.TRUE.equals(newVal)) {
|
||||
TimeChangeReceiver.scheduleNextDstChangeOrPeriodicSync(requireContext());
|
||||
GBApplication.deviceService().onSetTime();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
pref = findPreference("log_to_file");
|
||||
if (pref != null) {
|
||||
pref.setOnPreferenceChangeListener((preference, newVal) -> {
|
||||
|
@ -319,7 +319,12 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(final GBDevice device) {
|
||||
public boolean supportsSmartWakeup(GBDevice device, int alarmPosition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forcedSmartWakeup(GBDevice device, int alarmPosition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -332,10 +332,17 @@ public interface DeviceCoordinator {
|
||||
int getAlarmSlotCount(GBDevice device);
|
||||
|
||||
/**
|
||||
* Returns true if this device/coordinator supports alarms with smart wakeup
|
||||
* @return
|
||||
* Returns true if this device/coordinator supports an alarm with smart wakeup for the current position
|
||||
* @param alarmPosition Position of the alarm
|
||||
*/
|
||||
boolean supportsSmartWakeup(GBDevice device);
|
||||
boolean supportsSmartWakeup(GBDevice device, int alarmPosition);
|
||||
|
||||
/**
|
||||
* Returns true if the alarm at the specified position *must* be a smart alarm for this device/coordinator
|
||||
* @param alarmPosition Position of the alarm
|
||||
* @return True if it must be a smart alarm, false otherwise
|
||||
*/
|
||||
boolean forcedSmartWakeup(GBDevice device, int alarmPosition);
|
||||
|
||||
/**
|
||||
* Returns true if this device/coordinator supports alarm snoozing
|
||||
|
@ -151,11 +151,6 @@ public class UnknownDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -156,11 +156,6 @@ public class AsteroidOSDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -112,11 +112,6 @@ public class BangleJSCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
|
@ -118,11 +118,6 @@ public class BinarySensorCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -111,11 +111,6 @@ public class CasioGB6900DeviceCoordinator extends CasioDeviceCoordinator {
|
||||
return 5; // 4 regular and one snooze
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -142,11 +142,6 @@ public class CasioGBX100DeviceCoordinator extends CasioDeviceCoordinator {
|
||||
return 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -115,11 +115,6 @@ public class CasioGWB5600DeviceCoordinator extends CasioDeviceCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAppsManagement(final GBDevice device) {
|
||||
return false;
|
||||
|
@ -80,11 +80,6 @@ public class DomyosT540Coordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -105,11 +105,6 @@ public class FitProDeviceCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 8;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
|
@ -90,11 +90,6 @@ public class FlipperZeroCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Flipper devices";
|
||||
|
@ -80,11 +80,6 @@ public abstract class GalaxyBudsGenericCoordinator extends AbstractBLClassicDevi
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -136,11 +136,6 @@ public class HPlusCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 3; // FIXME - check the real value
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
|
@ -53,7 +53,7 @@ public class SG2Coordinator extends HPlusCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
public boolean supportsSmartWakeup(GBDevice device, int position) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -525,11 +525,6 @@ public abstract class HuamiCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
return true;
|
||||
|
@ -307,7 +307,7 @@ public abstract class ZeppOsCoordinator extends HuamiCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(final GBDevice device) {
|
||||
public boolean supportsSmartWakeup(final GBDevice device, int position) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -112,8 +112,13 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return huaweiCoordinator.supportsSmartAlarm(device);
|
||||
public boolean supportsSmartWakeup(GBDevice device, int position) {
|
||||
return huaweiCoordinator.supportsSmartAlarm(device, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forcedSmartWakeup(GBDevice device, int alarmPosition) {
|
||||
return huaweiCoordinator.forcedSmartWakeup(device, alarmPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -139,7 +144,7 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather() {
|
||||
return false;
|
||||
return huaweiCoordinator.supportsWeather();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -36,6 +36,7 @@ public final class HuaweiConstants {
|
||||
|
||||
public static final int TAG_RESULT = 127;
|
||||
public static final byte[] RESULT_SUCCESS = new byte[]{0x00, 0x01, (byte)0x86, (byte)0xA0};
|
||||
public static final int RESULT_SUCCESS_INT = 0x186a0;
|
||||
|
||||
public static class CryptoTags {
|
||||
public static final int encryption = 124;
|
||||
|
@ -308,10 +308,19 @@ public class HuaweiCoordinator {
|
||||
public boolean supportsSmartAlarm() {
|
||||
return supportsCommandForService(0x08, 0x02) ;
|
||||
}
|
||||
|
||||
public boolean supportsSmartAlarm(GBDevice gbDevice) {
|
||||
return supportsSmartAlarm() || getForceOption(gbDevice, PREF_FORCE_ENABLE_SMART_ALARM);
|
||||
}
|
||||
|
||||
public boolean supportsSmartAlarm(GBDevice gbDevice, int alarmPosition) {
|
||||
return supportsSmartAlarm(gbDevice) && alarmPosition == 0;
|
||||
}
|
||||
|
||||
public boolean forcedSmartWakeup(GBDevice device, int alarmPosition) {
|
||||
return supportsSmartAlarm(device, alarmPosition) && alarmPosition == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True if alarms can be changed on the device, false otherwise
|
||||
*/
|
||||
@ -327,6 +336,30 @@ public class HuaweiCoordinator {
|
||||
return supportsCommandForService(0x0c, 0x01);
|
||||
}
|
||||
|
||||
public boolean supportsWeather() {
|
||||
return supportsCommandForService(0x0f, 0x01);
|
||||
}
|
||||
|
||||
public boolean supportsWeatherUnit() {
|
||||
return supportsCommandForService(0x0f, 0x05);
|
||||
}
|
||||
|
||||
public boolean supportsWeatherExtended() {
|
||||
return supportsCommandForService(0x0f, 0x06);
|
||||
}
|
||||
|
||||
public boolean supportsWeatherForecasts() {
|
||||
return supportsCommandForService(0x0f, 0x08);
|
||||
}
|
||||
|
||||
public boolean supportsWeatherMoonRiseSet() {
|
||||
return supportsCommandForService(0x0f, 0x0a);
|
||||
}
|
||||
|
||||
public boolean supportsWeatherTides() {
|
||||
return supportsCommandForService(0x0f, 0x0b);
|
||||
}
|
||||
|
||||
public boolean supportsWorkouts() {
|
||||
return supportsCommandForService(0x17, 0x01);
|
||||
}
|
||||
|
@ -112,8 +112,13 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return huaweiCoordinator.supportsSmartAlarm(device);
|
||||
public boolean supportsSmartWakeup(GBDevice device, int position) {
|
||||
return huaweiCoordinator.supportsSmartAlarm(device, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forcedSmartWakeup(GBDevice device, int alarmPosition) {
|
||||
return huaweiCoordinator.forcedSmartWakeup(device, alarmPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -139,7 +144,7 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather() {
|
||||
return false;
|
||||
return huaweiCoordinator.supportsWeather();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -32,6 +32,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Alarms;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.AccountRelated;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Calls;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FindPhone;
|
||||
@ -475,6 +476,20 @@ public class HuaweiPacket {
|
||||
return new FindPhone.Response(paramsProvider).fromPacket(this);
|
||||
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
|
||||
return this;
|
||||
case Weather.id:
|
||||
switch (this.commandId) {
|
||||
case Weather.WeatherSupport.id:
|
||||
return new Weather.WeatherSupport.Response(paramsProvider).fromPacket(this);
|
||||
case Weather.WeatherExtendedSupport.id:
|
||||
return new Weather.WeatherExtendedSupport.Response(paramsProvider).fromPacket(this);
|
||||
case Weather.WeatherStart.id:
|
||||
return new Weather.WeatherStart.Response(paramsProvider).fromPacket(this);
|
||||
case Weather.WeatherSunMoonSupport.id:
|
||||
return new Weather.WeatherSunMoonSupport.Response(paramsProvider).fromPacket(this);
|
||||
default:
|
||||
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
|
||||
return this;
|
||||
}
|
||||
case Workout.id:
|
||||
switch (this.commandId) {
|
||||
case Workout.WorkoutCount.id:
|
||||
@ -538,6 +553,13 @@ public class HuaweiPacket {
|
||||
default:
|
||||
return this;
|
||||
}
|
||||
case Weather.id:
|
||||
switch (this.commandId) {
|
||||
case Weather.WeatherForecastData.id:
|
||||
return new Weather.WeatherForecastData.OutgoingRequest(paramsProvider).fromPacket(this);
|
||||
default:
|
||||
return this;
|
||||
}
|
||||
default:
|
||||
return this;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2024 Damien Gaignon, Martin.JM
|
||||
/* Copyright (C) 2024 Damien Gaignon, Martin.JM, Vitalii Tomin
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@ -24,6 +24,7 @@ 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;
|
||||
@ -181,6 +182,10 @@ public class HuaweiTLV {
|
||||
return put(tag, ByteBuffer.allocate(8).putLong(value).array());
|
||||
}
|
||||
|
||||
public HuaweiTLV put(int tag, Double value) {
|
||||
return put(tag, ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putDouble(value).array());
|
||||
}
|
||||
|
||||
public HuaweiTLV put(int tag, int value) {
|
||||
return put(tag, ByteBuffer.allocate(4).putInt(value).array());
|
||||
}
|
||||
|
@ -0,0 +1,46 @@
|
||||
/* Copyright (C) 2024 Vitalii Tomin, Martin.JM
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
|
||||
public class GpsAndTime {
|
||||
public static final byte id = 0x18;
|
||||
|
||||
public static class CurrentGPSRequest extends HuaweiPacket {
|
||||
public static final byte id = 0x07;
|
||||
public CurrentGPSRequest (
|
||||
ParamsProvider paramsProvider,
|
||||
int timestamp,
|
||||
double lat,
|
||||
double lon
|
||||
) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = GpsAndTime.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, timestamp)
|
||||
.put(0x02, lon)
|
||||
.put(0x03, lat);
|
||||
this.isEncrypted = true;
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,763 @@
|
||||
/* Copyright (C) 2024 Martin.JM
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
|
||||
public class Weather {
|
||||
public static final byte id = 0x0f;
|
||||
|
||||
public static class Settings {
|
||||
// WeatherSupport
|
||||
public boolean weatherSupported = false;
|
||||
public boolean windSupported = false;
|
||||
public boolean pm25Supported = false;
|
||||
public boolean temperatureSupported = false;
|
||||
public boolean locationNameSupported = false;
|
||||
public boolean currentTemperatureSupported = false;
|
||||
public boolean unitSupported = false;
|
||||
public boolean airQualityIndexSupported = false;
|
||||
|
||||
// WeatherExtendedSupport
|
||||
public boolean timeSupported = false;
|
||||
public boolean sourceSupported = false;
|
||||
public boolean weatherIconSupported = false;
|
||||
|
||||
// WeatherSunMoonSupport
|
||||
public boolean sunRiseSetSupported = false;
|
||||
public boolean moonPhaseSupported = false;
|
||||
}
|
||||
|
||||
public enum WeatherIcon {
|
||||
// Also used for the text on the watch
|
||||
SUNNY,
|
||||
CLOUDY,
|
||||
OVERCAST,
|
||||
SHOWERS,
|
||||
THUNDERSTORMS,
|
||||
THUNDER_AND_HAIL,
|
||||
SLEET,
|
||||
LIGHT_RAIN,
|
||||
RAIN,
|
||||
HEAVY_RAIN,
|
||||
RAIN_STORM,
|
||||
HEAVY_RAIN_STORMS,
|
||||
SEVERE_RAIN_STORMS,
|
||||
SNOW_FLURRIES,
|
||||
LIGHT_SNOW,
|
||||
SNOW,
|
||||
HEAVY_SNOW,
|
||||
SNOWSTORMS,
|
||||
FOG,
|
||||
FREEZING_RAIN,
|
||||
DUST_STORM,
|
||||
LIGHT_TO_MODERATE_RAIN,
|
||||
MODERATE_TO_HEAVY_RAIN,
|
||||
HEAVY_TO_SEVERE_RAIN,
|
||||
HEAVY_TO_TORRENTIAL_RAIN,
|
||||
SEVERE_TO_TORRENTIAL_RAIN,
|
||||
LIGHT_TO_MODERATE_SNOW,
|
||||
MODERATE_TO_HEAVY_SNOW,
|
||||
HEAVY_SNOW_TO_BLIZZARD,
|
||||
DUST,
|
||||
SAND,
|
||||
SANDSTORMS,
|
||||
FREEZING, // misses small/non-moving icon
|
||||
HOT, // misses small/non-moving icon
|
||||
COLD, // misses small/non-moving icon
|
||||
WINDY,
|
||||
HAZY,
|
||||
UNKNOWN // Good to have probably
|
||||
}
|
||||
|
||||
private static byte iconToByte(WeatherIcon weatherIcon) {
|
||||
switch (weatherIcon) {
|
||||
case SUNNY:
|
||||
return 0x00;
|
||||
case CLOUDY:
|
||||
return 0x01;
|
||||
case OVERCAST:
|
||||
return 0x02;
|
||||
case SHOWERS:
|
||||
return 0x03;
|
||||
case THUNDERSTORMS:
|
||||
return 0x04;
|
||||
case THUNDER_AND_HAIL:
|
||||
return 0x05;
|
||||
case SLEET:
|
||||
return 0x06;
|
||||
case LIGHT_RAIN:
|
||||
return 0x07;
|
||||
case RAIN:
|
||||
return 0x08;
|
||||
case HEAVY_RAIN:
|
||||
return 0x09;
|
||||
case RAIN_STORM:
|
||||
return 0x0a;
|
||||
case HEAVY_RAIN_STORMS:
|
||||
return 0x0b;
|
||||
case SEVERE_RAIN_STORMS:
|
||||
return 0x0c;
|
||||
case SNOW_FLURRIES:
|
||||
return 0x0d;
|
||||
case LIGHT_SNOW:
|
||||
return 0x0e;
|
||||
case SNOW:
|
||||
return 0x0f;
|
||||
case HEAVY_SNOW:
|
||||
return 0x10;
|
||||
case SNOWSTORMS:
|
||||
return 0x11;
|
||||
case FOG:
|
||||
return 0x12;
|
||||
case FREEZING_RAIN:
|
||||
return 0x13;
|
||||
case DUST_STORM:
|
||||
return 0x14;
|
||||
case LIGHT_TO_MODERATE_RAIN:
|
||||
return 0x15;
|
||||
case MODERATE_TO_HEAVY_RAIN:
|
||||
return 0x16;
|
||||
case HEAVY_TO_SEVERE_RAIN:
|
||||
return 0x17;
|
||||
case HEAVY_TO_TORRENTIAL_RAIN:
|
||||
return 0x18;
|
||||
case SEVERE_TO_TORRENTIAL_RAIN:
|
||||
return 0x19;
|
||||
case LIGHT_TO_MODERATE_SNOW:
|
||||
return 0x1a;
|
||||
case MODERATE_TO_HEAVY_SNOW:
|
||||
return 0x1b;
|
||||
case HEAVY_SNOW_TO_BLIZZARD:
|
||||
return 0x1c;
|
||||
case DUST:
|
||||
return 0x1d;
|
||||
case SAND:
|
||||
return 0x1e;
|
||||
case SANDSTORMS:
|
||||
return 0x1f;
|
||||
case FREEZING:
|
||||
return 0x20;
|
||||
case HOT:
|
||||
return 0x21;
|
||||
case COLD:
|
||||
return 0x22;
|
||||
case WINDY:
|
||||
return 0x23;
|
||||
case HAZY:
|
||||
return 0x35;
|
||||
default:
|
||||
return 0x63; // Any higher and the current weather breaks
|
||||
}
|
||||
}
|
||||
|
||||
private static WeatherIcon byteToIcon(byte weatherIcon) {
|
||||
switch (weatherIcon) {
|
||||
case 0x00:
|
||||
return WeatherIcon.SUNNY;
|
||||
case 0x01:
|
||||
return WeatherIcon.CLOUDY;
|
||||
case 0x02:
|
||||
return WeatherIcon.OVERCAST;
|
||||
case 0x03:
|
||||
return WeatherIcon.SHOWERS;
|
||||
case 0x04:
|
||||
return WeatherIcon.THUNDERSTORMS;
|
||||
case 0x05:
|
||||
return WeatherIcon.THUNDER_AND_HAIL;
|
||||
case 0x06:
|
||||
return WeatherIcon.SLEET;
|
||||
case 0x07:
|
||||
return WeatherIcon.LIGHT_RAIN;
|
||||
case 0x08:
|
||||
return WeatherIcon.RAIN;
|
||||
case 0x09:
|
||||
return WeatherIcon.HEAVY_RAIN;
|
||||
case 0x0a:
|
||||
return WeatherIcon.RAIN_STORM;
|
||||
case 0x0b:
|
||||
return WeatherIcon.HEAVY_RAIN_STORMS;
|
||||
case 0x0c:
|
||||
return WeatherIcon.SEVERE_RAIN_STORMS;
|
||||
case 0x0d:
|
||||
return WeatherIcon.SNOW_FLURRIES;
|
||||
case 0x0e:
|
||||
return WeatherIcon.LIGHT_SNOW;
|
||||
case 0x0f:
|
||||
return WeatherIcon.SNOW;
|
||||
case 0x10:
|
||||
return WeatherIcon.HEAVY_SNOW;
|
||||
case 0x11:
|
||||
return WeatherIcon.SNOWSTORMS;
|
||||
case 0x12:
|
||||
return WeatherIcon.FOG;
|
||||
case 0x13:
|
||||
return WeatherIcon.FREEZING_RAIN;
|
||||
case 0x14:
|
||||
return WeatherIcon.DUST_STORM;
|
||||
case 0x15:
|
||||
return WeatherIcon.LIGHT_TO_MODERATE_RAIN;
|
||||
case 0x16:
|
||||
return WeatherIcon.MODERATE_TO_HEAVY_RAIN;
|
||||
case 0x17:
|
||||
return WeatherIcon.HEAVY_TO_SEVERE_RAIN;
|
||||
case 0x18:
|
||||
return WeatherIcon.HEAVY_TO_TORRENTIAL_RAIN;
|
||||
case 0x19:
|
||||
return WeatherIcon.SEVERE_TO_TORRENTIAL_RAIN;
|
||||
case 0x1a:
|
||||
return WeatherIcon.LIGHT_TO_MODERATE_SNOW;
|
||||
case 0x1b:
|
||||
return WeatherIcon.MODERATE_TO_HEAVY_SNOW;
|
||||
case 0x1c:
|
||||
return WeatherIcon.HEAVY_SNOW_TO_BLIZZARD;
|
||||
case 0x1d:
|
||||
return WeatherIcon.DUST;
|
||||
case 0x1e:
|
||||
return WeatherIcon.SAND;
|
||||
case 0x1f:
|
||||
return WeatherIcon.SANDSTORMS;
|
||||
case 0x20:
|
||||
return WeatherIcon.FREEZING;
|
||||
case 0x21:
|
||||
return WeatherIcon.HOT;
|
||||
case 0x22:
|
||||
return WeatherIcon.COLD;
|
||||
case 0x23:
|
||||
return WeatherIcon.WINDY;
|
||||
case 0x35:
|
||||
return WeatherIcon.HAZY;
|
||||
default:
|
||||
return WeatherIcon.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
public enum HuaweiTemperatureFormat {
|
||||
CELSIUS,
|
||||
FAHRENHEIT
|
||||
}
|
||||
|
||||
private static byte temperatureFormatToByte(HuaweiTemperatureFormat temperatureFormat) {
|
||||
if (temperatureFormat == HuaweiTemperatureFormat.FAHRENHEIT)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public enum MoonPhase {
|
||||
NEW_MOON,
|
||||
WAXING_CRESCENT,
|
||||
FIRST_QUARTER,
|
||||
WAXING_GIBBOUS,
|
||||
FULL_MOON,
|
||||
WANING_GIBBOUS,
|
||||
THIRD_QUARTER,
|
||||
WANING_CRESCENT
|
||||
}
|
||||
|
||||
public static MoonPhase degreesToMoonPhase(int degrees) {
|
||||
final int leeway = 6; // Give some leeway for the new moon, first quarter, full moon, and third quarter
|
||||
if (degrees < 0 || degrees > 360)
|
||||
return null;
|
||||
else if (degrees >= 360 - leeway || degrees <= leeway)
|
||||
return MoonPhase.NEW_MOON;
|
||||
else if (degrees < 90)
|
||||
return MoonPhase.WAXING_CRESCENT;
|
||||
else if (degrees <= 90 + leeway)
|
||||
return MoonPhase.FIRST_QUARTER;
|
||||
else if (degrees < 180 - leeway)
|
||||
return MoonPhase.WAXING_GIBBOUS;
|
||||
else if (degrees <= 180 + leeway)
|
||||
return MoonPhase.FULL_MOON;
|
||||
else if (degrees < 270 - leeway)
|
||||
return MoonPhase.WANING_GIBBOUS;
|
||||
else if (degrees <= 270 + leeway)
|
||||
return MoonPhase.THIRD_QUARTER;
|
||||
else
|
||||
return MoonPhase.WANING_CRESCENT;
|
||||
}
|
||||
|
||||
private static byte moonPhaseToByte (MoonPhase moonPhase) {
|
||||
switch (moonPhase) {
|
||||
case NEW_MOON:
|
||||
return 1;
|
||||
case WAXING_CRESCENT:
|
||||
return 2;
|
||||
case FIRST_QUARTER:
|
||||
return 3;
|
||||
case WAXING_GIBBOUS:
|
||||
return 4;
|
||||
case FULL_MOON:
|
||||
return 5;
|
||||
case WANING_GIBBOUS:
|
||||
return 6;
|
||||
case THIRD_QUARTER:
|
||||
return 7;
|
||||
case WANING_CRESCENT:
|
||||
return 8;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static MoonPhase byteToMoonPhase(byte moonPhase) {
|
||||
switch (moonPhase) {
|
||||
case 1:
|
||||
return MoonPhase.NEW_MOON;
|
||||
case 2:
|
||||
return MoonPhase.WAXING_CRESCENT;
|
||||
case 3:
|
||||
return MoonPhase.FIRST_QUARTER;
|
||||
case 4:
|
||||
return MoonPhase.WAXING_GIBBOUS;
|
||||
case 5:
|
||||
return MoonPhase.FULL_MOON;
|
||||
case 6:
|
||||
return MoonPhase.WANING_GIBBOUS;
|
||||
case 7:
|
||||
return MoonPhase.THIRD_QUARTER;
|
||||
case 8:
|
||||
return MoonPhase.WANING_CRESCENT;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CurrentWeatherRequest extends HuaweiPacket {
|
||||
public static final byte id = 0x01;
|
||||
|
||||
public CurrentWeatherRequest(
|
||||
ParamsProvider paramsProvider,
|
||||
Settings settings,
|
||||
WeatherIcon icon,
|
||||
Byte windDirection,
|
||||
Byte windSpeed,
|
||||
Byte lowestTemperature,
|
||||
Byte highestTemperature,
|
||||
Short pm25, // TODO: might be float?
|
||||
String locationName,
|
||||
Byte currentTemperature,
|
||||
HuaweiTemperatureFormat temperatureUnit,
|
||||
Short airQualityIndex,
|
||||
Integer observationTime,
|
||||
String sourceName
|
||||
) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV();
|
||||
|
||||
HuaweiTLV tlv81 = new HuaweiTLV();
|
||||
|
||||
if (icon != null && settings.weatherIconSupported) {
|
||||
tlv81.put(0x02, iconToByte(icon));
|
||||
}
|
||||
|
||||
if (settings.windSupported) {
|
||||
short wind = 0;
|
||||
if (windSpeed != null)
|
||||
wind = (short) windSpeed;
|
||||
|
||||
if (windDirection != null) {
|
||||
if (windDirection > 0)
|
||||
wind |= ((short) (windDirection * 8 / 360)) << 8;
|
||||
else
|
||||
wind |= ((short) (360 + windDirection) * 8 / 360) << 8;
|
||||
}
|
||||
tlv81.put(0x03, wind);
|
||||
}
|
||||
|
||||
if (settings.weatherIconSupported || settings.windSupported)
|
||||
this.tlv.put(0x81, tlv81);
|
||||
|
||||
if (lowestTemperature != null && highestTemperature != null && settings.temperatureSupported) {
|
||||
this.tlv.put(0x85, new HuaweiTLV()
|
||||
.put(0x06, lowestTemperature)
|
||||
.put(0x07, highestTemperature)
|
||||
);
|
||||
}
|
||||
if (windDirection != null && windSpeed != null && settings.windSupported)
|
||||
this.tlv.put(0x03, (short) ((((short) windDirection) << 8) | ((short) windSpeed)));
|
||||
if (pm25 != null && settings.pm25Supported)
|
||||
this.tlv.put(0x04, pm25);
|
||||
if (locationName != null && settings.locationNameSupported)
|
||||
this.tlv.put(0x08, locationName);
|
||||
if (currentTemperature != null && settings.currentTemperatureSupported)
|
||||
this.tlv.put(0x09, currentTemperature);
|
||||
if (temperatureUnit != null && settings.unitSupported)
|
||||
this.tlv.put(0x0a, temperatureFormatToByte(temperatureUnit));
|
||||
if (airQualityIndex != null && settings.airQualityIndexSupported)
|
||||
this.tlv.put(0x0b, airQualityIndex);
|
||||
if (observationTime != null && settings.timeSupported)
|
||||
this.tlv.put(0x0c, observationTime);
|
||||
if (sourceName != null && settings.sourceSupported)
|
||||
this.tlv.put(0x0e, sourceName);
|
||||
this.tlv.put(0x0f, (byte) 0);
|
||||
|
||||
this.isEncrypted = true;
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class WeatherSupport {
|
||||
public static final byte id = 0x02;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV().put(0x01);
|
||||
this.isEncrypted = true;
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public byte supportedBitmap = 0;
|
||||
|
||||
public boolean weatherSupported = false;
|
||||
public boolean windSupported = false;
|
||||
public boolean pm25Supported = false;
|
||||
public boolean temperatureSupported = false;
|
||||
public boolean locationNameSupported = false;
|
||||
public boolean currentTemperatureSupported = false;
|
||||
public boolean unitSupported = false;
|
||||
public boolean airQualityIndexSupported = false;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
if (!this.tlv.contains(0x01))
|
||||
throw new MissingTagException(0x01);
|
||||
this.supportedBitmap = this.tlv.getByte(0x01);
|
||||
|
||||
this.weatherSupported = (this.supportedBitmap & 0x01) != 0;
|
||||
this.windSupported = (this.supportedBitmap & 0x02) != 0;
|
||||
this.pm25Supported = (this.supportedBitmap & 0x04) != 0;
|
||||
this.temperatureSupported = (this.supportedBitmap & 0x08) != 0;
|
||||
this.locationNameSupported = (this.supportedBitmap & 0x10) != 0;
|
||||
this.currentTemperatureSupported = (this.supportedBitmap & 0x20) != 0;
|
||||
this.unitSupported = (this.supportedBitmap & 0x40) != 0;
|
||||
this.airQualityIndexSupported = (this.supportedBitmap & 0x80) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class WeatherDeviceRequest extends HuaweiPacket {
|
||||
public static final byte id = 0x04;
|
||||
|
||||
public WeatherDeviceRequest(ParamsProvider paramsProvider, int responseValue) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV().put(0x01, responseValue);
|
||||
this.isEncrypted = false;
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class WeatherUnitRequest extends HuaweiPacket {
|
||||
public static final byte id = 0x05;
|
||||
|
||||
public WeatherUnitRequest(ParamsProvider paramsProvider, HuaweiTemperatureFormat temperatureFormat) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV().put(0x01, temperatureFormatToByte(temperatureFormat));
|
||||
this.isEncrypted = true;
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class WeatherExtendedSupport {
|
||||
public static final byte id = 0x06;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV().put(0x01);
|
||||
this.isEncrypted = true;
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public short supportedBitmap = 0;
|
||||
|
||||
public boolean timeSupported = false;
|
||||
public boolean sourceSupported = false;
|
||||
public boolean weatherIconSupported = false;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
if (!this.tlv.contains(0x01))
|
||||
throw new MissingTagException(0x01);
|
||||
this.supportedBitmap = this.tlv.getShort(0x01);
|
||||
|
||||
this.timeSupported = (this.supportedBitmap & 0x01) != 0;
|
||||
this.sourceSupported = (this.supportedBitmap & 0x02) != 0;
|
||||
this.weatherIconSupported = (this.supportedBitmap & 0x04) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class WeatherForecastData {
|
||||
public static final byte id = 0x08;
|
||||
|
||||
public static class TimeData {
|
||||
public int timestamp;
|
||||
public WeatherIcon icon;
|
||||
public Byte temperature;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String timestampStr = new Date(timestamp * 1000L).toString();
|
||||
return "TimeData{" +
|
||||
"timestamp=" + timestamp +
|
||||
", timestamp=" + timestampStr +
|
||||
", icon=" + icon +
|
||||
", temperature=" + temperature +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public static class DayData {
|
||||
public int timestamp;
|
||||
public WeatherIcon icon;
|
||||
public Byte highTemperature;
|
||||
public Byte lowTemperature;
|
||||
public Integer sunriseTime;
|
||||
public Integer sunsetTime;
|
||||
public Integer moonRiseTime;
|
||||
public Integer moonSetTime;
|
||||
public MoonPhase moonPhase;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String timestampStr = new Date(timestamp * 1000L).toString();
|
||||
return "DayData{" +
|
||||
"timestamp=" + timestamp +
|
||||
", timestamp=" + timestampStr +
|
||||
", icon=" + icon +
|
||||
", highTemperature=" + highTemperature +
|
||||
", lowTemperature=" + lowTemperature +
|
||||
", sunriseTime=" + sunriseTime +
|
||||
", sunsetTime=" + sunsetTime +
|
||||
", moonRiseTime=" + moonRiseTime +
|
||||
", moonSetTime=" + moonSetTime +
|
||||
", moonPhase=" + moonPhase +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(
|
||||
ParamsProvider paramsProvider,
|
||||
Settings settings,
|
||||
List<TimeData> timeDataList,
|
||||
List<DayData> dayDataList
|
||||
) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV();
|
||||
|
||||
if (timeDataList != null && !timeDataList.isEmpty()) {
|
||||
HuaweiTLV timeDataTlv = new HuaweiTLV();
|
||||
for (TimeData timeData : timeDataList) {
|
||||
HuaweiTLV timeTlv = new HuaweiTLV();
|
||||
timeTlv.put(0x03, timeData.timestamp);
|
||||
if (timeData.icon != null && settings.weatherIconSupported)
|
||||
timeTlv.put(0x04, iconToByte(timeData.icon));
|
||||
if (timeData.temperature != null && (settings.temperatureSupported || settings.currentTemperatureSupported))
|
||||
timeTlv.put(0x05, timeData.temperature);
|
||||
timeDataTlv.put(0x82, timeTlv);
|
||||
}
|
||||
this.tlv.put(0x81, timeDataTlv);
|
||||
}
|
||||
|
||||
if (dayDataList != null && !dayDataList.isEmpty()) {
|
||||
HuaweiTLV dayDataTlv = new HuaweiTLV();
|
||||
for (DayData dayData : dayDataList) {
|
||||
HuaweiTLV dayTlv = new HuaweiTLV();
|
||||
dayTlv.put(0x12, dayData.timestamp);
|
||||
if (dayData.icon != null && settings.weatherIconSupported)
|
||||
dayTlv.put(0x13, iconToByte(dayData.icon));
|
||||
if (settings.temperatureSupported) {
|
||||
if (dayData.highTemperature != null)
|
||||
dayTlv.put(0x14, dayData.highTemperature);
|
||||
if (dayData.lowTemperature != null)
|
||||
dayTlv.put(0x15, dayData.lowTemperature);
|
||||
}
|
||||
if (settings.sunRiseSetSupported) {
|
||||
if (dayData.sunriseTime != null)
|
||||
dayTlv.put(0x16, dayData.sunriseTime);
|
||||
if (dayData.sunsetTime != null)
|
||||
dayTlv.put(0x17, dayData.sunsetTime);
|
||||
if (dayData.moonRiseTime != null)
|
||||
dayTlv.put(0x1a, dayData.moonRiseTime);
|
||||
if (dayData.moonSetTime != null)
|
||||
dayTlv.put(0x1b, dayData.moonSetTime);
|
||||
}
|
||||
if (dayData.moonPhase != null && settings.moonPhaseSupported)
|
||||
dayTlv.put(0x1e, moonPhaseToByte(dayData.moonPhase));
|
||||
dayDataTlv.put(0x91, dayTlv);
|
||||
}
|
||||
this.tlv.put(0x90, dayDataTlv);
|
||||
}
|
||||
|
||||
this.isEncrypted = true;
|
||||
this.isSliced = true;
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class OutgoingRequest extends HuaweiPacket {
|
||||
List<TimeData> timeDataList;
|
||||
List<DayData> dayDataList;
|
||||
|
||||
public OutgoingRequest(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
this.complete = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
timeDataList = new ArrayList<>(this.tlv.getObject(0x81).getObjects(0x82).size());
|
||||
for (HuaweiTLV timeTlv : this.tlv.getObject(0x81).getObjects(0x82)) {
|
||||
TimeData timeData = new TimeData();
|
||||
timeData.timestamp = timeTlv.getInteger(0x03);
|
||||
timeData.icon = byteToIcon(timeTlv.getByte(0x04));
|
||||
timeData.temperature = timeTlv.getByte(0x05);
|
||||
timeDataList.add(timeData);
|
||||
}
|
||||
|
||||
dayDataList = new ArrayList<>(this.tlv.getObject(0x90).getObjects(0x91).size());
|
||||
for (HuaweiTLV dayTlv : this.tlv.getObject(0x90).getObjects(0x91)) {
|
||||
DayData dayData = new DayData();
|
||||
dayData.timestamp = dayTlv.getInteger(0x12);
|
||||
dayData.icon = byteToIcon(dayTlv.getByte(0x13));
|
||||
dayData.highTemperature = dayTlv.getByte(0x14);
|
||||
dayData.lowTemperature = dayTlv.getByte(0x15);
|
||||
dayData.sunriseTime = dayTlv.getInteger(0x16);
|
||||
dayData.sunsetTime = dayTlv.getInteger(0x17);
|
||||
dayData.moonRiseTime = dayTlv.getInteger(0x1a);
|
||||
dayData.moonSetTime = dayTlv.getInteger(0x1b);
|
||||
dayData.moonPhase = byteToMoonPhase(dayTlv.getByte(0x1e));
|
||||
dayDataList.add(dayData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class WeatherStart {
|
||||
public static final byte id = 0x09;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
|
||||
public Request(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV().put(0x01, (byte) 0x03); // TODO: find out what this means
|
||||
this.isEncrypted = true;
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public int successCode = -1;
|
||||
public boolean success = false;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
this.successCode = this.tlv.getInteger(0x7f);
|
||||
this.success = this.successCode == 0x000186A0 || this.successCode == 0x000186A3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class WeatherSunMoonSupport {
|
||||
public static final byte id = 0x0a;
|
||||
|
||||
public static class Request extends HuaweiPacket {
|
||||
public Request(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = id;
|
||||
this.tlv = new HuaweiTLV().put(0x01);
|
||||
this.isEncrypted = true;
|
||||
this.complete = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Response extends HuaweiPacket {
|
||||
public int supportedBitmap = 0;
|
||||
|
||||
public boolean sunRiseSetSupported = false;
|
||||
public boolean moonPhaseSupported = false;
|
||||
|
||||
public Response(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseTlv() throws ParseException {
|
||||
if (!this.tlv.contains(0x01))
|
||||
throw new MissingTagException(0x01);
|
||||
this.supportedBitmap = this.tlv.getInteger(0x01);
|
||||
|
||||
this.sunRiseSetSupported = (this.supportedBitmap & 0x01) != 0;
|
||||
this.moonPhaseSupported = (this.supportedBitmap & 0x02) != 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -105,11 +105,6 @@ public class ID115Coordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -102,11 +102,6 @@ public class ITagCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -188,12 +188,6 @@ public class BFH16DeviceCoordinator extends AbstractBLEDeviceCoordinator
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather()
|
||||
{
|
||||
|
@ -140,11 +140,6 @@ public class TeclastH30Coordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
|
@ -92,7 +92,7 @@ public class Y5Coordinator extends AbstractBLEDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
public boolean supportsSmartWakeup(GBDevice device, int position) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -108,11 +108,6 @@ public class LefunDeviceCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return NUM_ALARM_SLOTS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
|
@ -140,11 +140,6 @@ public class WatchXPlusDeviceCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
|
@ -80,11 +80,6 @@ public class LiveviewCoordinator extends AbstractBLClassicDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -223,11 +223,6 @@ public class MakibesHR3Coordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 8;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
|
@ -139,7 +139,7 @@ public class MiBandCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
public boolean supportsSmartWakeup(GBDevice device, int position) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -115,11 +115,6 @@ public class MiScale2DeviceCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -101,11 +101,6 @@ public class No1F1Coordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
|
@ -109,11 +109,6 @@ public class NutCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -131,11 +131,6 @@ public class PebbleCoordinator extends AbstractBLClassicDeviceCoordinator {
|
||||
return 16;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return PebbleUtils.hasHRM(device.getModel());
|
||||
|
@ -84,11 +84,6 @@ public class PineTimeJFCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
|
@ -99,11 +99,6 @@ public class QC35Coordinator extends AbstractBLClassicDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -166,11 +166,6 @@ public class QHybridCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return isHybridHR();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return this.isHybridHR();
|
||||
|
@ -89,11 +89,6 @@ public abstract class RoidmiCoordinator extends AbstractBLClassicDeviceCoordinat
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -107,12 +107,6 @@ public class SMAQ2OSSCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -79,11 +79,6 @@ public class SoFlowCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -98,11 +98,6 @@ public abstract class SonyHeadphonesCoordinator extends AbstractBLClassicDeviceC
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -213,7 +213,7 @@ public class SonyWena3Coordinator extends AbstractBLEDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
public boolean supportsSmartWakeup(GBDevice device, int position) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -93,7 +93,7 @@ public class SonySWR12DeviceCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
public boolean supportsSmartWakeup(GBDevice device, int position) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -113,11 +113,6 @@ public class SuperCarsCoordinator extends AbstractDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Brand Base";
|
||||
|
@ -203,12 +203,17 @@ public class TestDeviceCoordinator extends AbstractDeviceCoordinator {
|
||||
|
||||
@Override
|
||||
public int getAlarmSlotCount(final GBDevice device) {
|
||||
return super.getAlarmSlotCount(device);
|
||||
return 5;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(final GBDevice device) {
|
||||
return supports(getTestDevice(), TestFeature.SMART_WAKEUP);
|
||||
public boolean supportsSmartWakeup(final GBDevice device, int position) {
|
||||
return supports(getTestDevice(), TestFeature.SMART_WAKEUP) && position <= 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forcedSmartWakeup(final GBDevice device, final int alarmPosition) {
|
||||
return supports(getTestDevice(), TestFeature.SMART_WAKEUP_FORCED_SLOT) && alarmPosition == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -56,6 +56,7 @@ public enum TestFeature {
|
||||
SCREENSHOTS,
|
||||
SLEEP_RESPIRATORY_RATE,
|
||||
SMART_WAKEUP,
|
||||
SMART_WAKEUP_FORCED_SLOT,
|
||||
SPO2,
|
||||
STRESS_MEASUREMENT,
|
||||
UNICODE_EMOJIS,
|
||||
|
@ -88,11 +88,6 @@ public class TLW64Coordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -118,11 +118,6 @@ public class UM25Coordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -125,11 +125,6 @@ public class VescCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -81,11 +81,6 @@ public class VibratissimoCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -94,11 +94,6 @@ public class VivomoveHrCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
|
@ -104,11 +104,6 @@ public class WaspOSCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -116,10 +116,6 @@ public class Watch9DeviceCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 3; // FIXME - check the real value
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
|
@ -131,7 +131,7 @@ public class WithingsSteelHRDeviceCoordinator extends AbstractDeviceCoordinator
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
public boolean supportsSmartWakeup(GBDevice device, int position) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -174,7 +174,7 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(final GBDevice device) {
|
||||
public boolean supportsSmartWakeup(final GBDevice device, int position) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -87,11 +87,6 @@ public class XWatchCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return false;
|
||||
|
@ -129,11 +129,6 @@ public class ZeTimeCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRealtimeData() {
|
||||
return true;
|
||||
|
@ -35,6 +35,7 @@ import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AndroidUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.PendingIntentUtils;
|
||||
@ -44,7 +45,9 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
public class TimeChangeReceiver extends BroadcastReceiver {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TimeChangeReceiver.class);
|
||||
|
||||
public static final String ACTION_DST_CHANGED = "nodomain.freeyourgadget.gadgetbridge.DST_CHANGED";
|
||||
public static final String ACTION_DST_CHANGED_OR_PERIODIC_SYNC = "nodomain.freeyourgadget.gadgetbridge.DST_CHANGED_OR_PERIODIC_SYNC";
|
||||
public static final long PERIODIC_SYNC_INTERVAL_MS = 158003000; // 43:53:23.000
|
||||
public static final long PERIODIC_SYNC_INTERVAL_MAX_MS = 172800000; // 48 hours
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
@ -63,7 +66,7 @@ public class TimeChangeReceiver extends BroadcastReceiver {
|
||||
switch (action) {
|
||||
case Intent.ACTION_TIME_CHANGED:
|
||||
case Intent.ACTION_TIMEZONE_CHANGED:
|
||||
case ACTION_DST_CHANGED:
|
||||
case ACTION_DST_CHANGED_OR_PERIODIC_SYNC:
|
||||
// Continue after the switch
|
||||
break;
|
||||
default:
|
||||
@ -71,49 +74,62 @@ public class TimeChangeReceiver extends BroadcastReceiver {
|
||||
return;
|
||||
}
|
||||
|
||||
// acquire wake lock, otherwise device might enter deep sleep immediately after returning from onReceive()
|
||||
AndroidUtils.acquirePartialWakeLock(context, "TimeSyncWakeLock", 10100);
|
||||
|
||||
final Date newTime = GregorianCalendar.getInstance().getTime();
|
||||
LOG.info("Time or Timezone changed, syncing with device: {} ({}), {}", DateTimeUtils.formatDate(newTime), newTime.toGMTString(), intent.getAction());
|
||||
LOG.info("Time/Timezone changed or periodic sync, syncing with device: {} ({}), {}", DateTimeUtils.formatDate(newTime), newTime.toGMTString(), intent.getAction());
|
||||
GBApplication.deviceService().onSetTime();
|
||||
|
||||
// Reschedule the next DST change, since the timezone may have changed
|
||||
scheduleNextDstChange(context);
|
||||
// Reschedule the next DST change (since the timezone may have changed) or periodic sync
|
||||
scheduleNextDstChangeOrPeriodicSync(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule an alarm to trigger on the next DST change, since ACTION_TIMEZONE_CHANGED is not broadcast otherwise.
|
||||
* Schedule an alarm to trigger on the next DST change, since ACTION_TIMEZONE_CHANGED is not broadcast otherwise
|
||||
* or schedule an alarm to trigger after PERIODIC_SYNC_INTERVAL_MS (whichever is earlier).
|
||||
*
|
||||
* @param context the context
|
||||
*/
|
||||
public static void scheduleNextDstChange(final Context context) {
|
||||
public static void scheduleNextDstChangeOrPeriodicSync(final Context context) {
|
||||
final ZoneId zoneId = ZoneId.systemDefault();
|
||||
final ZoneRules zoneRules = zoneId.getRules();
|
||||
final Instant now = Instant.now();
|
||||
final ZoneOffsetTransition transition = zoneRules.nextTransition(now);
|
||||
if (transition == null) {
|
||||
LOG.warn("No DST transition found for {}", zoneId);
|
||||
return;
|
||||
}
|
||||
|
||||
final long nextDstMillis = transition.getInstant().toEpochMilli();
|
||||
final long delayMillis = nextDstMillis - now.toEpochMilli() + 5000L;
|
||||
|
||||
final Intent i = new Intent(ACTION_DST_CHANGED);
|
||||
final Intent i = new Intent(ACTION_DST_CHANGED_OR_PERIODIC_SYNC);
|
||||
final PendingIntent pi = PendingIntentUtils.getBroadcast(context, 0, i, 0, false);
|
||||
|
||||
final AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
||||
final boolean exactAlarm = canScheduleExactAlarms(context, am);
|
||||
|
||||
LOG.info("Scheduling next DST change: {} (in {} millis) (exact = {})", nextDstMillis, delayMillis, exactAlarm);
|
||||
boolean exactAlarm = false;
|
||||
long delayMillis = PERIODIC_SYNC_INTERVAL_MS;
|
||||
|
||||
if (transition != null) {
|
||||
final long nextDstMillis = transition.getInstant().toEpochMilli();
|
||||
final long dstDelayMillis = nextDstMillis - now.toEpochMilli() + 5000L;
|
||||
if (dstDelayMillis < PERIODIC_SYNC_INTERVAL_MAX_MS) {
|
||||
exactAlarm = canScheduleExactAlarms(context, am);
|
||||
delayMillis = dstDelayMillis;
|
||||
LOG.info("Scheduling next DST change: {} (in {} millis) (exact = {})", nextDstMillis, delayMillis, exactAlarm);
|
||||
}
|
||||
} else {
|
||||
LOG.warn("No DST transition found for {}", zoneId);
|
||||
}
|
||||
|
||||
if (delayMillis == PERIODIC_SYNC_INTERVAL_MS) {
|
||||
LOG.info("Scheduling next periodic time sync in {} millis (exact = {})", delayMillis, exactAlarm);
|
||||
}
|
||||
|
||||
am.cancel(pi);
|
||||
|
||||
boolean scheduledExact = false;
|
||||
if (exactAlarm) {
|
||||
try {
|
||||
am.setExact(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + delayMillis, pi);
|
||||
am.setExact(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + delayMillis, pi);
|
||||
scheduledExact = true;
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to schedule exact alarm for next DST change", e);
|
||||
LOG.error("Failed to schedule exact alarm for next DST change or periodic time sync", e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,16 +137,22 @@ public class TimeChangeReceiver extends BroadcastReceiver {
|
||||
if (!scheduledExact) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
am.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + delayMillis, pi);
|
||||
am.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + delayMillis, pi);
|
||||
} else {
|
||||
am.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + delayMillis, pi);
|
||||
am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + delayMillis, pi);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to schedule inexact alarm next DST change", e);
|
||||
LOG.error("Failed to schedule inexact alarm for next DST change or periodic time sync", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void ifEnabledScheduleNextDstChangeOrPeriodicSync(final Context context) {
|
||||
if (GBApplication.getPrefs().getBoolean("datetime_synconconnect", true)) {
|
||||
scheduleNextDstChangeOrPeriodicSync(context);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean canScheduleExactAlarms(final Context context, final AlarmManager am) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
return am.canScheduleExactAlarms();
|
||||
|
@ -1179,8 +1179,11 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction("android.intent.action.TIME_SET");
|
||||
filter.addAction("android.intent.action.TIMEZONE_CHANGED");
|
||||
filter.addAction(TimeChangeReceiver.ACTION_DST_CHANGED);
|
||||
filter.addAction(TimeChangeReceiver.ACTION_DST_CHANGED_OR_PERIODIC_SYNC);
|
||||
registerReceiver(mTimeChangeReceiver, filter);
|
||||
// Ensure alarm is scheduled after registering broadcast receiver
|
||||
// (this is important in case receiver was unregistered when the previous alarm arrived).
|
||||
TimeChangeReceiver.ifEnabledScheduleNextDstChangeOrPeriodicSync(this);
|
||||
}
|
||||
if (mBlueToothPairingRequestReceiver == null) {
|
||||
mBlueToothPairingRequestReceiver = new BluetoothPairingRequestReceiver(this);
|
||||
|
@ -125,7 +125,7 @@ public class ZeppOsAlarmsService extends AbstractZeppOsService {
|
||||
if (alarm.getEnabled()) {
|
||||
alarmFlags = FLAG_ENABLED;
|
||||
}
|
||||
if (coordinator.supportsSmartWakeup(getSupport().getDevice()) && alarm.getSmartWakeup()) {
|
||||
if (coordinator.supportsSmartWakeup(getSupport().getDevice(), alarm.getPosition()) && alarm.getSmartWakeup()) {
|
||||
alarmFlags |= FLAG_SMART;
|
||||
}
|
||||
alarmMessage = new byte[]{
|
||||
|
@ -47,9 +47,11 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.DeviceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FindPhone;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Menstrual;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.MusicControl;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetPhoneInfoRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendMenstrualModifyTimeRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherDeviceRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetMusicStatusRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
@ -95,6 +97,7 @@ public class AsynchronousResponse {
|
||||
handleCallControls(response);
|
||||
handlePhoneInfo(response);
|
||||
handleMenstrualModifyTime(response);
|
||||
handleWeatherCheck(response);
|
||||
} catch (Request.ResponseParseException e) {
|
||||
LOG.error("Response parse exception", e);
|
||||
}
|
||||
@ -378,4 +381,18 @@ public class AsynchronousResponse {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void handleWeatherCheck(HuaweiPacket response) {
|
||||
if (response.serviceId == Weather.id && response.commandId == 0x04) {
|
||||
// Send back ok
|
||||
try {
|
||||
SendWeatherDeviceRequest sendWeatherDeviceRequest = new SendWeatherDeviceRequest(this.support);
|
||||
sendWeatherDeviceRequest.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Could not send weather device request", e);
|
||||
}
|
||||
|
||||
// TODO: send back weather?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btbr.AbstractBTBRDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder;
|
||||
|
||||
@ -117,4 +118,9 @@ public class HuaweiBRSupport extends AbstractBTBRDeviceSupport {
|
||||
if (!start)
|
||||
supportProvider.onStopFindPhone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendWeather(WeatherSpec weatherSpec) {
|
||||
supportProvider.onSendWeather(weatherSpec);
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
@ -125,4 +126,9 @@ public class HuaweiLESupport extends AbstractBTLEDeviceSupport {
|
||||
if (!start)
|
||||
supportProvider.onStopFindPhone();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendWeather(WeatherSpec weatherSpec) {
|
||||
supportProvider.onSendWeather(weatherSpec);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2024 Damien Gaignon, Martin.JM
|
||||
/* Copyright (C) 2024 Damien Gaignon, Martin.JM, Vitalii Tomin
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@ -50,6 +50,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCoordinatorSupp
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiCrypto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
@ -73,11 +74,20 @@ import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.RecordedDataTypes;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetEventAlarmList;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetNotificationConstraintsRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetSmartAlarmList;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendGpsAndTimeToDeviceRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherCurrentRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotifyHeartRateCapabilityRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotifyRestHeartRateCapabilityRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherExtendedSupportRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherForecastRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherStartRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherSunMoonSupportRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherSupportRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherUnitRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetAutomaticHeartrateRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetAutomaticSpoRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetDisconnectNotification;
|
||||
@ -1637,4 +1647,115 @@ public class HuaweiSupportProvider {
|
||||
LOG.error("Failed to set language settings request", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Weather.WeatherIcon openWeatherMapConditionCodeToHuaweiIcon(int conditionCode) {
|
||||
// More exact first, groups after
|
||||
switch (conditionCode) {
|
||||
case 500:
|
||||
return Weather.WeatherIcon.LIGHT_RAIN;
|
||||
case 501:
|
||||
return Weather.WeatherIcon.RAIN;
|
||||
case 502:
|
||||
return Weather.WeatherIcon.HEAVY_RAIN;
|
||||
case 503:
|
||||
return Weather.WeatherIcon.RAIN_STORM;
|
||||
case 504:
|
||||
return Weather.WeatherIcon.SEVERE_RAIN_STORMS;
|
||||
case 511:
|
||||
return Weather.WeatherIcon.FREEZING_RAIN;
|
||||
case 600:
|
||||
return Weather.WeatherIcon.LIGHT_SNOW;
|
||||
case 601:
|
||||
return Weather.WeatherIcon.SNOW;
|
||||
case 602:
|
||||
return Weather.WeatherIcon.HEAVY_SNOW;
|
||||
case 611:
|
||||
return Weather.WeatherIcon.SLEET;
|
||||
case 701:
|
||||
case 741:
|
||||
return Weather.WeatherIcon.FOG;
|
||||
case 721:
|
||||
return Weather.WeatherIcon.HAZY;
|
||||
case 751:
|
||||
return Weather.WeatherIcon.SAND;
|
||||
case 761:
|
||||
return Weather.WeatherIcon.DUST;
|
||||
case 800:
|
||||
return Weather.WeatherIcon.SUNNY;
|
||||
case 801:
|
||||
case 802:
|
||||
return Weather.WeatherIcon.CLOUDY;
|
||||
case 803:
|
||||
case 804:
|
||||
return Weather.WeatherIcon.OVERCAST;
|
||||
}
|
||||
if (conditionCode >= 200 && conditionCode < 300)
|
||||
return Weather.WeatherIcon.THUNDERSTORMS;
|
||||
if (conditionCode >= 300 && conditionCode < 400)
|
||||
return Weather.WeatherIcon.LIGHT_RAIN;
|
||||
if (conditionCode >= 500 && conditionCode < 600)
|
||||
return Weather.WeatherIcon.RAIN;
|
||||
if (conditionCode >= 600 && conditionCode < 700)
|
||||
return Weather.WeatherIcon.SNOW;
|
||||
return Weather.WeatherIcon.UNKNOWN;
|
||||
}
|
||||
|
||||
public void onSendWeather(WeatherSpec weatherSpec) {
|
||||
// Initialize weather settings and send weather
|
||||
if (!getHuaweiCoordinator().supportsWeather()) {
|
||||
LOG.error("onSendWeather called while weather is not supported.");
|
||||
return;
|
||||
}
|
||||
|
||||
Weather.Settings weatherSettings = new Weather.Settings();
|
||||
|
||||
SendWeatherStartRequest weatherStartRequest = new SendWeatherStartRequest(this, weatherSettings);
|
||||
Request lastRequest = weatherStartRequest;
|
||||
|
||||
if (getHuaweiCoordinator().supportsWeatherUnit()) {
|
||||
SendWeatherUnitRequest weatherUnitRequest = new SendWeatherUnitRequest(this);
|
||||
lastRequest.nextRequest(weatherUnitRequest);
|
||||
lastRequest = weatherUnitRequest;
|
||||
}
|
||||
|
||||
SendWeatherSupportRequest weatherSupportRequest = new SendWeatherSupportRequest(this, weatherSettings);
|
||||
lastRequest.nextRequest(weatherSupportRequest);
|
||||
lastRequest = weatherSupportRequest;
|
||||
|
||||
if (getHuaweiCoordinator().supportsWeatherExtended()) {
|
||||
SendWeatherExtendedSupportRequest weatherExtendedSupportRequest = new SendWeatherExtendedSupportRequest(this, weatherSettings);
|
||||
lastRequest.nextRequest(weatherExtendedSupportRequest);
|
||||
lastRequest = weatherExtendedSupportRequest;
|
||||
}
|
||||
|
||||
if (getHuaweiCoordinator().supportsWeatherMoonRiseSet()) {
|
||||
SendWeatherSunMoonSupportRequest weatherSunMoonSupportRequest = new SendWeatherSunMoonSupportRequest(this, weatherSettings);
|
||||
lastRequest.nextRequest(weatherSunMoonSupportRequest);
|
||||
lastRequest = weatherSunMoonSupportRequest;
|
||||
}
|
||||
|
||||
// End of initialization and start of actually sending weather
|
||||
|
||||
SendWeatherCurrentRequest sendWeatherCurrentRequest = new SendWeatherCurrentRequest(this, weatherSettings, weatherSpec);
|
||||
lastRequest.nextRequest(sendWeatherCurrentRequest);
|
||||
lastRequest = sendWeatherCurrentRequest;
|
||||
|
||||
SendGpsAndTimeToDeviceRequest sendGpsAndTimeToDeviceRequest = new SendGpsAndTimeToDeviceRequest(this);
|
||||
lastRequest.nextRequest(sendGpsAndTimeToDeviceRequest);
|
||||
lastRequest = sendGpsAndTimeToDeviceRequest;
|
||||
|
||||
if (getHuaweiCoordinator().supportsWeatherForecasts()) {
|
||||
SendWeatherForecastRequest sendWeatherForecastRequest = new SendWeatherForecastRequest(this, weatherSettings, weatherSpec);
|
||||
lastRequest.nextRequest(sendWeatherForecastRequest);
|
||||
lastRequest = sendWeatherForecastRequest;
|
||||
}
|
||||
|
||||
try {
|
||||
weatherStartRequest.doPerform();
|
||||
} catch (IOException e) {
|
||||
// TODO: Use translatable string
|
||||
GB.toast(context, "Failed to send weather", Toast.LENGTH_SHORT, GB.ERROR, e);
|
||||
LOG.error("Failed to send weather", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,52 @@
|
||||
/* Copyright (C) 2024 Vitalii Tomin, Martin.JM
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.CurrentPosition;
|
||||
|
||||
public class SendGpsAndTimeToDeviceRequest extends Request {
|
||||
|
||||
public SendGpsAndTimeToDeviceRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = GpsAndTime.id;
|
||||
this.commandId = GpsAndTime.CurrentGPSRequest.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
Location location = new CurrentPosition().getLastKnownLocation();
|
||||
return new GpsAndTime.CurrentGPSRequest(
|
||||
this.paramsProvider,
|
||||
(int) (Calendar.getInstance().getTime().getTime() / 1000L) - 60, // Backdating a bit seems to work better
|
||||
location.getLatitude(),
|
||||
location.getLongitude()
|
||||
).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/* Copyright (C) 2024 Martin.JM
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
|
||||
|
||||
public class SendWeatherCurrentRequest extends Request {
|
||||
Weather.Settings settings;
|
||||
WeatherSpec weatherSpec;
|
||||
|
||||
public SendWeatherCurrentRequest(HuaweiSupportProvider support, Weather.Settings settings, WeatherSpec weatherSpec) {
|
||||
super(support);
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = Weather.CurrentWeatherRequest.id;
|
||||
this.settings = settings;
|
||||
this.weatherSpec = weatherSpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
Weather.HuaweiTemperatureFormat temperatureFormat = Weather.HuaweiTemperatureFormat.CELSIUS;
|
||||
String unit = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric));
|
||||
if (unit.equals(GBApplication.getContext().getString(R.string.p_unit_imperial)))
|
||||
temperatureFormat = Weather.HuaweiTemperatureFormat.FAHRENHEIT;
|
||||
try {
|
||||
Short pm25 = null;
|
||||
Short aqi = null;
|
||||
if (weatherSpec.airQuality != null) {
|
||||
pm25 = (short) weatherSpec.airQuality.pm25; // TODO: does this work?
|
||||
aqi = (short) weatherSpec.airQuality.aqi;
|
||||
}
|
||||
return new Weather.CurrentWeatherRequest(
|
||||
this.paramsProvider,
|
||||
settings,
|
||||
supportProvider.openWeatherMapConditionCodeToHuaweiIcon(weatherSpec.currentConditionCode),
|
||||
(byte) weatherSpec.windDirection,
|
||||
(byte) weatherSpec.windSpeedAsBeaufort(),
|
||||
(byte) (weatherSpec.todayMinTemp - 273),
|
||||
(byte) (weatherSpec.todayMaxTemp - 273),
|
||||
pm25,
|
||||
weatherSpec.location,
|
||||
(byte) (weatherSpec.currentTemp - 273),
|
||||
temperatureFormat,
|
||||
aqi,
|
||||
weatherSpec.timestamp,
|
||||
"Gadgetbridge"
|
||||
).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/* Copyright (C) 2024 Martin.JM
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendWeatherDeviceRequest extends Request {
|
||||
|
||||
public SendWeatherDeviceRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = 0x04;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new Weather.WeatherDeviceRequest(paramsProvider, HuaweiConstants.RESULT_SUCCESS_INT).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/* Copyright (C) 2024 Martin.JM
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendWeatherExtendedSupportRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendWeatherExtendedSupportRequest.class);
|
||||
|
||||
private Weather.Settings settings;
|
||||
|
||||
public SendWeatherExtendedSupportRequest(HuaweiSupportProvider support, Weather.Settings settings) {
|
||||
super(support);
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = Weather.WeatherExtendedSupport.id;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new Weather.WeatherExtendedSupport.Request(this.paramsProvider).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
if (receivedPacket instanceof Weather.WeatherExtendedSupport.Response) {
|
||||
this.settings.timeSupported = ((Weather.WeatherExtendedSupport.Response) receivedPacket).timeSupported;
|
||||
this.settings.sourceSupported = ((Weather.WeatherExtendedSupport.Response) receivedPacket).sourceSupported;
|
||||
this.settings.weatherIconSupported = ((Weather.WeatherExtendedSupport.Response) receivedPacket).weatherIconSupported;
|
||||
} else {
|
||||
LOG.error("WeatherExtendedSupport response is not of type WeatherExtendedSupport response");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
/* Copyright (C) 2024 Martin.JM
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather.WeatherForecastData;
|
||||
|
||||
public class SendWeatherForecastRequest extends Request {
|
||||
Weather.Settings weatherSettings;
|
||||
WeatherSpec weatherSpec;
|
||||
|
||||
public SendWeatherForecastRequest(HuaweiSupportProvider support, Weather.Settings weatherSettings, WeatherSpec weatherSpec) {
|
||||
super(support);
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = Weather.WeatherForecastData.id;
|
||||
this.weatherSettings = weatherSettings;
|
||||
this.weatherSpec = weatherSpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
int hourlyCount = Math.min(weatherSpec.hourly.size(), 24);
|
||||
int dayCount = Math.min(weatherSpec.forecasts.size(), 8);
|
||||
|
||||
ArrayList<WeatherForecastData.TimeData> timeDataArrayList = new ArrayList<>(hourlyCount);
|
||||
ArrayList<WeatherForecastData.DayData> dayDataArrayList = new ArrayList<>(dayCount);
|
||||
for (int i = 0; i < hourlyCount; i++) {
|
||||
WeatherSpec.Hourly hourly = weatherSpec.hourly.get(i);
|
||||
WeatherForecastData.TimeData timeData = new WeatherForecastData.TimeData();
|
||||
timeData.timestamp = hourly.timestamp;
|
||||
timeData.icon = supportProvider.openWeatherMapConditionCodeToHuaweiIcon(hourly.conditionCode);
|
||||
timeData.temperature = (byte) (hourly.temp - 273);
|
||||
timeDataArrayList.add(timeData);
|
||||
}
|
||||
|
||||
// Add today as well
|
||||
WeatherForecastData.DayData today = new WeatherForecastData.DayData();
|
||||
today.timestamp = weatherSpec.timestamp;
|
||||
today.icon = supportProvider.openWeatherMapConditionCodeToHuaweiIcon(weatherSpec.currentConditionCode);
|
||||
today.highTemperature = (byte) (weatherSpec.todayMaxTemp - 273);
|
||||
today.lowTemperature = (byte) (weatherSpec.todayMinTemp - 273);
|
||||
today.sunriseTime = weatherSpec.sunRise;
|
||||
today.sunsetTime = weatherSpec.sunSet;
|
||||
today.moonRiseTime = weatherSpec.moonRise;
|
||||
today.moonSetTime = weatherSpec.moonSet;
|
||||
today.moonPhase = Weather.degreesToMoonPhase(weatherSpec.moonPhase);
|
||||
dayDataArrayList.add(today);
|
||||
|
||||
for (int i = 0; i < dayCount - 1; i++) {
|
||||
WeatherSpec.Daily daily = weatherSpec.forecasts.get(i);
|
||||
WeatherForecastData.DayData dayData = new WeatherForecastData.DayData();
|
||||
dayData.timestamp = weatherSpec.timestamp + (60*60*24 * (i + 1));
|
||||
dayData.icon = supportProvider.openWeatherMapConditionCodeToHuaweiIcon(daily.conditionCode);
|
||||
dayData.highTemperature = (byte) (daily.maxTemp - 273);
|
||||
dayData.lowTemperature = (byte) (daily.minTemp - 273);
|
||||
dayData.sunriseTime = daily.sunRise;
|
||||
dayData.sunsetTime = daily.sunSet;
|
||||
dayData.moonRiseTime = daily.moonRise;
|
||||
dayData.moonSetTime = daily.moonSet;
|
||||
dayData.moonPhase = Weather.degreesToMoonPhase(daily.moonPhase);
|
||||
dayDataArrayList.add(dayData);
|
||||
}
|
||||
try {
|
||||
return new WeatherForecastData.Request(
|
||||
this.paramsProvider,
|
||||
this.weatherSettings,
|
||||
timeDataArrayList,
|
||||
dayDataArrayList
|
||||
).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
/* Copyright (C) 2024 Martin.JM
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class SendWeatherStartRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendWeatherStartRequest.class);
|
||||
|
||||
public int response = -1;
|
||||
private Weather.Settings weatherSettings;
|
||||
|
||||
public SendWeatherStartRequest(HuaweiSupportProvider support, Weather.Settings weatherSettings) {
|
||||
super(support);
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = Weather.WeatherStart.id;
|
||||
this.weatherSettings = weatherSettings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new Weather.WeatherStart.Request(this.paramsProvider).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
if (receivedPacket instanceof Weather.WeatherStart.Response) {
|
||||
if (!((Weather.WeatherStart.Response) receivedPacket).success) {
|
||||
this.stopChain();
|
||||
GB.toast(supportProvider.getContext(), "Received non-ok status for WeatherStart response", Toast.LENGTH_SHORT, GB.INFO);
|
||||
LOG.info("Received non-ok status for WeatherStart response");
|
||||
} else {
|
||||
weatherSettings.weatherSupported = true;
|
||||
}
|
||||
} else {
|
||||
this.stopChain();
|
||||
GB.toast(supportProvider.getContext(), "WeatherStart response is not of type WeatherStart response", Toast.LENGTH_SHORT, GB.ERROR);
|
||||
LOG.error("WeatherStart response is not of type WeatherStart response");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
/* Copyright (C) 2024 Martin.JM
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendWeatherSunMoonSupportRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendWeatherSunMoonSupportRequest.class);
|
||||
|
||||
private Weather.Settings settings;
|
||||
|
||||
public SendWeatherSunMoonSupportRequest(HuaweiSupportProvider support, Weather.Settings settings) {
|
||||
super(support);
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = Weather.WeatherSunMoonSupport.id;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new Weather.WeatherSunMoonSupport.Request(this.paramsProvider).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
if (receivedPacket instanceof Weather.WeatherSunMoonSupport.Response) {
|
||||
this.settings.sunRiseSetSupported = ((Weather.WeatherSunMoonSupport.Response) receivedPacket).sunRiseSetSupported;
|
||||
this.settings.moonPhaseSupported = ((Weather.WeatherSunMoonSupport.Response) receivedPacket).moonPhaseSupported;
|
||||
} else {
|
||||
LOG.error("WeatherExtendedSupport response is not of type WeatherExtendedSupport response");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/* Copyright (C) 2024 Martin.JM
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendWeatherSupportRequest extends Request {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SendWeatherSupportRequest.class);
|
||||
|
||||
private Weather.Settings settings;
|
||||
|
||||
public SendWeatherSupportRequest(HuaweiSupportProvider support, Weather.Settings settings) {
|
||||
super(support);
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = Weather.WeatherSupport.id;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new Weather.WeatherSupport.Request(this.paramsProvider).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processResponse() throws ResponseParseException {
|
||||
if (receivedPacket instanceof Weather.WeatherSupport.Response) {
|
||||
this.settings.weatherSupported = ((Weather.WeatherSupport.Response) receivedPacket).weatherSupported;
|
||||
this.settings.windSupported = ((Weather.WeatherSupport.Response) receivedPacket).windSupported;
|
||||
this.settings.pm25Supported = ((Weather.WeatherSupport.Response) receivedPacket).pm25Supported;
|
||||
this.settings.temperatureSupported = ((Weather.WeatherSupport.Response) receivedPacket).temperatureSupported;
|
||||
this.settings.locationNameSupported = ((Weather.WeatherSupport.Response) receivedPacket).locationNameSupported;
|
||||
this.settings.currentTemperatureSupported = ((Weather.WeatherSupport.Response) receivedPacket).currentTemperatureSupported;
|
||||
this.settings.unitSupported = ((Weather.WeatherSupport.Response) receivedPacket).unitSupported;
|
||||
this.settings.airQualityIndexSupported = ((Weather.WeatherSupport.Response) receivedPacket).airQualityIndexSupported;
|
||||
} else {
|
||||
LOG.error("WeatherSupport response is not of type WeatherSupport response");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/* Copyright (C) 2024 Martin.JM
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendWeatherUnitRequest extends Request {
|
||||
|
||||
public SendWeatherUnitRequest(HuaweiSupportProvider support) {
|
||||
super(support);
|
||||
this.serviceId = Weather.id;
|
||||
this.commandId = Weather.WeatherUnitRequest.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
Weather.HuaweiTemperatureFormat temperatureFormat = Weather.HuaweiTemperatureFormat.CELSIUS;
|
||||
String unit = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric));
|
||||
if (unit.equals(GBApplication.getContext().getString(R.string.p_unit_imperial)))
|
||||
temperatureFormat = Weather.HuaweiTemperatureFormat.FAHRENHEIT;
|
||||
try {
|
||||
return new Weather.WeatherUnitRequest(this.paramsProvider, temperatureFormat).serialize();
|
||||
} catch (HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -32,6 +32,7 @@ import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelUuid;
|
||||
import android.os.Parcelable;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.text.TextUtils;
|
||||
@ -337,4 +338,16 @@ public class AndroidUtils {
|
||||
}
|
||||
GBApplication.getContext().startActivity(launchIntent);
|
||||
}
|
||||
|
||||
public static PowerManager.WakeLock acquirePartialWakeLock(Context context, String tag, long timeout) {
|
||||
try {
|
||||
PowerManager powermanager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
PowerManager.WakeLock wl = powermanager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Gadgetbridge:" + tag);
|
||||
wl.acquire(timeout);
|
||||
return wl;
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to take partial wake lock {}: ", tag, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -214,7 +214,7 @@
|
||||
<string name="pref_default">Default</string>
|
||||
<string name="pref_header_datetime">Date and Time</string>
|
||||
<string name="pref_title_datetime_syctimeonconnect">Sync time</string>
|
||||
<string name="pref_summary_datetime_syctimeonconnect">Sync time to Gadgetbridge device when connecting, and when time or time zone changes on Android device</string>
|
||||
<string name="pref_summary_datetime_syctimeonconnect">Sync time to Gadgetbridge device(s) when connecting, when time or time zone changes on Android device, and periodically</string>
|
||||
<string name="pref_title_theme">Theme</string>
|
||||
<string name="pref_theme_light">Light</string>
|
||||
<string name="pref_theme_dark">Dark</string>
|
||||
|
9
app/src/mainline/res/values/strings.xml
Normal file
9
app/src/mainline/res/values/strings.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string translatable="false" name="pebble_content_provider">com.getpebble.android.provider</string>
|
||||
<string translatable="false" name="app_name">@string/application_name_generic</string>
|
||||
<string translatable="false" name="title_activity_controlcenter">@string/title_activity_controlcenter_generic</string>
|
||||
<string translatable="false" name="about_activity_title">@string/about_activity_title_generic</string>
|
||||
<string translatable="false" name="about_description">@string/about_description_generic</string>
|
||||
<string translatable="false" name="gadgetbridge_running">@string/gadgetbridge_running_generic</string>
|
||||
</resources>
|
BIN
app/src/nightly/res/drawable-hdpi/ic_launcher.png
Normal file
BIN
app/src/nightly/res/drawable-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
BIN
app/src/nightly/res/drawable-mdpi/ic_launcher.png
Normal file
BIN
app/src/nightly/res/drawable-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
BIN
app/src/nightly/res/drawable-xhdpi/ic_launcher.png
Normal file
BIN
app/src/nightly/res/drawable-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.6 KiB |
BIN
app/src/nightly/res/drawable-xxhdpi/ic_launcher.png
Normal file
BIN
app/src/nightly/res/drawable-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
app/src/nightly/res/drawable-xxxhdpi/ic_launcher.png
Normal file
BIN
app/src/nightly/res/drawable-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
8
app/src/nightly/res/values/strings.xml
Normal file
8
app/src/nightly/res/values/strings.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<resources>
|
||||
<string translatable="false" name="app_name">@string/application_name_main_nightly</string>
|
||||
<string translatable="false" name="title_activity_controlcenter">@string/title_activity_controlcenter_main_nightly</string>
|
||||
<string translatable="false" name="about_activity_title">@string/about_activity_title_main_nightly</string>
|
||||
<string translatable="false" name="about_description">@string/about_description_main_nightly</string>
|
||||
<string translatable="false" name="gadgetbridge_running">@string/gadgetbridge_running_main_nightly</string>
|
||||
</resources>
|
9
app/src/nopebble/res/values/strings.xml
Normal file
9
app/src/nopebble/res/values/strings.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string translatable="false" name="pebble_content_provider">com.getpebble.android.nopebble.provider</string>
|
||||
<string translatable="false" name="app_name">@string/application_name_main_nopebble</string>
|
||||
<string translatable="false" name="title_activity_controlcenter">@string/title_activity_controlcenter_main_nopebble</string>
|
||||
<string translatable="false" name="about_activity_title">@string/about_activity_title_main_nopebble</string>
|
||||
<string translatable="false" name="about_description">@string/about_description_main_nopebble</string>
|
||||
<string translatable="false" name="gadgetbridge_running">@string/gadgetbridge_running_main_nopebble</string>
|
||||
</resources>
|
Loading…
Reference in New Issue
Block a user