diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java
index 1967fbaea..112a5349d 100644
--- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java
+++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java
@@ -90,6 +90,7 @@ public class GBDaoGenerator {
addCasioGBX100Sample(schema, user, device);
addFitProActivitySample(schema, user, device);
addPineTimeActivitySample(schema, user, device);
+ addWithingsSteelHRActivitySample(schema, user, device);
addHybridHRActivitySample(schema, user, device);
addVivomoveHrActivitySample(schema, user, device);
addGarminFitFile(schema, user, device);
@@ -818,7 +819,7 @@ public class GBDaoGenerator {
Property deviceId = batteryLevel.addLongProperty("deviceId").primaryKey().notNull().getProperty();
batteryLevel.addToOne(device, deviceId);
batteryLevel.addIntProperty("level").notNull();
- batteryLevel.addIntProperty("batteryIndex").notNull().primaryKey();;
+ batteryLevel.addIntProperty("batteryIndex").notNull().primaryKey();
return batteryLevel;
}
@@ -847,4 +848,18 @@ public class GBDaoGenerator {
addHeartRateProperties(activitySample);
return activitySample;
}
+
+ private static Entity addWithingsSteelHRActivitySample(Schema schema, Entity user, Entity device) {
+ Entity activitySample = addEntity(schema, "WithingsSteelHRActivitySample");
+ activitySample.implementsSerializable();
+ addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
+ activitySample.addIntProperty("duration").notNull();
+ activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
+ activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
+ activitySample.addIntProperty("distance").notNull();
+ activitySample.addIntProperty("calories").notNull();
+ addHeartRateProperties(activitySample);
+ activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
+ return activitySample;
+ }
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a6c8f4f9b..919a0f358 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -722,6 +722,10 @@
android:name=".devices.qhybrid.CalibrationActivity"
android:label="@string/qhybrid_title_calibration"
android:parentActivityName=".devices.qhybrid.HRConfigActivity" />
+ . */
+package nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.DashPathEffect;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import nodomain.freeyourgadget.gadgetbridge.R;
+
+public class RotaryControl extends View {
+
+ public interface RotationListener {
+ void onRotation(short movementAmount);
+ }
+
+ private Path circlePath;
+
+ private int controlPointX;
+ private int controlPointY;
+
+ private int controlCenterX;
+ private int controlCenterY;
+ private int controlRadius;
+
+ private int padding;
+ private int controlPointSize;
+ private int controlPointColor;
+ private int lineColor;
+ private int lineThickness;
+ private double startAngle;
+ private double angle ;
+ private boolean isControlPointSelected = false;
+ private Paint paint = new Paint();
+ private Paint controlPointPaint = new Paint();
+ private RotationListener rotationListener;
+
+ public RotaryControl(Context context) {
+ this(context, null);
+ }
+
+ public RotaryControl(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public RotaryControl(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context, attrs, defStyleAttr);
+ }
+
+ private void init(Context context, AttributeSet attrs, int defStyleAttr) {
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RotaryControl, defStyleAttr, 0);
+
+ startAngle = a.getFloat(R.styleable.RotaryControl_start_angle, (float) Math.PI / 2);
+ angle = startAngle;
+ controlPointSize = a.getDimensionPixelSize(R.styleable.RotaryControl_controlpoint_size, 50);
+ controlPointColor = a.getColor(R.styleable.RotaryControl_controlpoint_color, Color.GRAY);
+ lineThickness = a.getDimensionPixelSize(R.styleable.RotaryControl_line_thickness, 20);
+ lineColor = a.getColor(R.styleable.RotaryControl_line_color, Color.RED);
+ calculateAndSetPadding();
+ a.recycle();
+ }
+
+ private void calculateAndSetPadding() {
+ int totalPadding = getPaddingLeft() + getPaddingRight() + getPaddingBottom() + getPaddingTop() + getPaddingEnd() + getPaddingStart();
+ padding = totalPadding / 6;
+ }
+
+ @Override
+ protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
+ int smallerDim = width > height ? height : width;
+ int largestCenteredSquareLeft = (width - smallerDim) / 2;
+ int largestCenteredSquareTop = (height - smallerDim) / 2;
+ int largestCenteredSquareRight = largestCenteredSquareLeft + smallerDim;
+ int largestCenteredSquareBottom = largestCenteredSquareTop + smallerDim;
+ controlCenterX = largestCenteredSquareRight / 2 + (width - largestCenteredSquareRight) / 2;
+ controlCenterY = largestCenteredSquareBottom / 2 + (height - largestCenteredSquareBottom) / 2;
+ controlRadius = smallerDim / 2 - lineThickness / 2 - padding;
+
+ super.onSizeChanged(width, height, oldWidth, oldHeight);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ drawRotationCircle(canvas);
+ drawControlPoint(canvas);
+
+ }
+
+ private void drawControlPoint(Canvas canvas) {
+ controlPointX = (int) (controlCenterX + controlRadius * Math.cos(angle));
+ controlPointY = (int) (controlCenterY - controlRadius * Math.sin(angle));
+ controlPointPaint.setColor(controlPointColor);
+ controlPointPaint.setStyle(Paint.Style.FILL);
+ controlPointPaint.setAlpha(128);
+ Path controlPointPath = new Path();
+ controlPointPath.addCircle(controlPointX, controlPointY, controlPointSize, Path.Direction.CW);
+ canvas.drawPath(controlPointPath, controlPointPaint);
+ }
+
+ private void drawRotationCircle(Canvas canvas) {
+ DashPathEffect dashPath = new DashPathEffect(new float[]{8,22}, (float)1.0);
+ paint.setPathEffect(dashPath);
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(lineThickness);
+ paint.setAntiAlias(true);
+ paint.setColor(lineColor);
+ circlePath = new Path();
+ circlePath.addCircle(controlCenterX, controlCenterY, controlRadius, Path.Direction.CW);
+ canvas.drawPath(circlePath, paint);
+ }
+
+ private void updateRotationPosition(double touchX, double touchY) {
+ double distanceX = touchX - controlCenterX;
+ double distanceY = controlCenterY - touchY;
+ double c = Math.sqrt(Math.pow(distanceX, 2) + Math.pow(distanceY, 2));
+ double currentAngle = Math.acos(distanceX / c);
+ if (distanceY < 0) {
+ currentAngle = -currentAngle;
+ }
+
+ int movementAmount = (int) ((currentAngle - angle) * 100);
+
+ int i = (int) movementAmount;
+ if (movementAmount != 0) {
+ if (Math.abs(movementAmount) > 15) {
+ movementAmount /= movementAmount;
+ }
+
+ rotationListener.onRotation((short) -movementAmount);
+ }
+
+ angle = currentAngle;
+ }
+
+ public void setRotationListener(RotationListener listener) {
+ rotationListener = listener;
+ }
+
+ public void reset() {
+ angle = startAngle;
+ invalidate();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+ double x = ev.getX();
+ double y = ev.getY();
+ if (x < controlPointX + controlPointSize && x > controlPointX - controlPointSize && y < controlPointY + controlPointSize && y > controlPointY - controlPointSize) {
+ getParent().requestDisallowInterceptTouchEvent(true);
+ isControlPointSelected = true;
+ updateRotationPosition(x, y);
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ if (isControlPointSelected) {
+ double x = ev.getX();
+ double y = ev.getY();
+ updateRotationPosition(x, y);
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_UP: {
+ getParent().requestDisallowInterceptTouchEvent(false);
+ isControlPointSelected = false;
+ break;
+ }
+ }
+
+ invalidate();
+ return true;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsCalibrationActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsCalibrationActivity.java
new file mode 100644
index 000000000..f46d1cd82
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsCalibrationActivity.java
@@ -0,0 +1,159 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr;
+
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+
+public class WithingsCalibrationActivity extends AbstractGBActivity {
+
+ enum Hands {
+ HOURS((short)1),
+ MINUTES((short)0),
+ ACTIVITY_TARGET((short)2);
+
+ private short code;
+ private Hands(short code) {
+ this.code = code;
+ }
+
+ }
+
+ private GBDevice device;
+ private LocalBroadcastManager localBroadcastManager;
+ private String[] calibrationAdvices = new String[3];
+ private Hands[] hands = new Hands[]{Hands.HOURS, Hands.MINUTES, Hands.ACTIVITY_TARGET};
+ private short handIndex = 0;
+ private Button previousButton;
+ private Button nextButton;
+ private Button okButton;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_withings_calibration);
+ List devices = GBApplication.app().getDeviceManager().getSelectedDevices();
+ for(GBDevice device : devices){
+ if(device.getType() == DeviceType.WITHINGS_STEEL_HR ){
+ this.device = device;
+ break;
+ }
+ }
+
+ if (device == null) {
+ Toast.makeText(this, R.string.watch_not_connected, Toast.LENGTH_LONG).show();
+ finish();
+ return;
+ }
+
+ initView();
+ localBroadcastManager = LocalBroadcastManager.getInstance(this);
+ localBroadcastManager.sendBroadcast(new Intent(WithingsSteelHRDeviceSupport.START_HANDS_CALIBRATION_CMD));
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (localBroadcastManager != null) {
+ localBroadcastManager.sendBroadcast(new Intent(WithingsSteelHRDeviceSupport.STOP_HANDS_CALIBRATION_CMD));
+ }
+ }
+
+ private void initView() {
+
+ calibrationAdvices[0] = getString(R.string.withings_calibration_text_hours);
+ calibrationAdvices[1] = getString(R.string.withings_calibration_text_minutes);
+ calibrationAdvices[2] = getString(R.string.withings_calibration_text_activity_target);
+
+ RotaryControl rotaryControl = findViewById(R.id.rotary_control);
+ rotaryControl.setRotationListener(new RotaryControl.RotationListener() {
+ @Override
+ public void onRotation(short movementAmount) {
+ Intent calibration = new Intent(WithingsSteelHRDeviceSupport.HANDS_CALIBRATION_CMD);
+ calibration.putExtra("hand", hands[handIndex].code);
+ calibration.putExtra("movementAmount", movementAmount);
+ localBroadcastManager.sendBroadcast(calibration);
+ }
+ });
+
+ TextView textView = findViewById(R.id.withings_calibration_textview);
+ textView.setText(calibrationAdvices[0]);
+ previousButton = findViewById(R.id.withings_calibration_button_previous);
+ previousButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ handIndex--;
+ enableButtons();
+ textView.setText(calibrationAdvices[handIndex]);
+ rotaryControl.reset();
+ }
+ });
+ nextButton = findViewById(R.id.withings_calibration_button_next);
+ nextButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ handIndex++;
+ enableButtons();
+ textView.setText(calibrationAdvices[handIndex]);
+ rotaryControl.reset();
+ }
+ });
+
+ okButton = findViewById(R.id.withings_calibration_button_ok);
+ okButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ finish();
+ }
+ });
+
+ enableButtons();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void enableButtons() {
+ nextButton.setEnabled(handIndex < 2);
+ previousButton.setEnabled(handIndex > 0);
+ okButton.setEnabled(handIndex == 2);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsSteelHRDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsSteelHRDeviceCoordinator.java
new file mode 100644
index 000000000..f964e0272
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsSteelHRDeviceCoordinator.java
@@ -0,0 +1,175 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr;
+
+import android.app.Activity;
+import android.content.Context;
+import android.net.Uri;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.util.Locale;
+
+import de.greenrobot.dao.query.QueryBuilder;
+import nodomain.freeyourgadget.gadgetbridge.GBException;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
+import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySampleDao;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
+
+public class WithingsSteelHRDeviceCoordinator extends AbstractDeviceCoordinator {
+
+ @Override
+ protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
+ Long deviceId = device.getId();
+ QueryBuilder> qb = session.getWithingsSteelHRActivitySampleDao().queryBuilder();
+ qb.where(WithingsSteelHRActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
+ }
+
+ @NonNull
+ @Override
+ public DeviceType getSupportedType(GBDeviceCandidate candidate) {
+ String name = candidate.getDevice().getName();
+ if (name != null && (name.toLowerCase(Locale.ROOT).startsWith("steel") || name.toLowerCase(Locale.ROOT).startsWith("activite"))) {
+ return DeviceType.WITHINGS_STEEL_HR;
+ }
+
+ return DeviceType.UNKNOWN;
+ }
+
+ @Override
+ public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
+ return new int[]{
+ R.xml.devicesettings_withingssteelhr
+ };
+ }
+
+ @Override
+ public DeviceType getDeviceType() {
+ return DeviceType.WITHINGS_STEEL_HR;
+ }
+
+ @Override
+ public int getBondingStyle(){
+ return BONDING_STYLE_BOND;
+ }
+
+
+ @Nullable
+ @Override
+ public Class extends Activity> getPairingActivity() {
+ return null;
+ }
+
+ @Override
+ public boolean supportsRemSleep() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsActivityDataFetching() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsActivityTracking() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsActivityTracks() {
+ return true;
+ }
+
+ @Override
+ public SampleProvider extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
+ return new WithingsSteelHRSampleProvider(device, session);
+ }
+
+ @Override
+ public InstallHandler findInstallHandler(Uri uri, Context context) {
+ return null;
+ }
+
+ @Override
+ public boolean supportsScreenshots() {
+ return false;
+ }
+
+ @Override
+ public int getAlarmSlotCount(GBDevice gbDevice) {
+ return 3;
+ }
+
+ @Override
+ public boolean supportsAlarmDescription(GBDevice device) {
+ return true;
+ }
+
+ @Override
+ public boolean supportsSmartWakeup(GBDevice device) {
+ return true;
+ }
+
+ @Override
+ public boolean supportsHeartRateMeasurement(GBDevice device) {
+ return true;
+ }
+
+ @Override
+ public String getManufacturer() {
+ return "Withings";
+ }
+
+ @Override
+ public boolean supportsAppsManagement(GBDevice gbDevice) {
+ return false;
+ }
+
+ @Override
+ public Class extends Activity> getAppsManagementActivity() {
+ return null;
+ }
+
+ @Override
+ public boolean supportsCalendarEvents() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsRealtimeData() {
+ return true;
+ }
+
+ @Override
+ public boolean supportsWeather() {
+ return false;
+ }
+
+ @Override
+ public boolean supportsFindDevice() {
+ return false;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsSteelHRSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsSteelHRSampleProvider.java
new file mode 100644
index 000000000..393422a98
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsSteelHRSampleProvider.java
@@ -0,0 +1,110 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.List;
+
+import de.greenrobot.dao.AbstractDao;
+import de.greenrobot.dao.Property;
+import de.greenrobot.dao.query.Query;
+import de.greenrobot.dao.query.QueryBuilder;
+import de.greenrobot.dao.query.WhereCondition;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySampleDao;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.ActivitySampleHandler;
+
+public class WithingsSteelHRSampleProvider extends AbstractSampleProvider {
+ private static final Logger logger = LoggerFactory.getLogger(WithingsSteelHRSampleProvider.class);
+
+ public WithingsSteelHRSampleProvider(GBDevice device, DaoSession session) {
+ super(device, session);
+ }
+
+ @Override
+ public AbstractDao getSampleDao() {
+ return getSession().getWithingsSteelHRActivitySampleDao();
+ }
+
+ @Nullable
+ @Override
+ protected Property getRawKindSampleProperty() {
+ return WithingsSteelHRActivitySampleDao.Properties.RawKind;
+ }
+
+ @NonNull
+ @Override
+ protected Property getTimestampSampleProperty() {
+ return WithingsSteelHRActivitySampleDao.Properties.Timestamp;
+ }
+
+ @NonNull
+ @Override
+ protected Property getDeviceIdentifierSampleProperty() {
+ return WithingsSteelHRActivitySampleDao.Properties.DeviceId;
+ }
+
+ @Override
+ public List getActivitySamples(int timestamp_from, int timestamp_to) {
+ return super.getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ALL);
+ }
+
+ @Override
+ public int normalizeType(int rawType) {
+ return rawType;
+ }
+
+ @Override
+ public int toRawActivityKind(int activityKind) {
+ switch (activityKind) {
+ case ActivityKind.TYPE_UNKNOWN:
+ return 0;
+ case ActivityKind.TYPE_LIGHT_SLEEP:
+ return 1;
+ case ActivityKind.TYPE_DEEP_SLEEP:
+ return 2;
+ default:
+ return activityKind;
+ }
+ }
+
+ @Override
+ public float normalizeIntensity(int rawIntensity) {
+ if (rawIntensity > 0) {
+ return (float) (Math.log(rawIntensity) / 8);
+ }
+
+ return 0;
+ }
+
+ @Override
+ public WithingsSteelHRActivitySample createActivitySample() {
+ return new WithingsSteelHRActivitySample();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
index 008108190..d871ec78c 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
@@ -138,6 +138,7 @@ public enum DeviceType {
SUPER_CARS(530, R.drawable.ic_device_supercars, R.drawable.ic_device_supercars_disabled, R.string.devicetype_super_cars),
ASTEROIDOS(540, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_asteroidos),
SOFLOW_SO6(550, R.drawable.ic_device_vesc, R.drawable.ic_device_vesc_disabled, R.string.devicetype_soflow_s06),
+ WITHINGS_STEEL_HR(560, R.drawable.ic_device_watchxplus, R.drawable.ic_device_watchxplus_disabled, R.string.withings_steel_hr),
TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test);
private final int key;
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java
index 54da7a7e2..99e0bb332 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java
@@ -111,6 +111,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.Vibrati
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.VivomoveHrSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.waspos.WaspOSDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xwatch.XWatchSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.zetime.ZeTimeDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -375,6 +376,8 @@ public class DeviceSupportFactory {
return new ServiceDeviceSupport(new AsteroidOSDeviceSupport());
case SOFLOW_SO6:
return new ServiceDeviceSupport(new SoFlowSupport());
+ case WITHINGS_STEEL_HR:
+ return new ServiceDeviceSupport(new WithingsSteelHRDeviceSupport());
case VIVOMOVE_HR:
return new ServiceDeviceSupport(new VivomoveHrSupport());
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/TransactionBuilder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/TransactionBuilder.java
index 0e3b811e7..d6e43f701 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/TransactionBuilder.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/TransactionBuilder.java
@@ -26,6 +26,8 @@ import org.slf4j.LoggerFactory;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
+import java.util.Arrays;
+
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.NotifyAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ReadAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.RequestConnectionPriorityAction;
@@ -61,6 +63,18 @@ public class TransactionBuilder {
return add(action);
}
+ public TransactionBuilder writeChunkedData(BluetoothGattCharacteristic characteristic, byte[] data, int chunkSize) {
+ for (int start = 0; start < data.length; start += chunkSize) {
+ int end = start + chunkSize;
+ if (end > data.length) end = data.length;
+ WriteAction action = new WriteAction(characteristic, Arrays.copyOfRange(data, start, end));
+ add(action);
+ }
+
+ return this;
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public TransactionBuilder requestMtu(int mtu){
return add(
new RequestMtuAction(mtu)
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/AuthenticationHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/AuthenticationHandler.java
new file mode 100644
index 000000000..1bc1a3ab8
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/AuthenticationHandler.java
@@ -0,0 +1,128 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr;
+
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.os.Build;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Random;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.WithingsUUID;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.AbstractResponseHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.Challenge;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ChallengeResponse;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.Probe;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ProbeOsVersion;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ProbeReply;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
+
+public class AuthenticationHandler extends AbstractResponseHandler {
+ private static final Logger logger = LoggerFactory.getLogger(AuthenticationHandler.class);
+
+ // TODO: Save this somewhere if we actually decide to use te secret for more security:
+ private final String secret = "2EM5zNP37QzM00hmP6BFTD92nG15XwNd";
+ private WithingsSteelHRDeviceSupport support;
+ private Challenge challengeToSend;
+
+ public AuthenticationHandler(WithingsSteelHRDeviceSupport support) {
+ super(support);
+ this.support = support;
+ }
+
+ @Override
+ public void handleResponse(Message response) {
+ short messageType = response.getType();
+ if (messageType == WithingsMessageType.PROBE) {
+ handleProbeReply(response);
+ } else if (messageType == WithingsMessageType.CHALLENGE) {
+ handleChallenge(response);
+ } else {
+ logger.warn("Received unkown message: " + messageType + ", will ignore this.");
+ }
+ }
+
+ private void handleChallenge(Message challengeMessage) {
+ try {
+ Challenge challenge = getTypeFromReply(Challenge.class, challengeMessage);
+ ChallengeResponse challengeResponse = new ChallengeResponse();
+ challengeResponse.setResponse(createResponse(challenge));
+ Message message = new WithingsMessage(WithingsMessageType.CHALLENGE);
+ message.addDataStructure(challengeResponse);
+ challengeToSend = new Challenge();
+ challengeToSend.setMacAddress(challenge.getMacAddress());
+ byte[] bArr = new byte[16];
+ new Random().nextBytes(bArr);
+ challengeToSend.setChallenge(bArr);
+ message.addDataStructure(challengeToSend);
+ support.sendToDevice(message);
+ } catch (Exception e) {
+ logger.error("Failed to create response to challenge: " + e.getMessage());
+ }
+ }
+
+ private void handleProbeReply(Message message) {
+ ProbeReply probeReply = getTypeFromReply(ProbeReply.class, message);
+ if (probeReply == null) {
+ throw new IllegalArgumentException("Message does not contain the required datastructure ProbeReply");
+ }
+
+ ChallengeResponse response = getTypeFromReply(ChallengeResponse.class, message);
+
+ if (response == null || Arrays.equals(response.getResponse(), createResponse(challengeToSend))) {
+ support.getDevice().setFirmwareVersion(String.valueOf(probeReply.getFirmwareVersion()));
+ } else {
+ throw new SecurityException("Response is not the one expected!");
+ }
+
+ support.onAuthenticationFinished();
+ }
+
+ private byte[] createResponse(Challenge challenge) {
+ try {
+ ByteBuffer allocate = ByteBuffer.allocate(challenge.getChallenge().length + challenge.getMacAddress().getBytes().length + secret.getBytes().length);
+ allocate.put(challenge.getChallenge());
+ allocate.put(challenge.getMacAddress().getBytes());
+ allocate.put(secret.getBytes());
+ return MessageDigest.getInstance("SHA1").digest(allocate.array());
+ } catch (NoSuchAlgorithmException e) {
+ logger.error("Failed to create response to challenge: " + e.getMessage());
+ }
+
+ return new byte[0];
+ }
+
+ private T getTypeFromReply(Class type, Message message) {
+ for (WithingsStructure structure : message.getDataStructures()) {
+ if (type.isInstance(structure)) {
+ return (T)structure;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/IconHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/IconHelper.java
new file mode 100644
index 000000000..8203b1e59
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/IconHelper.java
@@ -0,0 +1,71 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+
+import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil;
+
+public class IconHelper {
+
+ public static byte[] getIconBytesFromDrawable(Drawable drawable) {
+ Bitmap bitmap = BitmapUtil.toBitmap(drawable);
+ Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, 22, 24, true);
+ int size = scaledBitmap.getRowBytes() * scaledBitmap.getHeight();
+ return toByteArray(scaledBitmap);
+ }
+
+ public static byte[] toByteArray(Bitmap bitmap) {
+ int width = bitmap.getWidth();
+ int height = bitmap.getHeight();
+ int bytesPerColumn = getBytesPerColumn(height);
+ byte[] rawData = new byte[bytesPerColumn * width];
+ for (int col = 0; col < width; col++) {
+ for (int row = 0; row < height; row++) {
+ int pixel = bitmap.getPixel(col, row);
+ if (shouldPixelbeAdded(pixel)) {
+ int bitIndex = bytesPerColumn * col + row / 8;
+ rawData[bitIndex] = setBit(rawData[bitIndex], row);
+ }
+ }
+ }
+
+ return rawData;
+ }
+
+ private static boolean shouldPixelbeAdded(int pixel) {
+ double luma = ((Color.red(pixel) * 0.2126d) + (Color.green(pixel) * 0.7152d) + (Color.blue(pixel) * 0.0722d)) * (Color.alpha(pixel) / 255.0f);
+ return luma > 0;
+ }
+
+ private static byte setBit(byte bits, int position) {
+ bits |= 1 << (position % 8);
+ return bits;
+ }
+
+ private static int getBytesPerColumn(int rowCount) {
+ int result = (int) rowCount / 8;
+ if (result * 8 < rowCount) {
+ result++;
+ }
+
+ return result;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/WithingsSteelHRDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/WithingsSteelHRDeviceSupport.java
new file mode 100644
index 000000000..00ad34dd2
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/WithingsSteelHRDeviceSupport.java
@@ -0,0 +1,725 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr;
+
+import static nodomain.freeyourgadget.gadgetbridge.GBApplication.getContext;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+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.hplus.HPlusConstants;
+import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
+import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
+import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
+import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.ServerTransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.WithingsActivityType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.WithingsServerAction;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.WithingsUUID;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.ActivitySampleHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.BatteryStateHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.Conversation;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.ConversationQueue;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.HeartRateHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.ResponseHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.SetupFinishedHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.SimpleConversation;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.SyncFinishedHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.WorkoutScreenListHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivityTarget;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.AlarmName;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.AlarmSettings;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.AlarmStatus;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.AncsStatus;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.DataStructureFactory;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.EndOfTransmission;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.GetActivitySamples;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ImageData;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ImageMetaData;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.Locale;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.MoveHand;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.Probe;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ProbeOsVersion;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ScreenSettings;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.Time;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.TypeVersion;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.User;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.UserUnit;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.UserUnitConstants;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WorkoutScreen;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.ExpectedResponse;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.MessageBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.MessageFactory;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.SimpleHexToByteMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming.IncomingMessageHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming.IncomingMessageHandlerFactory;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming.LiveWorkoutHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification.GetNotificationAttributes;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification.GetNotificationAttributesResponse;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification.NotificationProvider;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification.NotificationSource;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
+import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
+import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
+
+public class WithingsSteelHRDeviceSupport extends AbstractBTLEDeviceSupport {
+
+ private static final Logger logger = LoggerFactory.getLogger(WithingsSteelHRDeviceSupport.class);
+ public static final String LAST_ACTIVITY_SYNC = "lastActivitySync";
+ public static final String HANDS_CALIBRATION_CMD = "withings_hands_calibration";
+ public static final String START_HANDS_CALIBRATION_CMD = "start_withings_hands_calibration";
+ public static final String STOP_HANDS_CALIBRATION_CMD = "stop_withings_hands_calibration";
+ private static Prefs prefs = GBApplication.getPrefs();
+ private MessageBuilder messageBuilder;
+ private LiveWorkoutHandler liveWorkoutHandler;
+ private ActivitySampleHandler activitySampleHandler;
+ private ConversationQueue conversationQueue;
+ private boolean firstTimeConnect;
+ private BluetoothGattCharacteristic notificationSourceCharacteristic;
+ private BluetoothGattCharacteristic dataSourceCharacteristic;
+ private BluetoothDevice device;
+ private boolean syncInProgress;
+ private ActivityUser activityUser;
+ private NotificationProvider notificationProvider;
+ private IncomingMessageHandlerFactory incomingMessageHandlerFactory;
+ private final BroadcastReceiver commandReceiver;
+ private int mtuSize = 115;
+
+ public WithingsSteelHRDeviceSupport() {
+ super(logger);
+ notificationProvider = NotificationProvider.getInstance(this);
+ messageBuilder = new MessageBuilder(this, new MessageFactory(new DataStructureFactory()));
+ liveWorkoutHandler = new LiveWorkoutHandler(this);
+ incomingMessageHandlerFactory = IncomingMessageHandlerFactory.getInstance(this);
+ addSupportedService(WithingsUUID.WITHINGS_SERVICE_UUID);
+ addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
+ addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
+ addANCSService();
+ activityUser = new ActivityUser();
+
+ IntentFilter commandFilter = new IntentFilter(HANDS_CALIBRATION_CMD);
+ commandFilter.addAction(START_HANDS_CALIBRATION_CMD);
+ commandFilter.addAction(STOP_HANDS_CALIBRATION_CMD);
+ commandReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction() == null) {
+ return;
+ }
+
+ switch (intent.getAction()) {
+ case HANDS_CALIBRATION_CMD:
+ MoveHand moveHand = new MoveHand();
+ moveHand.setHand(intent.getShortExtra("hand", (short)1));
+ moveHand.setMovement(intent.getShortExtra("movementAmount", (short)1));
+ sendToDevice(new WithingsMessage(WithingsMessageType.MOVE_HAND, moveHand));
+ break;
+ case START_HANDS_CALIBRATION_CMD:
+ sendToDevice(new WithingsMessage(WithingsMessageType.START_HANDS_CALIBRATION));
+ break;
+ case STOP_HANDS_CALIBRATION_CMD:
+ sendToDevice(new WithingsMessage(WithingsMessageType.STOP_HANDS_CALIBRATION));
+ break;
+ }
+ }
+ };
+
+ LocalBroadcastManager.getInstance(GBApplication.getContext()).registerReceiver(commandReceiver, commandFilter);
+ }
+
+ @Override
+ public boolean getSendWriteRequestResponse() {
+ return true;
+ }
+
+
+ @Override
+ protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
+ logger.debug("Starting initialization...");
+ conversationQueue = new ConversationQueue(this);
+ builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
+ getDevice().setFirmwareVersion("N/A");
+ getDevice().setFirmwareVersion2("N/A");
+ BluetoothGattCharacteristic characteristic = getCharacteristic(WithingsUUID.WITHINGS_WRITE_CHARACTERISTIC_UUID);
+ builder.notify(characteristic, true);
+ logger.debug("Requesting change of MTU...");
+ builder.requestMtu(119);
+ return builder;
+ }
+
+ @Override
+ public boolean connectFirstTime() {
+ firstTimeConnect = true;
+ return connect();
+ }
+
+ @Override
+ public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
+ logger.debug("MTU has changed to " + mtu);
+ mtuSize = mtu;
+ if (firstTimeConnect) {
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.INITIAL_CONNECT));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_LOCALE, new Locale("de")));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.START_HANDS_CALIBRATION));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.STOP_HANDS_CALIBRATION));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_TIME, new Time()));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_USER_UNIT, new UserUnit(UserUnitConstants.DISTANCE, getUnit())));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_USER_UNIT, new UserUnit(UserUnitConstants.CLOCK_MODE, getTimeMode())));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ACTIVITY_TARGET, new ActivityTarget(activityUser.getStepsGoal())));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ANCS_STATUS));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ANCS_STATUS, new AncsStatus(true)));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_BATTERY_STATUS), new BatteryStateHandler(this));
+ addScreenListCommands();
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SETUP_FINISHED), new SetupFinishedHandler(this));
+ } else {
+ Message message = new WithingsMessage(WithingsMessageType.PROBE);
+ message.addDataStructure(new Probe((short) 1, (short) 1, 5100401));
+ message.addDataStructure(new ProbeOsVersion((short) Build.VERSION.SDK_INT));
+ conversationQueue.clear();
+ addSimpleConversationToQueue(message, new AuthenticationHandler(this));
+ }
+
+ if (!firstTimeConnect) {
+ finishInitialization();
+ }
+ conversationQueue.send();
+ }
+
+ public void doSync() {
+ activitySampleHandler = new ActivitySampleHandler(this);
+ conversationQueue.clear();
+ try {
+ if (syncInProgress || !shoudSync()) {
+ return;
+ }
+
+ getDevice().setBusyTask("Syncing");
+ syncInProgress = true;
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.INITIAL_CONNECT));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ANCS_STATUS));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_BATTERY_STATUS), new BatteryStateHandler(this));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_TIME, new Time()));
+ WithingsMessage message = new WithingsMessage(WithingsMessageType.SET_USER);
+ message.addDataStructure(getUser());
+ // The UserSecret appears in the original communication with the HealthMate app. Until now GB works without the secret.
+ // This makes the "authentication" far easier. However if it turns out that this is needed, we would need to find a way to savely store a unique generated secret.
+ // message.addDataStructure(new UserSecret());
+ addSimpleConversationToQueue(message);
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ACTIVITY_TARGET, new ActivityTarget(activityUser.getStepsGoal())));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_USER_UNIT, new UserUnit(UserUnitConstants.DISTANCE, getUnit())));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_USER_UNIT, new UserUnit(UserUnitConstants.CLOCK_MODE, getTimeMode())));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ALARM_SETTINGS));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_SCREEN_SETTINGS));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ALARM));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ALARM_ENABLED));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_WORKOUT_SCREEN_LIST), new WorkoutScreenListHandler(this));
+ Calendar c = Calendar.getInstance();
+ c.setTimeInMillis(getLastSyncTimestamp());
+ message = new WithingsMessage(WithingsMessageType.GET_ACTIVITY_SAMPLES, ExpectedResponse.EOT);
+ message.addDataStructure(new GetActivitySamples(c.getTimeInMillis() / 1000, (short) 0));
+ addSimpleConversationToQueue(message, activitySampleHandler);
+ message = new WithingsMessage(WithingsMessageType.GET_MOVEMENT_SAMPLES, ExpectedResponse.EOT);
+ message.addDataStructure(new GetActivitySamples(c.getTimeInMillis() / 1000, (short) 0));
+ message.addDataStructure(new TypeVersion());
+ addSimpleConversationToQueue(message, activitySampleHandler);
+ message = new WithingsMessage(WithingsMessageType.GET_HEARTRATE_SAMPLES, ExpectedResponse.EOT);
+ message.addDataStructure(new GetActivitySamples(c.getTimeInMillis() / 1000, (short) 0));
+ message.addDataStructure(new TypeVersion());
+ addSimpleConversationToQueue(message, activitySampleHandler);
+ } catch (Exception e) {
+ logger.error("Could not synchronize! ", e);
+ conversationQueue.clear();
+ } finally {
+ // This must be done in all cases or the watch won't respond anymore!
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SYNC_OK), new SyncFinishedHandler(this));
+ }
+ conversationQueue.send();
+ }
+
+
+ @Override
+ public boolean onCharacteristicChanged(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic) {
+ if (super.onCharacteristicChanged(gatt, characteristic)) {
+ return true;
+ }
+
+ byte[] data = characteristic.getValue();
+
+ boolean complete = messageBuilder.buildMessage(data);
+ if (complete) {
+ Message message = messageBuilder.getMessage();
+ if (message.isIncomingMessage()) {
+ logger.debug("received incoming message: " + message.getType());
+ IncomingMessageHandler handler = incomingMessageHandlerFactory.getHandler(message);
+ handler.handleMessage(message);
+ } else {
+ conversationQueue.processResponse(message);
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onSetCallState(CallSpec callSpec) {
+ if (callSpec.command == CallSpec.CALL_INCOMING) {
+ NotificationSpec notificationSpec = new NotificationSpec();
+ notificationSpec.sourceAppId = "incoming.call";
+ notificationSpec.title = callSpec.number;
+ notificationSpec.sender = callSpec.name;
+ notificationSpec.type = NotificationType.GENERIC_PHONE;
+ notificationProvider.notifyClient(notificationSpec);
+ } else {
+ logger.info("Received yet unhandled call command: " + callSpec.command);
+ }
+ }
+
+ @Override
+ public void onNotification(NotificationSpec notificationSpec) {
+ notificationProvider.notifyClient(notificationSpec);
+ }
+
+ @Override
+ public void onSetAlarms(ArrayList extends Alarm> alarms) {
+ if (alarms.size() > 3) {
+ throw new IllegalArgumentException("Steel HR does only have three alarmslots!");
+ }
+
+ if (alarms.size() == 0) {
+ return;
+ }
+
+ boolean noAlarmsEnabled = true;
+ conversationQueue.clear();
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ALARM));
+ for (Alarm alarm : alarms) {
+ if (alarm.getEnabled() && !alarm.getUnused()) {
+ noAlarmsEnabled = false;
+ addAlarm(alarm);
+ }
+ }
+
+ if (noAlarmsEnabled) {
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ALARM_ENABLED, new AlarmStatus(false)));
+ }
+
+ conversationQueue.send();
+ }
+
+ @Override
+ public boolean onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
+ this.device = device;
+ return true;
+ }
+
+ @Override
+ public boolean onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
+ if (characteristic.getUuid().equals(WithingsUUID.CONTROL_POINT_CHARACTERISTIC_UUID)) {
+ logger.debug("Got GetNotificationAttributesRequest: " + GB.hexdump(value));
+ GetNotificationAttributes request = new GetNotificationAttributes();
+ request.deserialize(value);
+ notificationProvider.handleNotificationAttributeRequest(request);
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onFetchRecordedData(int dataTypes) {
+ doSync();
+ }
+
+ @Override
+ public void onHeartRateTest() {
+ conversationQueue.clear();
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_HR), new HeartRateHandler(this));
+ conversationQueue.send();
+ }
+
+ @Override
+ public void onSendConfiguration(String config) {
+ try {
+ switch (config) {
+ case HuamiConst.PREF_WORKOUT_ACTIVITY_TYPES_SORTABLE:
+ setWorkoutActivityTypes();
+ break;
+ default:
+ logger.debug("unknown configuration setting received: " + config);
+ }
+ } catch (Exception e) {
+ GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR, e);
+ }
+ }
+
+ @Override
+ public void onTestNewFunction() {
+ String hexMessage = "0105080015050900111006040102030507000000000000000000";
+ conversationQueue.clear();
+ addSimpleConversationToQueue(new SimpleHexToByteMessage(hexMessage));
+ conversationQueue.send();
+ }
+
+ @Override
+ public boolean useAutoConnect() {
+ return false;
+ }
+
+ public void sendToDevice(Message message) {
+ if (message == null) {
+ return;
+ }
+
+ try {
+ TransactionBuilder builder = createTransactionBuilder("conversation");
+ builder.setCallback(this);
+ BluetoothGattCharacteristic characteristic = getCharacteristic(WithingsUUID.WITHINGS_WRITE_CHARACTERISTIC_UUID);
+ if (characteristic == null) {
+ logger.info("Characteristic with UUID " + WithingsUUID.WITHINGS_WRITE_CHARACTERISTIC_UUID + " not found.");
+ return;
+ }
+
+ byte[] rawData = message.getRawData();
+ builder.writeChunkedData(characteristic, rawData, mtuSize - 4);
+ builder.queue(getQueue());
+ } catch (Exception e) {
+ logger.warn("Could not send message because of " + e.getMessage());
+ }
+ }
+
+ public void sendAncsNotificationSourceNotification(NotificationSource notificationSource) {
+ try {
+ ServerTransactionBuilder builder = performServer("notificationSourceNotification");
+ notificationSourceCharacteristic.setValue(notificationSource.serialize());
+ builder.add(new WithingsServerAction(device, notificationSourceCharacteristic));
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ logger.error("Could not send notification.", e);
+ GB.toast("Could not send notification.", Toast.LENGTH_LONG, GB.ERROR, e);
+ }
+ }
+
+ public void sendAncsDataSourceNotification(GetNotificationAttributesResponse response) {
+ try {
+ ServerTransactionBuilder builder = performServer("dataSourceNotification");
+ byte[] data = response.serialize();
+ dataSourceCharacteristic.setValue(response.serialize());
+ builder.add(new WithingsServerAction(device, dataSourceCharacteristic));
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ logger.error("Could not send notification.", e);
+ GB.toast("Could not send notification.", Toast.LENGTH_LONG, GB.ERROR, e);
+ }
+ }
+
+ public void finishInitialization() {
+ TransactionBuilder builder = createTransactionBuilder("setupFinished");
+ builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
+ builder.queue(getQueue());
+ logger.debug("Finished initialization.");
+ }
+
+ public void finishSync() {
+ syncInProgress = false;
+ if (getDevice().isBusy()) {
+ getDevice().unsetBusyTask();
+ getDevice().sendDeviceUpdateIntent(getContext());
+ }
+ activitySampleHandler.onSyncFinished();
+ saveLastSyncTimestamp(new Date().getTime());
+ }
+
+ void onAuthenticationFinished() {
+ if (!firstTimeConnect) {
+ addScreenListCommands();
+ doSync();
+ } else {
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ANCS_STATUS, new AncsStatus(true)));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ANCS_STATUS));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_BATTERY_STATUS), new BatteryStateHandler(this));
+ conversationQueue.send();
+ }
+ }
+
+ private void addAlarm(Alarm alarm) {
+ AlarmSettings alarmSettings = new AlarmSettings();
+ alarmSettings.setHour((short) alarm.getHour());
+ alarmSettings.setMinute((short) alarm.getMinute());
+ alarmSettings.setDayOfWeek(mapRepetitionToWithingsValue(alarm));
+ if (alarm.getSmartWakeup()) {
+ // Healthmate has the possibility to change the minutecount, in GB we use a fixed value of 15
+ alarmSettings.setSmartWakeupMinutes((short) 15);
+ }
+
+ Message alarmMessage = new WithingsMessage(WithingsMessageType.SET_ALARM, alarmSettings);
+ if (!StringUtils.isEmpty(alarm.getTitle())) {
+ AlarmName alarmName = new AlarmName(alarm.getTitle());
+ alarmMessage.addDataStructure(alarmName);
+ }
+
+ addSimpleConversationToQueue(alarmMessage);
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ALARM_ENABLED, new AlarmStatus(true)));
+ }
+
+ private short mapRepetitionToWithingsValue(Alarm alarm) {
+ int repetition = 0;
+ if (alarm.getRepetition(Alarm.ALARM_MON)) {
+ repetition += 0x02;
+ }
+ if (alarm.getRepetition(Alarm.ALARM_TUE)) {
+ repetition += 0x04;
+ }
+ if (alarm.getRepetition(Alarm.ALARM_WED)) {
+ repetition += 0x08;
+ }
+ if (alarm.getRepetition(Alarm.ALARM_THU)) {
+ repetition += 0x10;
+ }
+ if (alarm.getRepetition(Alarm.ALARM_FRI)) {
+ repetition += 0x20;
+ }
+ if (alarm.getRepetition(Alarm.ALARM_SAT)) {
+ repetition += 0x40;
+ }
+ if (alarm.getRepetition(Alarm.ALARM_SUN)) {
+ repetition += 0x01;
+ }
+
+ return (short)(repetition + 0x80);
+ }
+
+ private void addANCSService() {
+ BluetoothGattService withingsGATTService = new BluetoothGattService(WithingsUUID.WITHINGS_ANCS_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY);
+ notificationSourceCharacteristic = new BluetoothGattCharacteristic(WithingsUUID.NOTIFICATION_SOURCE_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ);
+ notificationSourceCharacteristic.addDescriptor(new BluetoothGattDescriptor(WithingsUUID.CCC_DESCRIPTOR_UUID, BluetoothGattCharacteristic.PERMISSION_WRITE));
+ withingsGATTService.addCharacteristic(notificationSourceCharacteristic);
+ withingsGATTService.addCharacteristic(new BluetoothGattCharacteristic(WithingsUUID.CONTROL_POINT_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE));
+ dataSourceCharacteristic = new BluetoothGattCharacteristic(WithingsUUID.DATA_SOURCE_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ);
+ dataSourceCharacteristic.addDescriptor(new BluetoothGattDescriptor(WithingsUUID.CCC_DESCRIPTOR_UUID, BluetoothGattCharacteristic.PERMISSION_WRITE));
+ withingsGATTService.addCharacteristic(dataSourceCharacteristic);
+ addSupportedServerService(withingsGATTService);
+ }
+
+ private void addSimpleConversationToQueue(Message message) {
+ addSimpleConversationToQueue(message, null);
+ }
+
+ private void addSimpleConversationToQueue(Message message, ResponseHandler handler) {
+ Conversation conversation = new SimpleConversation(handler);
+ conversation.setRequest(message);
+ conversationQueue.addConversation(conversation);
+ }
+
+ private void saveLastSyncTimestamp(@NonNull long timestamp) {
+ SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).edit();
+ editor.putLong(LAST_ACTIVITY_SYNC, timestamp);
+ editor.apply();
+ }
+
+ private long getLastSyncTimestamp() {
+ SharedPreferences settings = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
+ long lastSyncTime = settings.getLong(LAST_ACTIVITY_SYNC, 0);
+ if (lastSyncTime > 0) {
+ return lastSyncTime;
+ } else {
+ Date currentDate = new Date();
+ Calendar c = Calendar.getInstance();
+ c.setTimeInMillis(currentDate.getTime());
+ c.add(Calendar.HOUR, - 10);
+ return c.getTimeInMillis();
+ }
+ }
+
+ private boolean shoudSync() {
+ long lastSynced = getLastSyncTimestamp();
+ int minuteInMillis = 60 * 1000;
+ return new Date().getTime() - lastSynced > minuteInMillis;
+ }
+
+ private User getUser() {
+ User user = new User();
+ ActivityUser activityUser = new ActivityUser();
+ user.setName(activityUser.getName());
+ user.setGender((byte) activityUser.getGender());
+ user.setHeight(activityUser.getHeightCm());
+ user.setWeight(activityUser.getWeightKg());
+ user.setBirthdate(activityUser.getUserBirthday());
+ return user;
+ }
+
+ private void addScreenListCommands() {
+ // TODO: this needs to be more reworked, at the moment for example the notification screen is always on and this is full of magic numbers that need to be identified properly:
+ Message message = new WithingsMessage(WithingsMessageType.SET_SCREEN_LIST);
+ ScreenSettings settings = new ScreenSettings();
+ settings.setId(0xff);
+ settings.setIdOnDevice((byte)6);
+ message.addDataStructure(settings);
+
+ settings = new ScreenSettings();
+ settings.setId(0x3d);
+ settings.setIdOnDevice((byte)1);
+ message.addDataStructure(settings);
+
+ settings = new ScreenSettings();
+ settings.setId(0x33);
+ settings.setIdOnDevice((byte)4);
+ message.addDataStructure(settings);
+
+ settings = new ScreenSettings();
+ settings.setId(0x2d);
+ settings.setIdOnDevice((byte)2);
+ message.addDataStructure(settings);
+
+ settings = new ScreenSettings();
+ settings.setId(0x2a);
+ settings.setIdOnDevice((byte)3);
+ message.addDataStructure(settings);
+
+ settings = new ScreenSettings();
+ settings.setId(0x26);
+ settings.setIdOnDevice((byte)7);
+ message.addDataStructure(settings);
+
+ settings = new ScreenSettings();
+ settings.setId(0x39);
+ settings.setIdOnDevice((byte)9);
+ message.addDataStructure(settings);
+
+ message.addDataStructure(new EndOfTransmission());
+ addSimpleConversationToQueue(message);
+ }
+
+ private void setWorkoutActivityTypes() {
+ final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress());
+
+ final List allActivityTypes = Arrays.asList(getContext().getResources().getStringArray(R.array.pref_withings_steel_activity_types_values));
+ final List defaultActivityTypes = Arrays.asList(getContext().getResources().getStringArray(R.array.pref_withings_steel_activity_types_default));
+ final String activityTypesPref = prefs.getString("workout_activity_types_sortable", null);
+
+ final List enabledActivityTypes;
+ if (activityTypesPref == null || activityTypesPref.equals("")) {
+ enabledActivityTypes = defaultActivityTypes;
+ } else {
+ enabledActivityTypes = Arrays.asList(activityTypesPref.split(","));
+ }
+
+ conversationQueue.clear();
+ for (int i = 0; i < enabledActivityTypes.size(); i++) {
+ String workoutType = enabledActivityTypes.get(i);
+ try {
+ Message message = createWorkoutScreenMessage(workoutType);
+ if (i == enabledActivityTypes.size() - 1) {
+ message.addDataStructure(new EndOfTransmission());
+ }
+ addSimpleConversationToQueue(message);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ conversationQueue.send();
+ }
+
+ @NonNull
+ private Message createWorkoutScreenMessage(String workoutType) {
+ WithingsActivityType withingsActivityType = WithingsActivityType.fromPrefValue(workoutType);
+ int code = withingsActivityType.getCode();
+ Message message = new WithingsMessage(WithingsMessageType.SET_WORKOUT_SCREEN, ExpectedResponse.NONE);
+ WorkoutScreen workoutScreen = new WorkoutScreen();
+ workoutScreen.setId(code);
+ final int stringId = getContext().getResources().getIdentifier("activity_type_" + workoutType, "string", getContext().getPackageName());
+ workoutScreen.setName(getContext().getString(stringId));
+ message.addDataStructure(workoutScreen);
+
+ ImageMetaData imageMetaData = new ImageMetaData();
+ imageMetaData.setHeight((byte)24);
+ imageMetaData.setWidth((byte)22);
+ message.addDataStructure(imageMetaData);
+
+ ImageData imageData = new ImageData();
+ final int drawableId = ActivityKind.getIconId(withingsActivityType.toActivityKind());
+ Drawable drawable = getContext().getDrawable(drawableId);
+ imageData.setImageData(IconHelper.getIconBytesFromDrawable(drawable));
+ message.addDataStructure(imageData);
+
+ return message;
+ }
+
+ private short getTimeMode() {
+ GBPrefs gbPrefs = new GBPrefs(new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress())));
+ String tmode = gbPrefs.getTimeFormat();
+
+ if ("24h".equals(tmode)) {
+ return UserUnitConstants.UNIT_24H;
+ } else {
+ return UserUnitConstants.UNIT_12H;
+ }
+ }
+
+ private short getUnit() {
+ String units = prefs.getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric));
+
+ if (units.equals(GBApplication.getContext().getString(R.string.p_unit_metric))) {
+ return UserUnitConstants.UNIT_KM;
+ } else {
+ return UserUnitConstants.UNIT_MILES;
+ }
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/ActivityEntry.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/ActivityEntry.java
new file mode 100644
index 000000000..0b8e99638
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/ActivityEntry.java
@@ -0,0 +1,101 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity;
+
+public class ActivityEntry {
+ private int timestamp;
+ private int duration;
+ private int rawKind = -1;
+ private int heartrate;
+ private int steps;
+ private int calories;
+ private int distance;
+ private int rawIntensity;
+ private boolean isHeartrate;
+
+ public int getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(int timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public int getDuration() {
+ return duration;
+ }
+
+ public void setDuration(int duration) {
+ this.duration = duration;
+ }
+
+ public int getHeartrate() {
+ return heartrate;
+ }
+
+ public void setIsHeartrate(int heartrate) {
+ this.heartrate = heartrate;
+ }
+
+ public int getRawKind() {
+ return rawKind;
+ }
+
+ public void setRawKind(int rawKind) {
+ this.rawKind = rawKind;
+ }
+
+ public int getSteps() {
+ return steps;
+ }
+
+ public void setSteps(int steps) {
+ this.steps = steps;
+ }
+
+ public int getCalories() {
+ return calories;
+ }
+
+ public void setCalories(int calories) {
+ this.calories = calories;
+ }
+
+ public int getDistance() {
+ return distance;
+ }
+
+ public void setDistance(int distance) {
+ this.distance = distance;
+ }
+
+ public int getRawIntensity() {
+ return rawIntensity;
+ }
+
+ public void setRawIntensity(int rawIntensity) {
+ this.rawIntensity = rawIntensity;
+ }
+
+ public boolean isHeartrate() {
+ return isHeartrate;
+ }
+
+ public void setIsHeartrate(boolean heartrate) {
+ isHeartrate = heartrate;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/SleepActivitySampleHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/SleepActivitySampleHelper.java
new file mode 100644
index 000000000..706f5e12b
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/SleepActivitySampleHelper.java
@@ -0,0 +1,93 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsSteelHRSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+
+/**
+ * This class is needed for sleep tracking as the withings steel HR sends heartrate while sleeping in an extra activity.
+ * This leads to breaking the sleep session in the sleep calculation of GB.
+ */
+public class SleepActivitySampleHelper {
+
+ private static Logger logger = LoggerFactory.getLogger(SleepActivitySampleHelper.class);
+ private static int mergeCount;
+
+ public static WithingsSteelHRActivitySample mergeIfNecessary(WithingsSteelHRSampleProvider provider, WithingsSteelHRActivitySample sample) {
+ if (!shouldMerge(sample)) {
+ return sample;
+ }
+
+ WithingsSteelHRActivitySample overlappingSample = getOverlappingSample(provider, (int)sample.getTimestamp());
+ if (overlappingSample != null) {
+ sample = doMerge(overlappingSample, sample);
+ }
+
+ return sample;
+ }
+
+ private static WithingsSteelHRActivitySample getOverlappingSample(WithingsSteelHRSampleProvider provider, long timestamp) {
+ List samples = provider.getActivitySamples((int)timestamp - 500, (int)timestamp);
+ if (samples.isEmpty()) {
+ return null;
+ }
+
+ for (int i = samples.size()-1; i >= 0; i--) {
+ WithingsSteelHRActivitySample lastSample = samples.get(i);
+ if (isNotHeartRateOnly(lastSample, (int) timestamp)) {
+ return lastSample;
+ }
+ }
+
+ return null;
+ }
+
+ private static boolean isNotHeartRateOnly(WithingsSteelHRActivitySample lastSample, int timestamp) {
+ return lastSample.getRawKind() != ActivityKind.TYPE_NOT_MEASURED; // && lastSample.getTimestamp() <= timestamp && (lastSample.getTimestamp() + lastSample.getDuration()) >= timestamp);
+ }
+
+ private static boolean shouldMerge(WithingsSteelHRActivitySample sample) {
+ return sample.getSteps() == 0
+ && sample.getDistance() == 0
+ && sample.getRawKind() == -1
+ && sample.getCalories() == 0
+ && sample.getHeartRate() > 1
+ && sample.getRawIntensity() == 0;
+ }
+
+ private static WithingsSteelHRActivitySample doMerge(WithingsSteelHRActivitySample origin, WithingsSteelHRActivitySample update) {
+ WithingsSteelHRActivitySample mergeResult = new WithingsSteelHRActivitySample();
+ mergeResult.setTimestamp(update.getTimestamp());
+ mergeResult.setRawKind(origin.getRawKind());
+ mergeResult.setRawIntensity(origin.getRawIntensity());
+ mergeResult.setDuration(origin.getDuration() - (update.getTimestamp() - origin.getTimestamp()));
+ mergeResult.setDevice(origin.getDevice());
+ mergeResult.setDeviceId(origin.getDeviceId());
+ mergeResult.setUser(origin.getUser());
+ mergeResult.setUserId(origin.getUserId());
+ mergeResult.setProvider(origin.getProvider());
+ mergeResult.setHeartRate(update.getHeartRate());
+ return mergeResult;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/WithingsActivityType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/WithingsActivityType.java
new file mode 100644
index 000000000..f6ce0770b
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/WithingsActivityType.java
@@ -0,0 +1,168 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity;
+
+import java.util.Locale;
+
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiWorkoutScreenActivityType;
+
+public enum WithingsActivityType {
+
+ WALKING(1),
+ RUNNING(2),
+ HIKING(3),
+ BIKING(6),
+ SWIMMING(7),
+ SURFING(8),
+ KITESURFING(9),
+ WINDSURFING(10),
+ TENNIS(12),
+ PINGPONG(13),
+ SQUASH(14),
+ BADMINTON(15),
+ WEIGHTLIFTING(16),
+ GYMNASTICS(17),
+ ELLIPTICAL(18),
+ PILATES(19),
+ BASKETBALL(20),
+ SOCCER(21),
+ FOOTBALL(22),
+ RUGBY(23),
+ VOLLEYBALL(24),
+ GOLFING(227),
+ YOGA(28),
+ DANCING(29),
+ BOXING(30),
+ SKIING(34),
+ SNOWBOARDING(35),
+ ROWING(0), // The code has yet to be identified.
+ ZUMBA(188),
+ BASEBALL(191),
+ HANDBALL(192),
+ HOCKEY(193),
+ ICEHOCKEY(194),
+ CLIMBING(195),
+ ICESKATING(196),
+ RIDING(26),
+ OTHER(36);
+
+ private int code;
+
+ WithingsActivityType(int typeCode) {
+ this.code = typeCode;
+ }
+
+ public static WithingsActivityType fromCode(int withingsCode) {
+ for (WithingsActivityType type : values()) {
+ if (type.code == withingsCode) {
+ return type;
+ }
+ }
+ throw new RuntimeException("No matching WithingsActivityType for code: " + withingsCode);
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public int toActivityKind() {
+ switch (this) {
+ case WALKING:
+ return ActivityKind.TYPE_WALKING;
+ case RUNNING:
+ return ActivityKind.TYPE_RUNNING;
+ case HIKING:
+ return ActivityKind.TYPE_HIKING;
+ case BIKING:
+ return ActivityKind.TYPE_CYCLING;
+ case SWIMMING:
+ return ActivityKind.TYPE_SWIMMING;
+ case SURFING:
+ return ActivityKind.TYPE_ACTIVITY;
+ case KITESURFING:
+ return ActivityKind.TYPE_ACTIVITY;
+ case WINDSURFING:
+ return ActivityKind.TYPE_ACTIVITY;
+ case TENNIS:
+ return ActivityKind.TYPE_ACTIVITY;
+ case PINGPONG:
+ return ActivityKind.TYPE_PINGPONG;
+ case SQUASH:
+ return ActivityKind.TYPE_ACTIVITY;
+ case BADMINTON:
+ return ActivityKind.TYPE_BADMINTON;
+ case WEIGHTLIFTING:
+ return ActivityKind.TYPE_ACTIVITY;
+ case GYMNASTICS:
+ return ActivityKind.TYPE_EXERCISE;
+ case ELLIPTICAL:
+ return ActivityKind.TYPE_ELLIPTICAL_TRAINER;
+ case PILATES:
+ return ActivityKind.TYPE_YOGA;
+ case BASKETBALL:
+ return ActivityKind.TYPE_BASKETBALL;
+ case SOCCER:
+ return ActivityKind.TYPE_SOCCER;
+ case FOOTBALL:
+ return ActivityKind.TYPE_ACTIVITY;
+ case RUGBY:
+ return ActivityKind.TYPE_ACTIVITY;
+ case VOLLEYBALL:
+ return ActivityKind.TYPE_ACTIVITY;
+ case GOLFING:
+ return ActivityKind.TYPE_ACTIVITY;
+ case YOGA:
+ return ActivityKind.TYPE_YOGA;
+ case DANCING:
+ return ActivityKind.TYPE_ACTIVITY;
+ case BOXING:
+ return ActivityKind.TYPE_ACTIVITY;
+ case SKIING:
+ return ActivityKind.TYPE_ACTIVITY;
+ case SNOWBOARDING:
+ return ActivityKind.TYPE_ACTIVITY;
+ case ROWING:
+ return ActivityKind.TYPE_ROWING_MACHINE;
+ case ZUMBA:
+ return ActivityKind.TYPE_ACTIVITY;
+ case BASEBALL:
+ return ActivityKind.TYPE_CRICKET;
+ case HANDBALL:
+ return ActivityKind.TYPE_ACTIVITY;
+ case HOCKEY:
+ return ActivityKind.TYPE_ACTIVITY;
+ case ICEHOCKEY:
+ return ActivityKind.TYPE_ACTIVITY;
+ case CLIMBING:
+ return ActivityKind.TYPE_CLIMBING;
+ case ICESKATING:
+ return ActivityKind.TYPE_ACTIVITY;
+ default:
+ return ActivityKind.TYPE_UNKNOWN;
+ }
+ }
+
+ public static WithingsActivityType fromPrefValue(final String prefValue) {
+ for (final WithingsActivityType type : values()) {
+ if (type.name().toLowerCase(Locale.ROOT).equals(prefValue.replace("_", "").toLowerCase(Locale.ROOT))) {
+ return type;
+ }
+ }
+ throw new RuntimeException("No matching WithingsActivityType for pref value: " + prefValue);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/WithingsServerAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/WithingsServerAction.java
new file mode 100644
index 000000000..438a71a15
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/WithingsServerAction.java
@@ -0,0 +1,43 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattServer;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEServerAction;
+
+public class WithingsServerAction extends BtLEServerAction
+{
+ private BluetoothGattCharacteristic characteristic;
+
+ public WithingsServerAction(BluetoothDevice device, BluetoothGattCharacteristic characteristic) {
+ super(device);
+ this.characteristic = characteristic;
+ }
+
+ @Override
+ public boolean expectsResult() {
+ return false;
+ }
+
+ @Override
+ public boolean run(BluetoothGattServer server) {
+ return server.notifyCharacteristicChanged(getDevice(), characteristic, false);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/WithingsUUID.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/WithingsUUID.java
new file mode 100644
index 000000000..064f90fcb
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/WithingsUUID.java
@@ -0,0 +1,34 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication;
+
+import java.util.UUID;
+
+public final class WithingsUUID {
+
+ public static final UUID WITHINGS_SERVICE_UUID = UUID.fromString("00000020-5749-5448-0037-000000000000");
+ public static final UUID WITHINGS_WRITE_CHARACTERISTIC_UUID = UUID.fromString("00000024-5749-5448-0037-000000000000");
+ public static final UUID WITHINGS_APP_CHARACTERISTIC_UUID = UUID.fromString("10000059-5749-5448-0037-000000000000");
+ public static final UUID WITHINGS_APP_CHARACTERISTIC2_UUID = UUID.fromString("10000028-5749-5448-0037-000000000000");
+ public static final UUID CCC_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
+ public static final UUID WITHINGS_ANCS_SERVICE_UUID = UUID.fromString("10000057-5749-5448-0037-00000000000000");
+ public static final UUID NOTIFICATION_SOURCE_CHARACTERISTIC_UUID = UUID.fromString("10000059-5749-5448-0037-00000000000000");
+ public static final UUID CONTROL_POINT_CHARACTERISTIC_UUID = UUID.fromString("10000058-5749-5448-0037-00000000000000");
+ public static final UUID DATA_SOURCE_CHARACTERISTIC_UUID = UUID.fromString("1000005a-5749-5448-0037-00000000000000");
+
+ private WithingsUUID() {}
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractConversation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractConversation.java
new file mode 100644
index 000000000..9c3231a7d
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractConversation.java
@@ -0,0 +1,107 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructureType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+
+public abstract class AbstractConversation implements Conversation {
+
+ private List observers = new ArrayList();
+
+ private boolean complete;
+
+ protected Message request;
+
+ private short requestType;
+
+ protected ResponseHandler responseHandler;
+
+ public AbstractConversation(ResponseHandler responseHandler) {
+ this.responseHandler = responseHandler;
+ }
+
+ @Override
+ public void registerObserver(ConversationObserver observer) {
+ observers.add(observer);
+ }
+
+ @Override
+ public void removeObserver(ConversationObserver observer) {
+ observers.remove(observer);
+ }
+
+ @Override
+ public void setRequest(Message message) {
+ this.request = message;
+ this.requestType = message.getType();
+ }
+
+ @Override
+ public Message getRequest() {
+ return request;
+ }
+
+ @Override
+ public void handleResponse(Message response) {
+ if (response.getType() == requestType) {
+ if (request.needsResponse()) {
+ complete = true;
+ } else if (request.needsEOT()) {
+ complete = hasEOT(response);
+ }
+
+ doHandleResponse(response);
+ if (complete) {
+ notifyObservers(requestType);
+ }
+ }
+ }
+
+ @Override
+ public boolean isComplete() {
+ return complete;
+ }
+
+ protected void notifyObservers(short messageType) {
+ for (ConversationObserver observer : observers) {
+ observer.onConversationCompleted(messageType);
+ }
+ }
+
+ private boolean hasEOT(Message message) {
+ List dataList = message.getDataStructures();
+ if (dataList != null) {
+ for (WithingsStructure strucuter :
+ dataList) {
+ if (strucuter.getType() == WithingsStructureType.END_OF_TRANSMISSION) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ protected abstract void doSendRequest(Message message);
+
+ protected abstract void doHandleResponse(Message message);
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractResponseHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractResponseHandler.java
new file mode 100644
index 000000000..0549afc21
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractResponseHandler.java
@@ -0,0 +1,30 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
+
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+
+public abstract class AbstractResponseHandler implements ResponseHandler {
+ protected GBDevice device;
+ protected WithingsSteelHRDeviceSupport support;
+
+ public AbstractResponseHandler(WithingsSteelHRDeviceSupport support) {
+ this.support = support;
+ this.device = support.getDevice();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ActivitySampleHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ActivitySampleHandler.java
new file mode 100644
index 000000000..4bce8cdef
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ActivitySampleHandler.java
@@ -0,0 +1,269 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsSteelHRSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.ActivityEntry;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.WithingsActivityType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WorkoutType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleCalories;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleCalories2;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleDuration;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleMovement;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleSleep;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleTime;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivityHeartrate;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructureType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class ActivitySampleHandler extends AbstractResponseHandler {
+
+ private static final Logger logger = LoggerFactory.getLogger(ActivitySampleHandler.class);
+ private ActivityEntry activityEntry;
+ private List activityEntries = new ArrayList<>();
+ private List heartrateEntries = new ArrayList<>();
+
+ public ActivitySampleHandler(WithingsSteelHRDeviceSupport support) {
+ super(support);
+ }
+
+ @Override
+ public void handleResponse(Message response) {
+ List data = response.getDataStructures();
+ if (data != null) {
+ handleActivityData(data, response.getType());
+ }
+ }
+
+ public void onSyncFinished() {
+ mergeHeartrateSamplesIntoActivitySammples();
+ saveData();
+ }
+
+ private void handleActivityData(List dataList, short activityType) {
+ for (WithingsStructure data : dataList) {
+ switch (data.getType()) {
+ case WithingsStructureType.ACTIVITY_SAMPLE_TIME:
+ handleTimestamp(data, activityType);
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_DURATION:
+ handleDuration(data);
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_MOVEMENT:
+ handleMovement(data);
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_CALORIES:
+ handleCalories1(data);
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_CALORIES_2:
+ handleCalories2(data);
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_SLEEP:
+ handleSleep(data);
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_WALK:
+ handleWalk(data);
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_RUN:
+ handleRun(data);
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_SWIM:
+ handleSwim(data);
+ break;
+ case WithingsStructureType.ACTIVITY_HR:
+ handleHeartrate(data);
+ break;
+ case WithingsStructureType.WORKOUT_TYPE:
+ handleWorkoutType(data);
+ break;
+ default:
+ logger.info("Received yet unhandled activity data of type '" + data.getType() + "' with data '" + GB.hexdump(data.getRawData()) + "'.");
+ }
+ }
+
+ if (activityEntry != null) {
+ addToList(activityEntry);
+ }
+
+ }
+
+ private void handleTimestamp(WithingsStructure data, short activityType) {
+ if (activityEntry != null) {
+ addToList(activityEntry);
+ }
+
+ activityEntry = new ActivityEntry();
+ activityEntry.setIsHeartrate(activityType == WithingsMessageType.GET_HEARTRATE_SAMPLES);
+ activityEntry.setTimestamp((int)(((ActivitySampleTime)data).getDate().getTime()/1000));
+ }
+
+ private void handleWorkoutType(WithingsStructure data) {
+ WithingsActivityType activityType = WithingsActivityType.fromCode(((WorkoutType)data).getActivityType());
+ activityEntry.setRawKind(activityType.toActivityKind());
+ }
+
+ private void handleDuration(WithingsStructure data) {
+ activityEntry.setDuration(((ActivitySampleDuration)data).getDuration());
+ }
+
+ private void handleHeartrate(WithingsStructure data) {
+ activityEntry.setIsHeartrate(((ActivityHeartrate)data).getHeartrate());
+ }
+
+ private void handleMovement(WithingsStructure data) {
+ activityEntry.setRawKind(ActivityKind.TYPE_UNKNOWN);
+ activityEntry.setSteps(((ActivitySampleMovement)data).getSteps());
+ activityEntry.setDistance(((ActivitySampleMovement)data).getDistance());
+ }
+
+ private void handleWalk(WithingsStructure data) {
+ activityEntry.setRawKind(ActivityKind.TYPE_WALKING);
+ }
+
+ private void handleRun(WithingsStructure data) {
+ activityEntry.setRawKind(ActivityKind.TYPE_RUNNING);
+ }
+
+ private void handleSwim(WithingsStructure data) {
+ activityEntry.setRawKind(ActivityKind.TYPE_SWIMMING);
+ }
+
+ private void handleSleep(WithingsStructure data) {
+ int sleepType;
+ switch (((ActivitySampleSleep)data).getSleepType()) {
+ case 0:
+ sleepType = ActivityKind.TYPE_LIGHT_SLEEP;
+ activityEntry.setRawIntensity(10);
+ break;
+ case 2:
+ sleepType = ActivityKind.TYPE_DEEP_SLEEP;
+ activityEntry.setRawIntensity(70);
+ break;
+ case 3:
+ sleepType = ActivityKind.TYPE_REM_SLEEP;
+ activityEntry.setRawIntensity(80);
+ break;
+ default:
+ sleepType = ActivityKind.TYPE_LIGHT_SLEEP;
+ activityEntry.setRawIntensity(50);
+ }
+
+ activityEntry.setRawKind(sleepType);
+ }
+
+ private void handleCalories1(WithingsStructure data) {
+ activityEntry.setRawIntensity(((ActivitySampleCalories)data).getMet());
+ activityEntry.setCalories(((ActivitySampleCalories)data).getCalories());
+ }
+
+ private void handleCalories2(WithingsStructure data) {
+ activityEntry.setRawIntensity(((ActivitySampleCalories2)data).getMet());
+ activityEntry.setCalories(((ActivitySampleCalories2)data).getCalories());
+
+ }
+
+ private void addToList(ActivityEntry activityEntry) {
+ if (activityEntry.isHeartrate()) {
+ heartrateEntries.add(activityEntry);
+ } else {
+ activityEntries.add(activityEntry);
+ }
+ }
+
+ private void saveData() {
+ List activitySamples = new ArrayList<>();
+ for (ActivityEntry activityEntry : activityEntries) {
+ convertToSampleAndAddToList(activitySamples, activityEntry);
+ }
+ for (ActivityEntry activityEntry : heartrateEntries) {
+ convertToSampleAndAddToList(activitySamples, activityEntry);
+ }
+
+ writeToDB(activitySamples);
+ }
+
+ private void writeToDB(List activitySamples) {
+ try (DBHandler dbHandler = GBApplication.acquireDB()) {
+ Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
+ Long deviceId = DBHelper.getDevice(device, dbHandler.getDaoSession()).getId();
+ WithingsSteelHRSampleProvider provider = new WithingsSteelHRSampleProvider(device, dbHandler.getDaoSession());
+ for (WithingsSteelHRActivitySample sample : activitySamples) {
+ sample.setDeviceId(deviceId);
+ sample.setUserId(userId);
+ }
+ provider.addGBActivitySamples(activitySamples.toArray(new WithingsSteelHRActivitySample[0]));
+ } catch (Exception ex) {
+ logger.warn("Error saving activity data: " + ex.getLocalizedMessage());
+ }
+ }
+
+ private void mergeHeartrateSamplesIntoActivitySammples() {
+ for (ActivityEntry heartrateEntry : heartrateEntries) {
+ for (ActivityEntry activityEntry : activityEntries) {
+ if (doActivitiesOverlap(heartrateEntry, activityEntry)) {
+ updateHeartrateEntry(heartrateEntry, activityEntry);
+ }
+ }
+ }
+ }
+
+ private boolean doActivitiesOverlap(ActivityEntry heartrateEntry, ActivityEntry activityEntry) {
+ return activityEntry.getTimestamp() <= heartrateEntry.getTimestamp()
+ && (activityEntry.getTimestamp() + activityEntry.getDuration()) >= heartrateEntry.getTimestamp();
+ }
+
+ private void updateHeartrateEntry(ActivityEntry heartRateEntry, ActivityEntry activityEntry) {
+ heartRateEntry.setRawKind(activityEntry.getRawKind());
+ heartRateEntry.setRawIntensity(activityEntry.getRawIntensity());
+ heartRateEntry.setDuration(activityEntry.getDuration() - (heartRateEntry.getTimestamp() - activityEntry.getTimestamp()));
+ // If timestamps are exactly the same and only then, the heartrate entry would overwrite the activity entry in the DB, so we set more values.
+ // If we would do so everytime, steps and so on would be multiplicated.
+ if (heartRateEntry.getTimestamp() == activityEntry.getTimestamp()) {
+ heartRateEntry.setSteps(activityEntry.getSteps());
+ heartRateEntry.setDistance(activityEntry.getDistance());
+ heartRateEntry.setCalories(activityEntry.getCalories());
+ }
+ }
+
+ private void convertToSampleAndAddToList(List activitySamples, ActivityEntry activityEntry) {
+ WithingsSteelHRActivitySample sample = new WithingsSteelHRActivitySample();
+ sample.setTimestamp(activityEntry.getTimestamp());
+ sample.setDuration(activityEntry.getDuration());
+ sample.setHeartRate(activityEntry.getHeartrate());
+ sample.setSteps(activityEntry.getSteps());
+ sample.setRawKind(activityEntry.getRawKind());
+ sample.setCalories(activityEntry.getCalories());
+ sample.setDistance(activityEntry.getDistance());
+ sample.setRawIntensity(activityEntry.getRawIntensity());
+ activitySamples.add(sample);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/BatteryStateHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/BatteryStateHandler.java
new file mode 100644
index 000000000..d7d15ca8e
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/BatteryStateHandler.java
@@ -0,0 +1,57 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
+
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.BatteryValues;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+
+public class BatteryStateHandler extends AbstractResponseHandler {
+
+ public BatteryStateHandler(WithingsSteelHRDeviceSupport support) {
+ super(support);
+ }
+
+ @Override
+ public void handleResponse(Message response) {
+ handleBatteryState(response.getStructureByType(BatteryValues.class));
+ }
+
+ private void handleBatteryState(BatteryValues batteryValues) {
+ if (batteryValues == null) {
+ return;
+ }
+
+ GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
+ batteryInfo.level = batteryValues.getPercent();
+ switch (batteryValues.getStatus()) {
+ case 0:
+ batteryInfo.state = BatteryState.BATTERY_CHARGING;
+ break;
+ case 1:
+ batteryInfo.state = BatteryState.BATTERY_LOW;
+ break;
+ default:
+ batteryInfo.state = BatteryState.BATTERY_NORMAL;
+ }
+ batteryInfo.voltage = batteryValues.getVolt();
+ support.evaluateGBDeviceEvent(batteryInfo);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/Conversation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/Conversation.java
new file mode 100644
index 000000000..5cc7f283d
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/Conversation.java
@@ -0,0 +1,28 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+
+public interface Conversation {
+ void registerObserver(ConversationObserver observer);
+ void removeObserver(ConversationObserver observer);
+ void setRequest(Message message);
+ Message getRequest();
+ void handleResponse(Message mesage);
+ boolean isComplete();
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationObserver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationObserver.java
new file mode 100644
index 000000000..73f833d92
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationObserver.java
@@ -0,0 +1,21 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
+
+public interface ConversationObserver {
+ void onConversationCompleted(short conversationType);
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationQueue.java
new file mode 100644
index 000000000..b4e49376c
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationQueue.java
@@ -0,0 +1,100 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
+
+import android.bluetooth.BluetoothGattCharacteristic;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.WithingsUUID;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
+
+public class ConversationQueue implements ConversationObserver
+{
+ private static final Logger logger = LoggerFactory.getLogger(WithingsSteelHRDeviceSupport.class);
+ private final LinkedList queue = new LinkedList<>();
+ private WithingsSteelHRDeviceSupport support;
+
+ public ConversationQueue(WithingsSteelHRDeviceSupport support) {
+ this.support = support;
+ }
+
+ @Override
+ public void onConversationCompleted(short conversationType) {
+ queue.remove(getConversation(conversationType));
+ send();
+ }
+
+ public void clear() {
+ queue.clear();
+ }
+
+ public void send() {
+ logger.debug("Sending of queued messages has been requested.");
+ if (!queue.isEmpty()) {
+ Conversation nextInLine = queue.peek();
+ if (nextInLine!= null) {
+ logger.debug("Sending next queued message.");
+ Message request = nextInLine.getRequest();
+ support.sendToDevice(request);
+ }
+ }
+ }
+
+ public void addConversation(Conversation conversation) {
+ if (conversation == null) {
+ return;
+ }
+
+ if (conversation.getRequest().needsResponse() || conversation.getRequest().needsEOT()) {
+ queue.add(conversation);
+ conversation.registerObserver(this);
+ } else {
+ support.sendToDevice(conversation.getRequest());
+ }
+ }
+
+ public void processResponse(Message response) {
+ Conversation conversation = getConversation(response.getType());
+ if (conversation != null) {
+ conversation.handleResponse(response);
+ }
+ }
+
+ private Conversation getConversation(short requestType) {
+ for (Conversation conversation : queue) {
+ if (conversation.getRequest() != null && conversation.getRequest().getType() == requestType) {
+ return conversation;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/HeartRateHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/HeartRateHandler.java
new file mode 100644
index 000000000..d9f9c664d
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/HeartRateHandler.java
@@ -0,0 +1,87 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
+
+import android.content.Intent;
+
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.GregorianCalendar;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsSteelHRSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.SleepActivitySampleHelper;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.HeartRate;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.LiveHeartRate;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+
+public class HeartRateHandler extends AbstractResponseHandler {
+ private static final Logger logger = LoggerFactory.getLogger(HeartRateHandler.class);
+
+ public HeartRateHandler(WithingsSteelHRDeviceSupport support) {
+ super(support);
+ }
+
+ @Override
+ public void handleResponse(Message response) {
+ if (response.getDataStructures().size() > 0) {
+ handleHeartRateData(response.getDataStructures().get(0));
+ }
+ }
+
+ private void handleHeartRateData(WithingsStructure structure) {
+ int heartRate = 0;
+ if (structure instanceof HeartRate) {
+ heartRate = ((HeartRate)structure).getHeartrate();
+ } else if (structure instanceof LiveHeartRate) {
+ heartRate = ((LiveHeartRate)structure).getHeartrate();
+ }
+
+ if (heartRate > 0) {
+ saveHeartRateData(heartRate);
+ }
+ }
+
+ private void saveHeartRateData(int heartRate) {
+ WithingsSteelHRActivitySample sample = new WithingsSteelHRActivitySample();
+ sample.setTimestamp((int) (GregorianCalendar.getInstance().getTimeInMillis() / 1000L));
+ sample.setHeartRate(heartRate);
+ try (DBHandler dbHandler = GBApplication.acquireDB()) {
+ Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
+ Long deviceId = DBHelper.getDevice(device, dbHandler.getDaoSession()).getId();
+ WithingsSteelHRSampleProvider provider = new WithingsSteelHRSampleProvider(device, dbHandler.getDaoSession());
+ sample.setDeviceId(deviceId);
+ sample.setUserId(userId);
+ sample = SleepActivitySampleHelper.mergeIfNecessary(provider, sample);
+ provider.addGBActivitySample(sample);
+ Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
+ .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
+ LocalBroadcastManager.getInstance(support.getContext()).sendBroadcast(intent);
+ } catch (Exception ex) {
+ logger.warn("Error saving current heart rate: " + ex.getLocalizedMessage());
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ResponseHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ResponseHandler.java
new file mode 100644
index 000000000..096c6d64d
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ResponseHandler.java
@@ -0,0 +1,23 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+
+public interface ResponseHandler {
+ void handleResponse(Message response);
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SetupFinishedHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SetupFinishedHandler.java
new file mode 100644
index 000000000..5f7945464
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SetupFinishedHandler.java
@@ -0,0 +1,35 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
+
+public class SetupFinishedHandler extends AbstractResponseHandler {
+
+ public SetupFinishedHandler(WithingsSteelHRDeviceSupport support) {
+ super(support);
+ }
+
+ @Override
+ public void handleResponse(Message response) {
+ if (response.getType() == WithingsMessageType.SETUP_FINISHED) {
+ support.finishInitialization();;
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SimpleConversation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SimpleConversation.java
new file mode 100644
index 000000000..ca5e42228
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SimpleConversation.java
@@ -0,0 +1,42 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+
+public class SimpleConversation extends AbstractConversation {
+
+ public SimpleConversation(ResponseHandler responseHandler) {
+ super(responseHandler);
+ }
+
+ public SimpleConversation() {
+ super(null);
+ }
+
+ @Override
+ protected void doSendRequest(Message message) {
+ // Do nothing
+ }
+
+ @Override
+ protected void doHandleResponse(Message message) {
+ if (responseHandler != null) {
+ responseHandler.handleResponse(message);
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SyncFinishedHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SyncFinishedHandler.java
new file mode 100644
index 000000000..fa98e90f2
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SyncFinishedHandler.java
@@ -0,0 +1,37 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
+
+public class SyncFinishedHandler extends AbstractResponseHandler {
+
+ public SyncFinishedHandler(WithingsSteelHRDeviceSupport support) {
+ super(support);
+ }
+
+
+
+ @Override
+ public void handleResponse(Message response) {
+ if (response.getType() == WithingsMessageType.SYNC_OK) {
+ support.finishSync();
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/WorkoutScreenListHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/WorkoutScreenListHandler.java
new file mode 100644
index 000000000..fad7ad734
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/WorkoutScreenListHandler.java
@@ -0,0 +1,64 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
+
+import android.content.SharedPreferences;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.WithingsActivityType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WorkoutScreenList;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+
+public class WorkoutScreenListHandler extends AbstractResponseHandler {
+
+ public WorkoutScreenListHandler(WithingsSteelHRDeviceSupport support) {
+ super(support);
+ }
+
+ @Override
+ public void handleResponse(Message response) {
+ List data = response.getDataStructures();
+ if (data != null && !data.isEmpty()) {
+ WorkoutScreenList screenList = (WorkoutScreenList) data.get(0);
+ saveScreenList(screenList);
+ }
+ }
+
+ private void saveScreenList(WorkoutScreenList screenList) {
+ int[] workoutIds = screenList.getWorkoutIds();
+ List prefValues = new ArrayList<>();
+ for (int i = 0; i < workoutIds.length; i++) {
+ int currentId = workoutIds[i];
+ if (currentId > 0) {
+ WithingsActivityType type = WithingsActivityType.fromCode(currentId);
+ prefValues.add(type.name().toLowerCase(Locale.ROOT));
+ }
+ }
+
+ String workoutActivityTypes = String.join(",", prefValues);
+ GBDevice device = support.getDevice();
+ final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress());
+ prefs.edit().putString("workout_activity_types_sortable", workoutActivityTypes).apply();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivityHeartrate.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivityHeartrate.java
new file mode 100644
index 000000000..457e474ff
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivityHeartrate.java
@@ -0,0 +1,47 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class ActivityHeartrate extends WithingsStructure
+{
+ private int heartrate;
+
+ public int getHeartrate() {
+ return heartrate;
+ }
+
+ @Override
+ public short getLength() {
+ return 8;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ }
+
+ @Override
+ protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ heartrate = rawDataBuffer.get(0) & 0xff;
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.ACTIVITY_HR;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleCalories.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleCalories.java
new file mode 100644
index 000000000..71da171ee
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleCalories.java
@@ -0,0 +1,54 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class ActivitySampleCalories extends WithingsStructure {
+
+ private int calories;
+ private int met;
+
+ public int getCalories() {
+ return calories;
+ }
+
+ public int getMet() {
+ return met;
+ }
+
+ @Override
+ public short getLength() {
+ return 8;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ calories = rawDataBuffer.getShort() & 65535;
+ met = rawDataBuffer.getShort() & 65535;
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.ACTIVITY_SAMPLE_CALORIES;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleCalories2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleCalories2.java
new file mode 100644
index 000000000..da61199d9
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleCalories2.java
@@ -0,0 +1,25 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+public class ActivitySampleCalories2 extends ActivitySampleCalories {
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.ACTIVITY_SAMPLE_CALORIES_2;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleDuration.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleDuration.java
new file mode 100644
index 000000000..4b23da7fd
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleDuration.java
@@ -0,0 +1,51 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
+
+public class ActivitySampleDuration extends WithingsStructure {
+
+ private short duration;
+
+ public short getDuration() {
+ return duration;
+ }
+
+ @Override
+ public short getLength() {
+ return 6;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ duration = rawDataBuffer.getShort();
+ }
+
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.ACTIVITY_SAMPLE_DURATION;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleMovement.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleMovement.java
new file mode 100644
index 000000000..63df8f54c
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleMovement.java
@@ -0,0 +1,69 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class ActivitySampleMovement extends WithingsStructure {
+
+ private short steps;
+ private int distance;
+ private int asc;
+ private int desc;
+
+ public short getSteps() {
+ return steps;
+ }
+
+ public void setSteps(short steps) {
+ this.steps = steps;
+ }
+
+ public int getDistance() {
+ return distance;
+ }
+
+ public void setDistance(int distance) {
+ this.distance = distance;
+ }
+
+ @Override
+ public short getLength() {
+ return 18;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ buffer.putShort(steps);
+ buffer.putInt(distance);
+ buffer.putInt(asc);
+ buffer.putInt(desc);
+ }
+
+ @Override
+ public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ steps = rawDataBuffer.getShort();
+ distance = rawDataBuffer.getInt();
+ asc = rawDataBuffer.getInt();
+ desc = rawDataBuffer.getInt();
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.ACTIVITY_SAMPLE_MOVEMENT;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleRun.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleRun.java
new file mode 100644
index 000000000..2a1ae803f
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleRun.java
@@ -0,0 +1,36 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class ActivitySampleRun extends WithingsStructure {
+ @Override
+ public short getLength() {
+ return 0;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.ACTIVITY_SAMPLE_RUN;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleSleep.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleSleep.java
new file mode 100644
index 000000000..9df227e52
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleSleep.java
@@ -0,0 +1,50 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
+
+public class ActivitySampleSleep extends WithingsStructure {
+
+ private short sleepType;
+
+ public short getSleepType() {
+ return sleepType;
+ }
+
+ @Override
+ public short getLength() {
+ return 6;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ sleepType = rawDataBuffer.getShort();
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.ACTIVITY_SAMPLE_SLEEP;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleSwim.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleSwim.java
new file mode 100644
index 000000000..efb3e32b1
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleSwim.java
@@ -0,0 +1,36 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class ActivitySampleSwim extends WithingsStructure {
+ @Override
+ public short getLength() {
+ return 0;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.ACTIVITY_SAMPLE_SWIM;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleTime.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleTime.java
new file mode 100644
index 000000000..baaf95b9f
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleTime.java
@@ -0,0 +1,52 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+import java.util.Date;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
+
+public class ActivitySampleTime extends WithingsStructure {
+
+ private Date date;
+
+ public Date getDate() {
+ return date;
+ }
+
+ @Override
+ public short getLength() {
+ return 8;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ long timestampInSeconds = rawDataBuffer.getInt() & 4294967295L;
+ date = new Date(timestampInSeconds * 1000);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.ACTIVITY_SAMPLE_TIME;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleUnknown.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleUnknown.java
new file mode 100644
index 000000000..03ccfd696
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleUnknown.java
@@ -0,0 +1,36 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class ActivitySampleUnknown extends WithingsStructure {
+ @Override
+ public short getLength() {
+ return 8;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.ACTIVITY_SAMPLE_UNKNOWN;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleWalk.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleWalk.java
new file mode 100644
index 000000000..d68771148
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleWalk.java
@@ -0,0 +1,50 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
+
+public class ActivitySampleWalk extends WithingsStructure {
+
+ private short level;
+
+ public short getLevel() {
+ return level;
+ }
+
+ @Override
+ public short getLength() {
+ return 6;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ level = (short) (rawDataBuffer.getShort() & 65535);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.ACTIVITY_SAMPLE_WALK;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivityTarget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivityTarget.java
new file mode 100644
index 000000000..d4a9bf4e9
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivityTarget.java
@@ -0,0 +1,51 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class ActivityTarget extends WithingsStructure {
+
+ private long targetCount;
+
+ public ActivityTarget(long targetCount) {
+ this.targetCount = targetCount;
+ }
+
+ public long getTargetCount() {
+ return targetCount;
+ }
+
+ public void setTargetCount(long targetCount) {
+ this.targetCount = targetCount;
+ }
+
+ @Override
+ public short getLength() {
+ return 12;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer rawDataBuffer) {
+ rawDataBuffer.putLong(targetCount);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.ACTIVITY_TARGET;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmName.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmName.java
new file mode 100644
index 000000000..c2e05daef
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmName.java
@@ -0,0 +1,44 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+public class AlarmName extends WithingsStructure {
+
+ private String name;
+
+ public AlarmName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public short getLength() {
+ return (short) ((name != null ? name.getBytes().length : 0) + 1 + HEADER_SIZE);
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer rawDataBuffer) {
+ addStringAsBytesWithLengthByte(rawDataBuffer, name);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.ALARM_NAME;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmSettings.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmSettings.java
new file mode 100644
index 000000000..bb19b062b
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmSettings.java
@@ -0,0 +1,111 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class AlarmSettings extends WithingsStructure {
+ private short hour;
+ private short minute;
+ private short dayOfWeek;
+ private short dayOfMonth;
+ private short month;
+ private short year;
+ private short smartWakeupMinutes;
+
+ public short getHour() {
+ return hour;
+ }
+
+ public void setHour(short hour) {
+ this.hour = hour;
+ }
+
+ public short getMinute() {
+ return minute;
+ }
+
+ public void setMinute(short minute) {
+ this.minute = minute;
+ }
+
+ public short getDayOfWeek() {
+ return dayOfWeek;
+ }
+
+ public void setDayOfWeek(short dayOfWeek) {
+ this.dayOfWeek = dayOfWeek;
+ }
+
+ public short getDayOfMonth() {
+ return dayOfMonth;
+ }
+
+ public void setDayOfMonth(short dayOfMonth) {
+ this.dayOfMonth = dayOfMonth;
+ }
+
+ public short getMonth() {
+ return month;
+ }
+
+ public void setMonth(short month) {
+ this.month = month;
+ }
+
+ public short getYear() {
+ return year;
+ }
+
+ public void setYear(short year) {
+ this.year = year;
+ }
+
+ public short getYetUnkown() {
+ return smartWakeupMinutes;
+ }
+
+ public void setSmartWakeupMinutes(short smartWakeupMinutes) {
+ this.smartWakeupMinutes = smartWakeupMinutes;
+ }
+
+ @Override
+ public short getLength() {
+ return 11;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ buffer.put((byte)hour);
+ buffer.put((byte)minute);
+ buffer.put((byte)dayOfWeek);
+ buffer.put((byte)dayOfMonth);
+ buffer.put((byte)month);
+ buffer.put((byte)year);
+ buffer.put((byte)smartWakeupMinutes);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.ALARM;
+ }
+
+ @Override
+ public boolean withEndOfMessage() {
+ return true;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmStatus.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmStatus.java
new file mode 100644
index 000000000..708405be2
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmStatus.java
@@ -0,0 +1,51 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class AlarmStatus extends WithingsStructure {
+
+ private boolean enabled;
+
+ public AlarmStatus(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @Override
+ public short getLength() {
+ return 5;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ buffer.put(enabled? (byte) 1 : (byte) 0);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.ALARM_STATUS;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AncsStatus.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AncsStatus.java
new file mode 100644
index 000000000..0b95df5b5
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AncsStatus.java
@@ -0,0 +1,45 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class AncsStatus extends WithingsStructure {
+
+ private boolean isOn;
+
+ public AncsStatus() {}
+
+ public AncsStatus(boolean isOn) {
+ this.isOn = isOn;
+ }
+
+ @Override
+ public short getLength() {
+ return 5;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ buffer.put(isOn? (byte)0x01 : 0x00);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.ANCS_STATUS;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/BatteryValues.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/BatteryValues.java
new file mode 100644
index 000000000..adfeee192
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/BatteryValues.java
@@ -0,0 +1,73 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
+
+public class BatteryValues extends WithingsStructure {
+
+ private short percent;
+ private short status;
+ private int volt;
+
+ public short getPercent() {
+ return percent;
+ }
+
+ public void setPercent(short percent) {
+ this.percent = percent;
+ }
+
+ public short getStatus() {
+ return status;
+ }
+
+ public void setStatus(short status) {
+ this.status = status;
+ }
+
+ public int getVolt() {
+ return volt;
+ }
+
+ public void setVolt(int volt) {
+ this.volt = volt;
+ }
+
+ @Override
+ public short getLength() {
+ return 14;
+ }
+
+ @Override
+ public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ percent = (short)(rawDataBuffer.get() & 255);
+ status = (short)(rawDataBuffer.get() & 255);
+ volt = rawDataBuffer.getInt();
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.BATTERY_STATUS;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Challenge.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Challenge.java
new file mode 100644
index 000000000..a9709e052
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Challenge.java
@@ -0,0 +1,80 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+public class Challenge extends WithingsStructure {
+
+ private String macAddress;
+
+ private byte[] challenge;
+
+ public void setMacAddress(String macAddress) {
+ this.macAddress = macAddress;
+ }
+
+ public void setChallenge(byte[] challenge) {
+ this.challenge = challenge;
+ }
+
+ public String getMacAddress() {
+ return macAddress;
+ }
+
+ public byte[] getChallenge() {
+ return challenge;
+ }
+
+ @Override
+ public short getLength() {
+ int challengeLength = 0;
+ int macAddressLength = (macAddress != null ? macAddress.getBytes().length : 0) + 1;
+ if (challenge != null) {
+ challengeLength = challenge.length;
+ }
+
+ return (short) (macAddressLength + challengeLength + 5);
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ addStringAsBytesWithLengthByte(buffer, macAddress);
+
+ if (challenge != null) {
+ buffer.put((byte) challenge.length);
+ buffer.put(challenge);
+ } else {
+ buffer.put((byte)0);
+ }
+ }
+
+ @Override
+ public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ macAddress = getNextString(rawDataBuffer);
+ challenge = getNextByteArray(rawDataBuffer);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.CHALLENGE;
+ }
+
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ChallengeResponse.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ChallengeResponse.java
new file mode 100644
index 000000000..5fb86b32f
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ChallengeResponse.java
@@ -0,0 +1,54 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+public class ChallengeResponse extends WithingsStructure {
+
+ private byte[] response = new byte[0];
+
+ public byte[] getResponse() {
+ return response;
+ }
+
+ public void setResponse(byte[] response) {
+ this.response = response;
+ }
+
+ @Override
+ public short getLength() {
+ return (short) ((response != null ? response.length : 0) + 5);
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ addByteArrayWithLengthByte(buffer, response);
+ }
+
+ @Override
+ public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ response = getNextByteArray(rawDataBuffer);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.CHALLENGE_RESPONSE;
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/DataStructureFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/DataStructureFactory.java
new file mode 100644
index 000000000..89a4b591c
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/DataStructureFactory.java
@@ -0,0 +1,168 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class DataStructureFactory {
+
+ private static final Logger logger = LoggerFactory.getLogger(DataStructureFactory.class.getSimpleName());
+ private static final int HEADER_SIZE = 4;
+
+ public List createStructuresFromRawData(byte[] rawData) {
+ List structures = new ArrayList<>();
+ if (rawData == null) {
+ return structures;
+ }
+
+ List rawDataStructures = splitRawData(rawData);
+ for (byte[] rawDataStructure : rawDataStructures) {
+ WithingsStructure structure = null;
+
+ short structureTypeFromResponse = (short) BLETypeConversions.toInt16(rawDataStructure[1], rawDataStructure[0]);
+
+ switch (structureTypeFromResponse) {
+ case WithingsStructureType.HR:
+ structure = new HeartRate();
+ break;
+ case WithingsStructureType.LIVE_HR:
+ structure = new LiveHeartRate();
+ break;
+ case WithingsStructureType.BATTERY_STATUS:
+ structure = new BatteryValues();
+ break;
+ case WithingsStructureType.SCREEN_SETTINGS:
+ structure = new ScreenSettings();
+ break;
+ case WithingsStructureType.ANCS_STATUS:
+ structure = new AncsStatus();
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_TIME:
+ structure = new ActivitySampleTime();
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_DURATION:
+ structure = new ActivitySampleDuration();
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_MOVEMENT:
+ structure = new ActivitySampleMovement();
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_CALORIES:
+ structure = new ActivitySampleCalories();
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_CALORIES_2:
+ structure = new ActivitySampleCalories2();
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_SLEEP:
+ structure = new ActivitySampleSleep();
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_WALK:
+ structure = new ActivitySampleWalk();
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_RUN:
+ structure = new ActivitySampleRun();
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_SWIM:
+ structure = new ActivitySampleSwim();
+ break;
+ case WithingsStructureType.ACTIVITY_HR:
+ structure = new ActivityHeartrate();
+ break;
+ case WithingsStructureType.PROBE_REPLY:
+ structure = new ProbeReply();
+ break;
+ case WithingsStructureType.CHALLENGE:
+ structure = new Challenge();
+ break;
+ case WithingsStructureType.CHALLENGE_RESPONSE:
+ structure = new ChallengeResponse();
+ break;
+ case WithingsStructureType.ACTIVITY_SAMPLE_UNKNOWN:
+ structure = new ActivitySampleUnknown();
+ break;
+ case WithingsStructureType.END_OF_TRANSMISSION:
+ structure = new EndOfTransmission();
+ break;
+ case WithingsStructureType.WORKOUT_TYPE:
+ structure = new WorkoutType();
+ break;
+ case WithingsStructureType.LIVE_WORKOUT_START:
+ structure = new LiveWorkoutStart();
+ break;
+ case WithingsStructureType.LIVE_WORKOUT_END:
+ structure = new LiveWorkoutEnd();
+ break;
+ case WithingsStructureType.LIVE_WORKOUT_PAUSE_STATE:
+ structure = new LiveWorkoutPauseState();
+ break;
+ case WithingsStructureType.WORKOUT_SCREEN_LIST:
+ structure = new WorkoutScreenList();
+ break;
+ case WithingsStructureType.IMAGE_META_DATA:
+ structure = new ImageMetaData();
+ break;
+ case WithingsStructureType.GLYPH_ID:
+ structure = new GlyphId();
+ break;
+ case WithingsStructureType.NOTIFICATION_APP_ID:
+ structure = new GlyphId();
+ break;
+ default:
+ structure = null;
+ logger.info("Received yet unknown structure type: " + structureTypeFromResponse);
+ }
+
+ if (structure != null) {
+ structure.fillFromRawData(removeHeaderBytes(rawDataStructure));
+ structures.add(structure);
+ }
+ }
+
+ return structures;
+ }
+
+ private List splitRawData(byte[] rawData) {
+ int remainingBytes = rawData.length;
+ List result = new ArrayList<>();
+
+ while(remainingBytes > 3) {
+ short structureLength = (short) BLETypeConversions.toInt16(rawData[3], rawData[2]);
+ remainingBytes -= (structureLength + HEADER_SIZE);
+ try {
+ result.add(Arrays.copyOfRange(rawData, 0, structureLength + HEADER_SIZE));
+ if (remainingBytes > 0) {
+ rawData = Arrays.copyOfRange(rawData, structureLength + HEADER_SIZE, rawData.length);
+ }
+ } catch (Exception e) {
+ logger.warn("Splitting of data failed: " + GB.hexdump(rawData));
+ }
+ }
+
+ return result;
+ }
+
+ private byte[] removeHeaderBytes(byte[] data) {
+ return Arrays.copyOfRange(data, HEADER_SIZE, data.length);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/EndOfTransmission.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/EndOfTransmission.java
new file mode 100644
index 000000000..d969885f7
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/EndOfTransmission.java
@@ -0,0 +1,44 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class EndOfTransmission extends WithingsStructure {
+ @Override
+ public short getLength() {
+ return 4;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ public byte[] getRawData() {
+ ByteBuffer rawDataBuffer = ByteBuffer.allocate(4);
+ rawDataBuffer.putShort(getType());
+ rawDataBuffer.putShort((short)0);
+ return rawDataBuffer.array();
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.END_OF_TRANSMISSION;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/GetActivitySamples.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/GetActivitySamples.java
new file mode 100644
index 000000000..48d114ad8
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/GetActivitySamples.java
@@ -0,0 +1,47 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class GetActivitySamples extends WithingsStructure {
+
+ public long timestampFrom;
+
+ public short maxSampleCount;
+
+ public GetActivitySamples(long timestampFrom, short maxSampleCount) {
+ this.timestampFrom = timestampFrom;
+ this.maxSampleCount = maxSampleCount;
+ }
+
+ @Override
+ public short getLength() {
+ return 10;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ buffer.putInt((int)(timestampFrom & 4294967295L));
+ buffer.putShort((short)maxSampleCount);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.GET_ACTIVITY_SAMPLES;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/GlyphId.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/GlyphId.java
new file mode 100644
index 000000000..0cc68df00
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/GlyphId.java
@@ -0,0 +1,50 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class GlyphId extends WithingsStructure {
+
+ private long unicode;
+
+ public long getUnicode() {
+ return unicode;
+ }
+
+ @Override
+ public short getLength() {
+ return 8;
+ }
+
+ @Override
+ protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ int value = rawDataBuffer.getInt();
+ unicode = ByteBuffer.allocate(4).putInt(value).order(ByteOrder.LITTLE_ENDIAN).getInt(0);
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.GLYPH_ID;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/HeartRate.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/HeartRate.java
new file mode 100644
index 000000000..2d4736b16
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/HeartRate.java
@@ -0,0 +1,47 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class HeartRate extends WithingsStructure {
+
+ private int heartrate;
+
+ public int getHeartrate() {
+ return heartrate;
+ }
+
+ @Override
+ public short getLength() {
+ return 5;
+ }
+
+ @Override
+ public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ heartrate = rawDataBuffer.get(1) & 0xff;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.HR;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ImageData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ImageData.java
new file mode 100644
index 000000000..d2e1123e7
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ImageData.java
@@ -0,0 +1,47 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class ImageData extends WithingsStructure {
+
+ byte [] imageData;
+
+ public void setImageData(byte[] imageData) {
+ this.imageData = imageData;
+ }
+
+ @Override
+ public short getLength() {
+ return imageData != null ? (short)(imageData.length + 1 + HEADER_SIZE) : 1 + HEADER_SIZE;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ if (imageData != null) {
+ addByteArrayWithLengthByte(buffer, imageData);
+ } else {
+ addByteArrayWithLengthByte(buffer, new byte[0]);
+ }
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.IMAGE_DATA;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ImageMetaData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ImageMetaData.java
new file mode 100644
index 000000000..847e9e911
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ImageMetaData.java
@@ -0,0 +1,66 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class ImageMetaData extends WithingsStructure {
+
+ private byte unknown = 0x00;
+ private byte width;
+ private byte height;
+
+ public byte getWidth() {
+ return width;
+ }
+
+ public void setWidth(byte width) {
+ this.width = width;
+ }
+
+ public byte getHeight() {
+ return height;
+ }
+
+ public void setHeight(byte height) {
+ this.height = height;
+ }
+
+ @Override
+ public short getLength() {
+ return 7;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ buffer.put(unknown);
+ buffer.put(width);
+ buffer.put(height);
+ }
+
+ @Override
+ public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ unknown = rawDataBuffer.get();
+ width = rawDataBuffer.get();
+ height = rawDataBuffer.get();
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.IMAGE_META_DATA;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveHeartRate.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveHeartRate.java
new file mode 100644
index 000000000..dfe801428
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveHeartRate.java
@@ -0,0 +1,48 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class LiveHeartRate extends WithingsStructure {
+
+ private int heartrate;
+
+ public int getHeartrate() {
+ return heartrate;
+ }
+
+ @Override
+ public short getLength() {
+ return 1;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ heartrate = rawDataBuffer.get() & 0xff;
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.LIVE_HR;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutEnd.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutEnd.java
new file mode 100644
index 000000000..e5694a7b9
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutEnd.java
@@ -0,0 +1,50 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+import java.util.Date;
+
+public class LiveWorkoutEnd extends WithingsStructure {
+
+ private Date endtime;
+
+ public Date getEndtime() {
+ return endtime;
+ }
+
+ @Override
+ public short getLength() {
+ return 8;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ long timestampInSeconds = rawDataBuffer.getInt() & 4294967295L;
+ endtime = new Date(timestampInSeconds * 1000);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.LIVE_WORKOUT_END;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutPauseState.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutPauseState.java
new file mode 100644
index 000000000..e919432c7
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutPauseState.java
@@ -0,0 +1,66 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+import java.util.Date;
+
+public class LiveWorkoutPauseState extends WithingsStructure {
+
+ private byte yetunknown;
+
+ // Is always the same as long as the actual pause continues:
+ private Date starttime;
+
+ // This is just a guess, but observation show that this is quite possible the meaning of this value that is send when the pause is over
+ private int lengthInSeconds;
+
+ public Date getStarttime() {
+ return starttime;
+ }
+
+ public int getLengthInSeconds() {
+ return lengthInSeconds;
+ }
+
+ @Override
+ public short getLength() {
+ return 13;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ yetunknown = rawDataBuffer.get();
+
+ long timestampInSeconds = rawDataBuffer.getInt() & 4294967295L;
+ if (timestampInSeconds > 0) {
+ starttime = new Date(timestampInSeconds * 1000);
+ }
+
+ lengthInSeconds = rawDataBuffer.getInt();
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.LIVE_WORKOUT_PAUSE_STATE;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutStart.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutStart.java
new file mode 100644
index 000000000..f59d648c3
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutStart.java
@@ -0,0 +1,50 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+import java.util.Date;
+
+public class LiveWorkoutStart extends WithingsStructure {
+
+ private Date starttime;
+
+ public Date getStarttime() {
+ return starttime;
+ }
+
+ @Override
+ public short getLength() {
+ return 8;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ long timestampInSeconds = rawDataBuffer.getInt() & 4294967295L;
+ starttime = new Date(timestampInSeconds * 1000);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.LIVE_WORKOUT_START;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Locale.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Locale.java
new file mode 100644
index 000000000..e3e31a9d1
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Locale.java
@@ -0,0 +1,44 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+public class Locale extends WithingsStructure {
+
+ private String locale = "en";
+
+ public Locale(String locale) {
+ this.locale = locale;
+ }
+
+ @Override
+ public short getLength() {
+ return (short) ((locale != null ? locale.getBytes().length : 0) + 1 + HEADER_SIZE);
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer rawDataBuffer) {
+ addStringAsBytesWithLengthByte(rawDataBuffer, locale);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.LOCALE;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/MoveHand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/MoveHand.java
new file mode 100644
index 000000000..be82c1e54
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/MoveHand.java
@@ -0,0 +1,49 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class MoveHand extends WithingsStructure {
+
+ private short hand;
+ private short movement;
+
+ public void setHand(short hand) {
+ this.hand = hand;
+ }
+
+ public void setMovement(short movement) {
+ this.movement = movement;
+ }
+
+ @Override
+ public short getLength() {
+ return 7;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ buffer.put((byte) (hand & 255));
+ buffer.putShort(movement);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.MOVE_HAND;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Probe.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Probe.java
new file mode 100644
index 000000000..904f1caaf
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Probe.java
@@ -0,0 +1,51 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class Probe extends WithingsStructure {
+
+ private short os;
+
+ private short app;
+
+ private long version;
+
+ public Probe(short os, short app, long version) {
+ this.os = os;
+ this.app = app;
+ this.version = version;
+ }
+
+ @Override
+ public short getLength() {
+ return 10;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ buffer.put((byte) (os & 255));
+ buffer.put((byte) (app & 255));
+ buffer.putInt((int) (version & 4294967295L));
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.PROBE;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeOsVersion.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeOsVersion.java
new file mode 100644
index 000000000..da5adbf5b
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeOsVersion.java
@@ -0,0 +1,43 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class ProbeOsVersion extends WithingsStructure {
+
+ private short osVersion;
+
+ public ProbeOsVersion(short osVersion) {
+ this.osVersion = osVersion;
+ }
+
+ @Override
+ public short getLength() {
+ return 6;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ buffer.putShort(osVersion);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.PROBE_OS_VERSION;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeReply.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeReply.java
new file mode 100644
index 000000000..07ef568ec
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeReply.java
@@ -0,0 +1,111 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.AuthenticationHandler;
+import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
+
+public class ProbeReply extends WithingsStructure {
+ private static final Logger logger = LoggerFactory.getLogger(ProbeReply.class);
+ private int yetUnknown1;
+ private String name;
+ private String mac;
+ private String secret;
+ private int yetUnknown2;
+ private String mId;
+ private int yetUnknown3;
+ private int firmwareVersion;
+ private int yetUnknown4;
+
+ public int getYetUnknown1() {
+ return yetUnknown1;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getMac() {
+ return mac;
+ }
+
+ public String getSecret() {
+ return secret;
+ }
+
+ public int getYetUnknown2() {
+ return yetUnknown2;
+ }
+
+ public String getmId() {
+ return mId;
+ }
+
+ public int getYetUnknown3() {
+ return yetUnknown3;
+ }
+
+ public int getFirmwareVersion() {
+ return firmwareVersion;
+ }
+
+ public int getYetUnknown4() {
+ return yetUnknown4;
+ }
+
+ @Override
+ public short getLength() {
+ int length = (name != null ? name.getBytes(StandardCharsets.UTF_8).length : 0) + 1;
+ length += (mac != null ? mac.getBytes().length : 0) + 1;
+ length += (secret != null ? secret.getBytes().length : 0) + 1;
+ length += (mId != null ? mId.getBytes().length : 0) + 1;
+ return (short) (length + 24);
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ try {
+ yetUnknown1 = rawDataBuffer.getInt();
+ name = getNextString(rawDataBuffer);
+ mac = getNextString(rawDataBuffer);
+ secret = getNextString(rawDataBuffer);
+ yetUnknown2 = rawDataBuffer.getInt();
+ mId = getNextString(rawDataBuffer);
+ yetUnknown3 = rawDataBuffer.getInt();
+ firmwareVersion = rawDataBuffer.getInt();
+ yetUnknown4 = rawDataBuffer.getInt();
+ } catch (Exception e) {
+ logger.warn("Could not handle buffer with data " + StringUtils.bytesToHex(rawDataBuffer.array()));
+ }
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.PROBE_REPLY;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ScreenSettings.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ScreenSettings.java
new file mode 100644
index 000000000..22c6f438f
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ScreenSettings.java
@@ -0,0 +1,67 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class ScreenSettings extends WithingsStructure {
+
+ private int id;
+
+ // TODO change to an actual unique ID. Must then be changed in User too.
+ private int userId = 123456;
+ private int yetUnknown1 = 0;
+ private int yetUnknown2 = 0;
+ private byte idOnDevice;
+ private byte yetUnkown3 = 0x01;
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public byte getIdOnDevice() {
+ return idOnDevice;
+ }
+
+ public void setIdOnDevice(byte idOnDevice) {
+ this.idOnDevice = idOnDevice;
+ }
+
+ @Override
+ public short getLength() {
+ return 22;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ buffer.putInt(this.id);
+ buffer.putInt(this.userId);
+ buffer.putInt(this.yetUnknown1);
+ buffer.putInt(this.yetUnknown2);
+ buffer.put(this.idOnDevice);
+ buffer.put(this.yetUnkown3);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.SCREEN_SETTINGS;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/SourceAppId.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/SourceAppId.java
new file mode 100644
index 000000000..98892a599
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/SourceAppId.java
@@ -0,0 +1,52 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class SourceAppId extends WithingsStructure {
+
+ private String appId;
+
+ public String getAppId() {
+ return appId;
+ }
+
+ public void setAppId(String appId) {
+ this.appId = appId;
+ }
+
+ @Override
+ public short getLength() {
+ return (short) ((appId != null ? appId.getBytes().length : 0) + 5);
+ }
+
+ @Override
+ protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ appId = getNextString(rawDataBuffer);
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ addStringAsBytesWithLengthByte(buffer, appId);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.NOTIFICATION_APP_ID;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Status.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Status.java
new file mode 100644
index 000000000..861a6a1d5
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Status.java
@@ -0,0 +1,36 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class Status extends WithingsStructure {
+ @Override
+ public short getLength() {
+ return 5;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ buffer.put((byte) 0x01);
+ }
+
+ @Override
+ public short getType() {
+ return (short) 2420;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Time.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Time.java
new file mode 100644
index 000000000..e7d41f2d7
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Time.java
@@ -0,0 +1,102 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import org.threeten.bp.Instant;
+import org.threeten.bp.ZoneId;
+import org.threeten.bp.zone.ZoneOffsetTransition;
+import org.threeten.bp.zone.ZoneRules;
+
+import java.nio.ByteBuffer;
+import java.util.Date;
+import java.util.TimeZone;
+
+import ch.qos.logback.core.encoder.ByteArrayUtil;
+
+public class Time extends WithingsStructure {
+
+ private Instant now;
+ private int timeOffsetInSeconds;
+ private Instant nextDaylightSavingTransition;
+ private int nextDaylightSavingTransitionOffsetInSeconds;
+
+ public Time() {
+ now = Instant.now();
+ final TimeZone timezone = TimeZone.getDefault();
+ timeOffsetInSeconds = timezone.getOffset(now.toEpochMilli()) / 1000;
+ final ZoneId zoneId = ZoneId.systemDefault();
+ final ZoneRules zoneRules = zoneId.getRules();
+ final ZoneOffsetTransition nextTransition = zoneRules.nextTransition(Instant.now());
+ long nextTransitionTs = 0;
+ if (nextTransition != null) {
+ nextTransitionTs = nextTransition.getDateTimeBefore().atZone(zoneId).toEpochSecond();
+ nextDaylightSavingTransitionOffsetInSeconds = nextTransition.getOffsetAfter().getTotalSeconds();
+ }
+
+ nextDaylightSavingTransition = Instant.ofEpochSecond(nextTransitionTs);
+ }
+
+ public Instant getNow() {
+ return now;
+ }
+
+ public void setNow(Instant now) {
+ this.now = now;
+ }
+
+ public int getTimeOffsetInSeconds() {
+ return timeOffsetInSeconds;
+ }
+
+ public void setTimeOffsetInSeconds(int TimeOffsetInSeconds) {
+ this.timeOffsetInSeconds = TimeOffsetInSeconds;
+ }
+
+ public Instant getNextDaylightSavingTransition() {
+ return nextDaylightSavingTransition;
+ }
+
+ public void setNextDaylightSavingTransition(Instant nextDaylightSavingTransition) {
+ this.nextDaylightSavingTransition = nextDaylightSavingTransition;
+ }
+
+ public int getNextDaylightSavingTransitionOffsetInSeconds() {
+ return nextDaylightSavingTransitionOffsetInSeconds;
+ }
+
+ public void setNextDaylightSavingTransitionOffsetInSeconds(int nextDaylightSavingTransitionOffsetInSeconds) {
+ this.nextDaylightSavingTransitionOffsetInSeconds = nextDaylightSavingTransitionOffsetInSeconds;
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.TIME;
+ }
+
+ @Override
+ public short getLength() {
+ return 20;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer rawDataBuffer) {
+ rawDataBuffer.putInt((int)now.getEpochSecond());
+ rawDataBuffer.putInt(timeOffsetInSeconds);
+ rawDataBuffer.putInt((int)nextDaylightSavingTransition.getEpochSecond());
+ rawDataBuffer.putInt(nextDaylightSavingTransitionOffsetInSeconds);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/TypeVersion.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/TypeVersion.java
new file mode 100644
index 000000000..5a5156986
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/TypeVersion.java
@@ -0,0 +1,39 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class TypeVersion extends WithingsStructure {
+
+ private byte version = 0x01;
+
+ @Override
+ public short getLength() {
+ return 5;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ buffer.put(version);
+ }
+
+ @Override
+ public short getType() {
+ return 2401;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/User.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/User.java
new file mode 100644
index 000000000..950a89ab8
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/User.java
@@ -0,0 +1,102 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+
+public class User extends WithingsStructure {
+
+ // This is just a dummy value as this seems to be the withings account id,
+ // which we do not need, but the watch expects:
+ private int userID = 123456;
+ private int weight;
+ private int height;
+ //Seems to be 0x00 for male and 0x01 for female. Found no other in my tests.
+ private byte gender;
+ private Date birthdate;
+ private String name;
+
+ public int getUserID() {
+ return userID;
+ }
+
+ public void setUserID(int userID) {
+ this.userID = userID;
+ }
+
+ public int getWeight() {
+ return weight;
+ }
+
+ public void setWeight(int weight) {
+ this.weight = weight;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ public void setHeight(int height) {
+ this.height = height;
+ }
+
+ public byte getGender() {
+ return gender;
+ }
+
+ public void setGender(byte gender) {
+ this.gender = gender;
+ }
+
+ public Date getBirthdate() {
+ return birthdate;
+ }
+
+ public void setBirthdate(Date birthdate) {
+ this.birthdate = birthdate;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public short getLength() {
+ return (short) ((name != null ? name.getBytes(StandardCharsets.UTF_8).length : 0) + 22);
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer rawDataBuffer) {
+ rawDataBuffer.putInt(userID);
+ rawDataBuffer.putInt(weight);
+ rawDataBuffer.putInt(height);
+ rawDataBuffer.put(gender);
+ rawDataBuffer.putInt((int)(birthdate.getTime()/1000));
+ addStringAsBytesWithLengthByte(rawDataBuffer, name);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.USER;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserSecret.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserSecret.java
new file mode 100644
index 000000000..a030fb267
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserSecret.java
@@ -0,0 +1,39 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class UserSecret extends WithingsStructure {
+
+ private String secret = "2EM5zNP37QzM00hmP6BFTD92nG15XwNd";
+
+ @Override
+ public short getLength() {
+ return 37;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ addStringAsBytesWithLengthByte(buffer, secret);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.USER_SECRET;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserUnit.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserUnit.java
new file mode 100644
index 000000000..2adc4ebce
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserUnit.java
@@ -0,0 +1,52 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class UserUnit extends WithingsStructure {
+
+ private byte unknown1 = 0;
+ private byte unknown2 = 0;
+ private short type;
+ private short unit;
+
+ public UserUnit() {}
+
+ public UserUnit(short type, short unit) {
+ this.type = type;
+ this.unit = unit;
+ }
+
+ @Override
+ public short getLength() {
+ return 10;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ buffer.put(unknown1);
+ buffer.put(unknown2);
+ buffer.putShort(type);
+ buffer.putShort(unit);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.USER_UNIT;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserUnitConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserUnitConstants.java
new file mode 100644
index 000000000..7f4e38233
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserUnitConstants.java
@@ -0,0 +1,29 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+public final class UserUnitConstants {
+
+ public static final short DISTANCE = 2;
+ public static final short CLOCK_MODE = 4;
+ public static final short UNIT_KM = 24;
+ public static final short UNIT_MILES = 25;
+ public static final short UNIT_24H = 26;
+ public static final short UNIT_12H = 27;
+
+ private UserUnitConstants() {}
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructure.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructure.java
new file mode 100644
index 000000000..9e94d852f
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructure.java
@@ -0,0 +1,103 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+
+/**
+ * This abstract class is the common denominator for all data structures used inside commands and the corresponding responses.
+ * @see Message
+ */
+public abstract class WithingsStructure {
+ protected final static short HEADER_SIZE = 4;
+
+ /**
+ * Some messages have some end bytes, some have not.
+ * Subclasses that need to have the eom appended need to overwrite this class and return true.
+ * The default value is false.
+ *
+ * @return true if some end of message should be appended
+ */
+ public boolean withEndOfMessage() {
+ return false;
+ }
+
+ public byte[] getRawData() {
+ short length = (getLength());
+ ByteBuffer rawDataBuffer = ByteBuffer.allocate(length);
+ rawDataBuffer.putShort(getType());
+ rawDataBuffer.putShort((short)(length - HEADER_SIZE));
+ fillinTypeSpecificData(rawDataBuffer);
+ return rawDataBuffer.array();
+ }
+
+ protected void addStringAsBytesWithLengthByte(ByteBuffer buffer, String str) {
+ if (str == null) {
+ buffer.put((byte)0);
+ } else {
+ byte[] stringAsBytes = str.getBytes(StandardCharsets.UTF_8);
+ buffer.put((byte)stringAsBytes.length);
+ buffer.put(stringAsBytes);
+ }
+ }
+
+ protected void fillFromRawData(byte[] rawData) {
+ fillFromRawDataAsBuffer(ByteBuffer.wrap(rawData));
+ };
+
+ protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {};
+
+ public abstract short getLength();
+
+ protected abstract void fillinTypeSpecificData(ByteBuffer buffer);
+ public abstract short getType();
+
+ protected String getNextString(ByteBuffer byteBuffer) {
+ // For strings in the raw data the first byte of the data is the length of the string:
+ int stringLength = (short)(byteBuffer.get() & 255);
+ byte[] stringBytes = new byte[stringLength];
+ byteBuffer.get(stringBytes);
+ return new String(stringBytes, Charset.forName("UTF-8"));
+ }
+
+ protected byte[] getNextByteArray(ByteBuffer byteBuffer) {
+ int arrayLength = (short)(byteBuffer.get() & 255);
+ byte[] nextByteArray = new byte[arrayLength];
+ byteBuffer.get(nextByteArray);
+ return nextByteArray;
+ }
+
+ protected int[] getNextIntArray(ByteBuffer byteBuffer) {
+ int arrayLength = (short)(byteBuffer.get() & 255);
+ int[] nextIntArray = new int[arrayLength];
+ for (int i = 0; i < arrayLength; i++) {
+ nextIntArray[i] = byteBuffer.getInt();
+ }
+ return nextIntArray;
+ }
+
+ protected void addByteArrayWithLengthByte(ByteBuffer buffer, byte[] data) {
+ buffer.put((byte) data.length);
+ if (data.length != 0) {
+ buffer.put(data);
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructureType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructureType.java
new file mode 100644
index 000000000..14167c550
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructureType.java
@@ -0,0 +1,72 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+public class WithingsStructureType {
+
+ public static final short END_OF_TRANSMISSION = 256;
+ public static final short PROBE_REPLY = 257;
+ public static final short PROBE = 298;
+ public static final short CHALLENGE = 290;
+ public static final short CHALLENGE_RESPONSE = 291;
+ public static final short PROBE_OS_VERSION = 2344;
+ public static final short TIME = 1281;
+ public static final short SCREEN_SETTINGS = 1302;
+ public static final short WORKOUT_SCREEN_SETTINGS = 317;
+ public static final short BATTERY_STATUS = 1284;
+ public static final short USER = 1283;
+ public static final short USER_SECRET = 1299;
+ public static final short USER_UNIT = 281;
+ public static final short ACTIVITY_TARGET = 1297;
+ public static final short LOCALE = 289;
+ public static final short LIVE_HR = 2369;
+ public static final short HR = 2343;
+ public static final short ACTIVITY_HR = 2345;
+ public static final short ALARM = 1298;
+ public static final short ALARM_STATUS = 2329;
+ public static final short ALARM_NAME = 1300;
+ public static final short STEPS = 2390;
+ public static final short IMAGE_META_DATA = 2397;
+ public static final short IMAGE_DATA = 2398;
+ public static final short ANCS_STATUS = 2346;
+ public static final short NOTIFICATION_APP_ID = 2404;
+ public static final short GLYPH_ID = 2396;
+ public static final short MOVE_HAND = 1292;
+
+ public static final short GET_ACTIVITY_SAMPLES = 1286;
+ public static final short ACTIVITY_SAMPLE_TIME = 1537;
+ public static final short ACTIVITY_SAMPLE_DURATION = 1538;
+ public static final short ACTIVITY_SAMPLE_MOVEMENT = 1539;
+ public static final short ACTIVITY_SAMPLE_WALK = 1540;
+ public static final short ACTIVITY_SAMPLE_RUN = 1541;
+ public static final short ACTIVITY_SAMPLE_SWIM = 1549;
+ public static final short ACTIVITY_SAMPLE_SLEEP = 1543;
+ // There are two structure types containing information about calories:
+ public static final short ACTIVITY_SAMPLE_CALORIES = 1544;
+ public static final short ACTIVITY_SAMPLE_CALORIES_2 = 1546;
+ // No idea what this is, however it is in the response to requesting activities:
+ public static final short ACTIVITY_SAMPLE_UNKNOWN = 1547;
+ public static final short WORKOUT_TYPE = 2409;
+ public static final short LIVE_WORKOUT_START = 2418;
+ public static final short LIVE_WORKOUT_END = 2419;
+ public static final short LIVE_WORKOUT_PAUSE_STATE = 2439;
+ public static final short WORKOUT_GPS_STATE = 321;
+ public static final short WORKOUT_SCREEN_LIST = 316;
+ public static final short WORKOUT_SCREEN_DATA = 317;
+
+ private WithingsStructureType() {}
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutGpsState.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutGpsState.java
new file mode 100644
index 000000000..9535e7e5a
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutGpsState.java
@@ -0,0 +1,43 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class WorkoutGpsState extends WithingsStructure {
+
+ private final boolean gpsEnabled;
+
+ public WorkoutGpsState(boolean gpsEnabled) {
+ this.gpsEnabled = gpsEnabled;
+ }
+
+ @Override
+ public short getLength() {
+ return 6;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ buffer.putShort(gpsEnabled? (short)1 : 0);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.WORKOUT_GPS_STATE;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreen.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreen.java
new file mode 100644
index 000000000..d7d5251d7
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreen.java
@@ -0,0 +1,67 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class WorkoutScreen extends WithingsStructure {
+
+ public static final byte MODE_PACE = 2;
+ public static final byte MODE_SPEED = 3;
+ public static final byte MODE_ELSE = 1;
+
+ public int id;
+
+ public byte yetunknown1 = 0;
+
+ public String name;
+
+ public byte mode = MODE_PACE;
+
+ public short yetunknown2 = 0;
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setMode(byte mode) {
+ this.mode = mode;
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.WORKOUT_SCREEN_SETTINGS;
+ }
+
+ @Override
+ public short getLength() {
+ return (short) ((name != null ? name.getBytes().length : 0) + 9 + HEADER_SIZE);
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer rawDataBuffer) {
+ rawDataBuffer.putInt(id);
+ rawDataBuffer.put(yetunknown1);
+ addStringAsBytesWithLengthByte(rawDataBuffer, name);
+ rawDataBuffer.put(mode);
+ rawDataBuffer.putShort(yetunknown2);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreenData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreenData.java
new file mode 100644
index 000000000..3e7961f5c
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreenData.java
@@ -0,0 +1,52 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class WorkoutScreenData extends WithingsStructure {
+
+ public long id;
+ public short version;
+ public String name;
+ public short faceMode;
+ public int flag;
+
+ @Override
+ public short getLength() {
+ return (short) ((name != null ? name.getBytes().length : 0) + 13);
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ this.id = rawDataBuffer.getInt() & 4294967295L;
+ this.version = (short) (rawDataBuffer.get() & 255);
+ this.name = getNextString(rawDataBuffer);
+ this.faceMode = (short) (rawDataBuffer.get() & 255);
+ this.flag = rawDataBuffer.getShort() & 65535;
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.WORKOUT_SCREEN_DATA;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreenList.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreenList.java
new file mode 100644
index 000000000..8918cd038
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreenList.java
@@ -0,0 +1,48 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class WorkoutScreenList extends WithingsStructure {
+
+ private int[] workoutIds;
+
+ public int[] getWorkoutIds() {
+ return workoutIds;
+ }
+
+ @Override
+ public short getLength() {
+ return 37;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ workoutIds = getNextIntArray(rawDataBuffer);
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.WORKOUT_SCREEN_LIST;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutType.java
new file mode 100644
index 000000000..b33781c66
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutType.java
@@ -0,0 +1,50 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+public class WorkoutType extends WithingsStructure {
+
+ public static short RUNNING = 0;
+
+ private short activityType;
+
+ public short getActivityType() {
+ return activityType;
+ }
+
+ @Override
+ public short getLength() {
+ return 6;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ activityType = rawDataBuffer.getShort();
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.WORKOUT_TYPE;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/AbstractMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/AbstractMessage.java
new file mode 100644
index 000000000..04e990b4e
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/AbstractMessage.java
@@ -0,0 +1,98 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public abstract class AbstractMessage implements Message {
+
+ /**
+ * The header consist of the first byte 0x01 (probably the message format identifier),
+ * two bytes for the message type and 2 bytes for the actual datalength.
+ */
+ private static final int HEADER_SIZE = 5;
+ protected final static short EOM_SIZE = 4;
+
+ private List dataStructures = new ArrayList();
+
+ public List getDataStructures() {
+ return Collections.unmodifiableList(dataStructures);
+ }
+
+ @Override
+ public void addDataStructure(WithingsStructure data) {
+ dataStructures.add(data);
+ }
+
+ @Override
+ public byte[] getRawData() {
+ short structureLength = 0;
+ boolean setEndOfMessage = false;
+ for (WithingsStructure structure : dataStructures) {
+ if (structure.withEndOfMessage()) {
+ setEndOfMessage = true;
+ }
+ structureLength += (short)(structure.getLength());
+ }
+
+ if (setEndOfMessage) {
+ structureLength += EOM_SIZE;
+ }
+
+ ByteBuffer rawDataBuffer = ByteBuffer.allocate(HEADER_SIZE + structureLength);
+ rawDataBuffer.put((byte)0x01); // <= This seems to be always 0x01 for all commands
+ rawDataBuffer.putShort(getType());
+ rawDataBuffer.putShort(structureLength);
+
+ for (WithingsStructure structure : dataStructures) {
+ rawDataBuffer.put(structure.getRawData());
+ }
+
+ if (setEndOfMessage) {
+ addEndOfMessageBytes(rawDataBuffer);
+ }
+
+ return rawDataBuffer.array();
+ }
+
+ @Override
+ public T getStructureByType(Class type) {
+ for (WithingsStructure structure : this.getDataStructures()) {
+ if (type.isInstance(structure)) {
+ return (T)structure;
+ }
+ }
+
+ return null;
+ }
+
+ private void addEndOfMessageBytes(ByteBuffer buffer) {
+ buffer.putShort((short)256);
+ buffer.putShort((short)0);
+ }
+
+ public String toString() {
+ return GB.hexdump(this.getRawData()).toLowerCase(Locale.ROOT);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/ExpectedResponse.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/ExpectedResponse.java
new file mode 100644
index 000000000..55c186b4a
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/ExpectedResponse.java
@@ -0,0 +1,23 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
+
+public enum ExpectedResponse {
+ NONE,
+ SIMPLE,
+ EOT
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/GlyphRequestHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/GlyphRequestHandler.java
new file mode 100644
index 000000000..48c91dc01
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/GlyphRequestHandler.java
@@ -0,0 +1,92 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.widget.Toast;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.IconHelper;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.GlyphId;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ImageData;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ImageMetaData;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming.IncomingMessageHandler;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class GlyphRequestHandler implements IncomingMessageHandler {
+ private static final Logger logger = LoggerFactory.getLogger(GlyphRequestHandler.class);
+ private final WithingsSteelHRDeviceSupport support;
+
+ public GlyphRequestHandler(WithingsSteelHRDeviceSupport support) {
+ this.support = support;
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ try {
+ GlyphId glyphId = message.getStructureByType(GlyphId.class);
+ ImageMetaData imageMetaData = message.getStructureByType(ImageMetaData.class);
+ Message reply = new WithingsMessage(WithingsMessageType.GET_UNICODE_GLYPH);
+ reply.addDataStructure(glyphId);
+ reply.addDataStructure(imageMetaData);
+ ImageData imageData = new ImageData();
+ imageData.setImageData(createUnicodeImage(glyphId.getUnicode(), imageMetaData));
+ reply.addDataStructure(imageData);
+ logger.info("Sending reply to glyph request: " + reply);
+ support.sendToDevice(reply);
+ } catch (Exception e) {
+ logger.error("Failed to respond to glyph request.", e);
+ GB.toast("Failed to respond to glyph request:" + e.getMessage(), Toast.LENGTH_LONG, GB.WARN);
+ }
+ }
+
+ private byte[] createUnicodeImage(long unicode, ImageMetaData metaData) {
+ String str = new String(Character.toChars((int)unicode));
+ Paint paint = new Paint();
+ paint.setTypeface(null);
+ Rect rect = new Rect();
+ paint.setTextSize(calculateTextsize(paint, metaData.getHeight()));
+ paint.setAntiAlias(true);
+ paint.getTextBounds(str, 0, str.length(), rect);
+ paint.setColor(-1);
+ Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt();
+ int width = rect.width();
+ if (width <= 0) {
+ return new byte[0];
+ }
+ Bitmap createBitmap = Bitmap.createBitmap(width, metaData.getHeight(), Bitmap.Config.ARGB_8888);
+ new Canvas(createBitmap).drawText(str, -rect.left, -fontMetricsInt.top, paint);
+ return IconHelper.toByteArray(createBitmap);
+ }
+
+ private int calculateTextsize(Paint paint, int height) {
+ Paint.FontMetricsInt fontMetricsInt;
+ int textsize = 0;
+ do {
+ textsize++;
+ paint.setTextSize(textsize);
+ fontMetricsInt = paint.getFontMetricsInt();
+ } while (fontMetricsInt.bottom - fontMetricsInt.top < height);
+ return textsize - 1;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/Message.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/Message.java
new file mode 100644
index 000000000..6e73ca786
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/Message.java
@@ -0,0 +1,36 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
+
+/**
+ * This interface is the common denominator for all messages passed to and from the Steel HR.
+ *
+ */
+public interface Message {
+ List getDataStructures();
+ void addDataStructure(WithingsStructure data);
+ short getType();
+ byte[] getRawData();
+ boolean needsResponse();
+ boolean needsEOT();
+ boolean isIncomingMessage();
+ T getStructureByType(Class type);
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageBuilder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageBuilder.java
new file mode 100644
index 000000000..eccd5039c
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageBuilder.java
@@ -0,0 +1,89 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
+
+public class MessageBuilder {
+
+ private static final Logger logger = LoggerFactory.getLogger(MessageBuilder.class);
+ private WithingsSteelHRDeviceSupport support;
+ private MessageFactory messageFactory;
+ private ByteArrayOutputStream pendingMessage;
+ private Message message;
+
+ public MessageBuilder(WithingsSteelHRDeviceSupport support, MessageFactory messageFactory) {
+ this.support = support;
+ this.messageFactory = messageFactory;
+ }
+
+ public synchronized boolean buildMessage(byte[] rawData) {
+ if (pendingMessage == null && rawData[0] == 0x01) {
+ pendingMessage = new ByteArrayOutputStream();
+ } else if (pendingMessage == null) {
+ return false;
+ }
+
+ try {
+ pendingMessage.write(rawData);
+ } catch(IOException e) {
+ logger.error("Could not write data to stream: " + StringUtils.bytesToHex(rawData));
+ return false;
+ }
+
+ if (!isMessageComplete(pendingMessage.toByteArray())) {
+ return false;
+ } else {
+ Message message = messageFactory.createMessageFromRawData(pendingMessage.toByteArray());
+ pendingMessage = null;
+ if (message == null) {
+ logger.info("Cannot handle null message");
+ return false;
+ }
+
+ this.message = message;
+ return true;
+ }
+ }
+
+ public Message getMessage() {
+ return message;
+ }
+
+ private boolean isMessageComplete(byte[] messageData) {
+ if (messageData.length < 5) {
+ return false;
+ }
+
+ short totalDataLength = (short) BLETypeConversions.toInt16(messageData[4], messageData[3]);
+ byte[] rawStructureData = Arrays.copyOfRange(messageData, 5, messageData.length);
+ if (rawStructureData.length == totalDataLength) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageFactory.java
new file mode 100644
index 000000000..2a092c5c5
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageFactory.java
@@ -0,0 +1,55 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Arrays;
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.DataStructureFactory;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
+
+
+public class MessageFactory {
+ private static final Logger logger = LoggerFactory.getLogger(MessageFactory.class);
+ private DataStructureFactory dataStructureFactory;
+
+ public MessageFactory(DataStructureFactory dataStructureFactory) {
+ this.dataStructureFactory = new DataStructureFactory();
+ }
+
+ public Message createMessageFromRawData(byte[] rawData) {
+ if (rawData.length < 5 || rawData[0] != 0x01) {
+ return null;
+ }
+
+ short messageTypeFromResponse = (short) (BLETypeConversions.toInt16(rawData[2], rawData[1]) & 16383);
+ short totalDataLength = (short) BLETypeConversions.toInt16(rawData[4], rawData[3]);
+ boolean isIncoming = rawData[1] == 65 || rawData[1] == -127;
+ Message message = new WithingsMessage(messageTypeFromResponse, isIncoming);
+ byte[] rawStructureData = Arrays.copyOfRange(rawData, 5, rawData.length);
+ List structures = dataStructureFactory.createStructuresFromRawData(rawStructureData);
+ for (WithingsStructure structure : structures) {
+ message.addDataStructure(structure);
+ }
+
+ return message;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/SimpleHexToByteMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/SimpleHexToByteMessage.java
new file mode 100644
index 000000000..7a713c05f
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/SimpleHexToByteMessage.java
@@ -0,0 +1,71 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class SimpleHexToByteMessage implements Message {
+ private String hexString;
+
+ public SimpleHexToByteMessage(String hexString) {
+ this.hexString = hexString;
+ }
+
+ @Override
+ public List getDataStructures() {
+ return null;
+ }
+
+ @Override
+ public void addDataStructure(WithingsStructure data) {
+
+ }
+
+ @Override
+ public short getType() {
+ return 0;
+ }
+
+ @Override
+ public byte[] getRawData() {
+ return GB.hexStringToByteArray(hexString);
+ }
+
+ @Override
+ public boolean needsResponse() {
+ return false;
+ }
+
+ @Override
+ public boolean needsEOT() {
+ return false;
+ }
+
+ @Override
+ public boolean isIncomingMessage() {
+ return false;
+ }
+
+ @Override
+ public T getStructureByType(Class type) {
+ return null;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessage.java
new file mode 100644
index 000000000..8a789b834
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessage.java
@@ -0,0 +1,64 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
+
+public class WithingsMessage extends AbstractMessage {
+ private short type;
+ private ExpectedResponse expectedResponse = ExpectedResponse.SIMPLE;
+ private boolean isIncoming;
+
+ public WithingsMessage(short type) {
+ this.type = type;
+ }
+
+ public WithingsMessage(short type, boolean incoming) {
+ this.type = type;
+ this.isIncoming = incoming;
+ }
+
+ public WithingsMessage(short type, ExpectedResponse expectedResponse) {
+ this.type = type;
+ this.expectedResponse = expectedResponse;
+ }
+
+ public WithingsMessage(short type, WithingsStructure dataStructure) {
+ this.type = type;
+ this.addDataStructure(dataStructure);
+ }
+
+ @Override
+ public boolean needsResponse() {
+ return expectedResponse == ExpectedResponse.SIMPLE;
+ }
+
+ @Override
+ public boolean needsEOT() {
+ return expectedResponse == ExpectedResponse.EOT;
+ }
+
+ @Override
+ public short getType() {
+ return type;
+ }
+
+ @Override
+ public boolean isIncomingMessage() {
+ return isIncoming;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessageType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessageType.java
new file mode 100644
index 000000000..e05fa31a4
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessageType.java
@@ -0,0 +1,68 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
+
+/**
+ * Contains all identified commandtypes in the used TLV format of the messages exchanged
+ * between device and app.
+ */
+public final class WithingsMessageType {
+
+ public static final short PROBE = 257;
+ public static final short CHALLENGE = 296;
+ public static final short SET_TIME = 1281;
+ public static final short GET_BATTERY_STATUS = 1284;
+ public static final short SET_SCREEN_LIST = 1292;
+ public static final short INITIAL_CONNECT = 273;
+ public static final short START_HANDS_CALIBRATION = 286;
+ public static final short STOP_HANDS_CALIBRATION = 287;
+ public static final short MOVE_HAND = 284;
+ public static final short SET_ACTIVITY_TARGET = 1290;
+ public static final short SET_USER = 1282;
+ public static final short GET_USER = 1283;
+ public static final short SET_USER_UNIT = 274;
+ public static final short SET_LOCALE = 282;
+ public static final short SETUP_FINISHED = 275;
+ public static final short GET_HR = 2343;
+ public static final short GET_WORKOUT_SCREEN_LIST = 315;
+ public static final short SET_WORKOUT_SCREEN = 316;
+ public static final short START_LIVE_WORKOUT = 317;
+ public static final short STOP_LIVE_WORKOUT = 318;
+ public static final short SYNC = 321;
+ public static final short SYNC_RESPONSE = 16705;
+ public static final short SYNC_OK = 277;
+ public static final short GET_ALARM_SETTINGS = 298;
+ public static final short SET_ALARM = 325;
+ public static final short GET_ALARM = 293;
+ public static final short GET_ALARM_ENABLED = 2330;
+ public static final short SET_ALARM_ENABLED = 2331;
+ public static final short GET_ANCS_STATUS = 2353;
+ public static final short SET_ANCS_STATUS = 2345;
+ public static final short GET_SCREEN_SETTINGS = 1293;
+ // The next two do nearly the same, when I look at the responses, though only the first seems to deliver sleep samples
+ public static final short GET_ACTIVITY_SAMPLES = 2424;
+ public static final short GET_MOVEMENT_SAMPLES = 1286;
+
+ public static final short GET_SPORT_MODE = 2371;
+ public static final short GET_WORKOUT_GPS_STATUS = 323;
+ public static final short GET_HEARTRATE_SAMPLES = 2344;
+ public static final short LIVE_WORKOUT_DATA = 320;
+ public static final short GET_NOTIFICATION = 2404;
+ public static final short GET_UNICODE_GLYPH = 2403;
+
+ private WithingsMessageType() {}
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandler.java
new file mode 100644
index 000000000..508ab2257
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandler.java
@@ -0,0 +1,23 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+
+public interface IncomingMessageHandler {
+ public void handleMessage(Message message);
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandlerFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandlerFactory.java
new file mode 100644
index 000000000..5a7465411
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandlerFactory.java
@@ -0,0 +1,86 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.GlyphRequestHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
+
+public class IncomingMessageHandlerFactory {
+
+ private static final Logger logger = LoggerFactory.getLogger(IncomingMessageHandlerFactory.class);
+ private static IncomingMessageHandlerFactory instance;
+ private final WithingsSteelHRDeviceSupport support;
+ private Map handlers = new HashMap<>();
+
+ private IncomingMessageHandlerFactory(WithingsSteelHRDeviceSupport support) {
+ this.support = support;
+ }
+
+ public static IncomingMessageHandlerFactory getInstance(WithingsSteelHRDeviceSupport support) {
+ if (instance == null) {
+ instance = new IncomingMessageHandlerFactory(support);
+ }
+
+ return instance;
+ }
+
+ public IncomingMessageHandler getHandler(Message message) {
+ IncomingMessageHandler handler = handlers.get(message.getType());
+ switch (message.getType()) {
+ case WithingsMessageType.START_LIVE_WORKOUT:
+ case WithingsMessageType.STOP_LIVE_WORKOUT:
+ case WithingsMessageType.GET_WORKOUT_GPS_STATUS:
+ if (handler == null) {
+ handlers.put(message.getType(), new LiveWorkoutHandler(support));
+ }
+ break;
+ case WithingsMessageType.LIVE_WORKOUT_DATA:
+ if (handler == null) {
+ handlers.put(message.getType(), new LiveHeartrateHandler(support));
+ }
+ break;
+ case WithingsMessageType.GET_NOTIFICATION:
+ if (handler == null) {
+ handlers.put(message.getType(), new NotificationRequestHandler(support));
+ }
+ break;
+ case WithingsMessageType.GET_UNICODE_GLYPH:
+ if (handler == null) {
+ handlers.put(message.getType(), new GlyphRequestHandler(support));
+ }
+ break;
+ case WithingsMessageType.SYNC:
+ if (handler == null) {
+ handlers.put(message.getType(), new SyncRequestHandler(support));
+ }
+ break;
+ default:
+ logger.warn("Unhandled incoming message type: " + message.getType());
+ }
+
+ return handlers.get(message.getType());
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveHeartrateHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveHeartrateHandler.java
new file mode 100644
index 000000000..8baee5bd4
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveHeartrateHandler.java
@@ -0,0 +1,88 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming;
+
+import android.content.Intent;
+
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.GregorianCalendar;
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsSteelHRSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.SleepActivitySampleHelper;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.LiveHeartRate;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+
+public class LiveHeartrateHandler implements IncomingMessageHandler {
+ private static final Logger logger = LoggerFactory.getLogger(LiveHeartrateHandler.class);
+ private final WithingsSteelHRDeviceSupport support;
+
+ public LiveHeartrateHandler(WithingsSteelHRDeviceSupport support) {
+ this.support = support;
+ }
+
+
+ @Override
+ public void handleMessage(Message message) {
+ List data = message.getDataStructures();
+ if (data == null || data.isEmpty()) {
+ return;
+ }
+
+ WithingsStructure structure = data.get(0);
+ int heartRate = 0;
+ if (structure instanceof LiveHeartRate) {
+ heartRate = ((LiveHeartRate)structure).getHeartrate();
+ }
+
+ if (heartRate > 0) {
+ saveHeartRateData(heartRate);
+ }
+
+ }
+
+ private void saveHeartRateData(int heartRate) {
+ WithingsSteelHRActivitySample sample = new WithingsSteelHRActivitySample();
+ sample.setTimestamp((int) (GregorianCalendar.getInstance().getTimeInMillis() / 1000L));
+ sample.setHeartRate(heartRate);
+ try (DBHandler dbHandler = GBApplication.acquireDB()) {
+ Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
+ Long deviceId = DBHelper.getDevice(support.getDevice(), dbHandler.getDaoSession()).getId();
+ WithingsSteelHRSampleProvider provider = new WithingsSteelHRSampleProvider(support.getDevice(), dbHandler.getDaoSession());
+ sample.setDeviceId(deviceId);
+ sample.setUserId(userId);
+ sample = SleepActivitySampleHelper.mergeIfNecessary(provider, sample);
+ provider.addGBActivitySample(sample);
+ } catch (Exception ex) {
+ logger.warn("Error saving current heart rate: " + ex.getLocalizedMessage());
+ }
+ Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
+ .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
+ LocalBroadcastManager.getInstance(support.getContext()).sendBroadcast(intent);
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveWorkoutHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveWorkoutHandler.java
new file mode 100644
index 000000000..28ccf7de7
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveWorkoutHandler.java
@@ -0,0 +1,147 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming;
+
+import android.location.LocationManager;
+import android.widget.Toast;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
+import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
+import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.entities.User;
+import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.WithingsActivityType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.LiveWorkoutEnd;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.LiveWorkoutPauseState;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.LiveWorkoutStart;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructureType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WorkoutGpsState;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WorkoutType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class LiveWorkoutHandler implements IncomingMessageHandler {
+ private static final Logger logger = LoggerFactory.getLogger(LiveWorkoutHandler.class);
+ private final WithingsSteelHRDeviceSupport support;
+ private BaseActivitySummary baseActivitySummary;
+
+ public LiveWorkoutHandler(WithingsSteelHRDeviceSupport support) {
+ this.support = support;
+ }
+
+ public void handleMessage(Message message) {
+ List data = message.getDataStructures();
+ if (data != null) {
+ handleLiveData(data);
+ }
+ }
+
+ private void handleLiveData(List dataList) {
+ for (WithingsStructure data : dataList) {
+ switch (data.getType()) {
+ case WithingsStructureType.LIVE_WORKOUT_START:
+ handleStart((LiveWorkoutStart) data);
+ break;
+ case WithingsStructureType.LIVE_WORKOUT_END:
+ handleEnd((LiveWorkoutEnd) data);
+ break;
+ case WithingsStructureType.LIVE_WORKOUT_PAUSE_STATE:
+ handlePause((LiveWorkoutPauseState) data);
+ break;
+ case WithingsStructureType.WORKOUT_TYPE:
+ handleType((WorkoutType) data);
+ break;
+ default:
+ logger.info("Received yet unhandled live workout data of type '" + data.getType() + "' with data '" + GB.hexdump(data.getRawData()) + "'.");
+ }
+ }
+ }
+
+ private void handleStart(LiveWorkoutStart workoutStart) {
+ sendGpsState();
+ if (baseActivitySummary == null) {
+ baseActivitySummary = new BaseActivitySummary();
+ }
+
+ baseActivitySummary.setStartTime(workoutStart.getStarttime());
+ }
+
+ private void handlePause(LiveWorkoutPauseState workoutPause) {
+ // Not sure what to do with these events at the moment so we just log them.
+ if (workoutPause.getStarttime() == null) {
+ if (workoutPause.getLengthInSeconds() > 0) {
+ logger.info("Got workout pause end with duration: " + workoutPause.getLengthInSeconds());
+ } else {
+ logger.info("Currently no pause happened");
+ }
+ } else {
+ logger.info("Got workout pause started at: " + workoutPause.getStarttime());
+ }
+ }
+
+ private void handleEnd(LiveWorkoutEnd workoutEnd) {
+ OpenTracksController.stopRecording(support.getContext());
+ baseActivitySummary.setEndTime(workoutEnd.getEndtime());
+ saveBaseActivitySummary();
+ baseActivitySummary = null;
+ }
+
+ private void handleType(WorkoutType workoutType) {
+ WithingsActivityType withingsWorkoutType = WithingsActivityType.fromCode(workoutType.getActivityType());
+ OpenTracksController.startRecording(support.getContext(), withingsWorkoutType.toActivityKind());
+ if (baseActivitySummary == null) {
+ baseActivitySummary = new BaseActivitySummary();
+ }
+
+ baseActivitySummary.setActivityKind(withingsWorkoutType.toActivityKind());
+ }
+
+ private void sendGpsState() {
+ Message message = new WithingsMessage((short)(WithingsMessageType.START_LIVE_WORKOUT | 16384), new WorkoutGpsState(isGpsEnabled()));
+ support.sendToDevice(message);
+ }
+
+ private boolean isGpsEnabled() {
+ final LocationManager manager = (LocationManager) support.getContext().getSystemService(support.getContext().LOCATION_SERVICE );
+ return manager.isProviderEnabled(LocationManager.GPS_PROVIDER );
+ }
+
+ private void saveBaseActivitySummary() {
+ try (DBHandler dbHandler = GBApplication.acquireDB()) {
+ DaoSession session = dbHandler.getDaoSession();
+ Device device = DBHelper.getDevice(support.getDevice(), session);
+ User user = DBHelper.getUser(session);
+ baseActivitySummary.setDevice(device);
+ baseActivitySummary.setUser(user);
+ session.getBaseActivitySummaryDao().insertOrReplace(baseActivitySummary);
+ } catch (Exception ex) {
+ GB.toast(support.getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, ex);
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/NotificationRequestHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/NotificationRequestHandler.java
new file mode 100644
index 000000000..f36d3d217
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/NotificationRequestHandler.java
@@ -0,0 +1,101 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.widget.Toast;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.IconHelper;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ImageData;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ImageMetaData;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.SourceAppId;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification.NotificationProvider;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class NotificationRequestHandler implements IncomingMessageHandler {
+ private static final Logger logger = LoggerFactory.getLogger(NotificationRequestHandler.class);
+
+ private final WithingsSteelHRDeviceSupport support;
+ private Map appIconCache = new HashMap<>();
+
+ public NotificationRequestHandler(WithingsSteelHRDeviceSupport support) {
+ this.support = support;
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ try {
+ SourceAppId appId = message.getStructureByType(SourceAppId.class);
+ ImageMetaData imageMetaData = message.getStructureByType(ImageMetaData.class);
+ Message reply = new WithingsMessage(WithingsMessageType.GET_NOTIFICATION);
+ reply.addDataStructure(appId);
+ reply.addDataStructure(imageMetaData);
+ ImageData imageData = new ImageData();
+ imageData.setImageData(getImageData(appId.getAppId()));
+ reply.addDataStructure(imageData);
+ logger.info("Sending reply to notification request: " + reply);
+ support.sendToDevice(reply);
+ } catch (Exception e) {
+ logger.error("Failed to respond to notification request.", e);
+ GB.toast("Failed to respond to notification request:" + e.getMessage(), Toast.LENGTH_LONG, GB.WARN);
+ }
+ }
+
+ private byte[] getImageData(String sourceAppId) {
+ byte[] imageData = appIconCache.get(sourceAppId);
+ if (imageData == null) {
+ NotificationSpec notificationSpec = NotificationProvider.getInstance(support).getNotificationSpecForSourceAppId(sourceAppId);
+ if (notificationSpec != null) {
+ int iconId = notificationSpec.iconId;
+ try {
+ Drawable icon = null;
+ if (notificationSpec.iconId != 0) {
+ Context sourcePackageContext = support.getContext().createPackageContext(sourceAppId, 0);
+ icon = sourcePackageContext.getResources().getDrawable(notificationSpec.iconId);
+ }
+ if (icon == null) {
+ PackageManager pm = support.getContext().getPackageManager();
+ icon = pm.getApplicationIcon(sourceAppId);
+ }
+
+ imageData = IconHelper.getIconBytesFromDrawable(icon);
+ appIconCache.put(sourceAppId, imageData);
+ } catch (PackageManager.NameNotFoundException e) {
+ logger.error("Error while updating notification icons", e);
+ imageData = new byte[0];
+ }
+ } else {
+ imageData = new byte[0];
+ }
+ }
+
+ return imageData;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/SyncRequestHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/SyncRequestHandler.java
new file mode 100644
index 000000000..90a436ada
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/SyncRequestHandler.java
@@ -0,0 +1,38 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.ExpectedResponse;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessage;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
+
+public class SyncRequestHandler implements IncomingMessageHandler {
+
+ private final WithingsSteelHRDeviceSupport support;
+
+ public SyncRequestHandler(WithingsSteelHRDeviceSupport support) {
+ this.support = support;
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ support.sendToDevice(new WithingsMessage(WithingsMessageType.SYNC_RESPONSE));
+ support.doSync();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/AncsConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/AncsConstants.java
new file mode 100644
index 000000000..b061ac3c2
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/AncsConstants.java
@@ -0,0 +1,45 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification;
+
+public final class AncsConstants {
+
+ public static final byte EVENT_ID_NOTIFICATION_ADDED = 0;
+ public static final byte EVENT_ID_NOTIFICATION_MODIFIED = 1;
+ public static final byte EVENT_ID_NOTIFICATION_REMOVED = 2;
+
+ public static final byte EVENT_FLAGS_SILENT = (1 << 0);
+ public static final byte EVENT_FLAGS_IMPORTANT = (1 << 1);
+ public static final byte EVENT_FLAGS_PREEXISTING = (1 << 2);
+ public static final byte EVENT_FLAGS_POSITIVE_ACTION = (1 << 3);
+ public static final byte EVENT_FLAGS_NEGATIVE_ACTION = (1 << 4);
+
+ public static final byte CATEGORY_ID_OTHER = 0;
+ public static final byte CATEGORY_ID_INCOMING_CALL = 1;
+ public static final byte CATEGORY_ID_MISSED_CALL = 2;
+ public static final byte CATEGORY_ID_VOICEMAIL = 3;
+ public static final byte CATEGORY_ID_SOCIAL = 4;
+ public static final byte CATEGORY_ID_SCHEDULE = 5;
+ public static final byte CATEGORY_ID_EMAIL = 6;
+ public static final byte CATEGORY_ID_NEWS = 7;
+ public static final byte CATEGORY_ID_HEALTHANDFITNESS = 8;
+ public static final byte CATEGORY_ID_BUSINESSANDFINANCE = 9;
+ public static final byte CATEGORY_ID_LOCATION = 10;
+ public static final byte CATEGORY_ID_ENTERTAINMENT = 11;
+
+ private AncsConstants(){}
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/GetNotificationAttributes.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/GetNotificationAttributes.java
new file mode 100644
index 000000000..760c17a11
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/GetNotificationAttributes.java
@@ -0,0 +1,76 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+
+public class GetNotificationAttributes {
+ private byte commandID;
+ private int notificationUID;
+ private List attributes = new ArrayList<>();
+
+ public byte getCommandID() {
+ return commandID;
+ }
+
+ public void setCommandID(byte commandID) {
+ this.commandID = commandID;
+ }
+
+ public int getNotificationUID() {
+ return notificationUID;
+ }
+
+ public void setNotificationUID(int notificationUID) {
+ this.notificationUID = notificationUID;
+ }
+
+ public List getAttributes() {
+ return Collections.unmodifiableList(attributes);
+ }
+
+ public void addAttribute(RequestedNotificationAttribute attribute) {
+ attributes.add(attribute);
+ }
+
+ public void deserialize(byte[] rawData) {
+ ByteBuffer buffer = ByteBuffer.wrap(rawData);
+ commandID = buffer.get();
+ notificationUID = buffer.getInt();
+ while (buffer.hasRemaining()) {
+ RequestedNotificationAttribute requestedNotificationAttribute = new RequestedNotificationAttribute();
+ int length = 1;
+ if (buffer.remaining() >= 3) {
+ length = 3;
+ }
+
+ byte[] rawAttributeData = new byte[length];
+ buffer.get(rawAttributeData);
+ requestedNotificationAttribute.deserialize(rawAttributeData);
+ attributes.add(requestedNotificationAttribute);
+ }
+ }
+
+ public byte[] serialize() {
+ return new byte[0];
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/GetNotificationAttributesResponse.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/GetNotificationAttributesResponse.java
new file mode 100644
index 000000000..14b7a8bd7
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/GetNotificationAttributesResponse.java
@@ -0,0 +1,57 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.List;
+
+public class GetNotificationAttributesResponse {
+ private byte commandID = 0;
+ private int notificationUID;
+ private List attributes = new ArrayList<>();
+
+ public GetNotificationAttributesResponse(int notificationUID) {
+ this.notificationUID = notificationUID;
+ }
+
+ public void addAttribute(NotificationAttribute attribute) {
+ attributes.add(attribute);
+ }
+
+ public byte[] serialize() {
+ ByteBuffer buffer = ByteBuffer.allocate(getLength());
+ buffer.put(commandID);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+ buffer.putInt(notificationUID);
+ buffer.order(ByteOrder.BIG_ENDIAN);
+ for (NotificationAttribute attribute : attributes) {
+ buffer.put(attribute.serialize());
+ }
+ return buffer.array();
+ }
+
+ private int getLength() {
+ int length = 5;
+ for (NotificationAttribute attribute : attributes) {
+ length += attribute.getAttributeLength() + 3;
+ }
+
+ return length;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationAttribute.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationAttribute.java
new file mode 100644
index 000000000..f5bbdee46
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationAttribute.java
@@ -0,0 +1,88 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.charset.StandardCharsets;
+
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
+
+public class NotificationAttribute {
+ private byte attributeID;
+ private short attributeLength;
+ private short attributeMaxLength;
+ private String value;
+
+ public void setAttributeMaxLength(short attributeMaxLength) {
+ this.attributeMaxLength = attributeMaxLength;
+ }
+
+ public byte getAttributeID() {
+ return attributeID;
+ }
+
+ public void setAttributeID(byte attributeID) {
+ this.attributeID = attributeID;
+ }
+
+ public short getAttributeLength() {
+ short length = (short)(value != null? value.getBytes(StandardCharsets.UTF_8).length : 0);
+ if (attributeMaxLength > 0 && length > attributeMaxLength) {
+ length = attributeMaxLength;
+ }
+
+ return length;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ public byte[] serialize() {
+ attributeLength = getAttributeLength();
+ int length = attributeLength + 3;
+ ByteBuffer buffer = ByteBuffer.allocate(length);
+ buffer.put(attributeID);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+ buffer.putShort(attributeLength);
+ if (value != null) {
+ buffer.order(ByteOrder.BIG_ENDIAN);
+ buffer.put(value.getBytes(StandardCharsets.UTF_8), 0, attributeLength);
+ }
+
+ return buffer.array();
+ }
+
+ public void deserialize(byte[] rawData) {
+ ByteBuffer buffer = ByteBuffer.wrap(rawData);
+ attributeID = buffer.get();
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+ attributeLength = buffer.getShort();
+ buffer.order(ByteOrder.BIG_ENDIAN);
+ if (attributeLength > 0) {
+ byte[] rawValue = new byte[attributeLength];
+ buffer.get(rawValue);
+ value = new String(rawValue, StandardCharsets.UTF_8);
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationProvider.java
new file mode 100644
index 000000000..fe3ab81eb
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationProvider.java
@@ -0,0 +1,184 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class NotificationProvider {
+
+ private static final Logger logger = LoggerFactory.getLogger(NotificationProvider.class);
+ private final WithingsSteelHRDeviceSupport support;
+ private final Map pendingNotifications = new HashMap<>();
+ private static NotificationProvider instance;
+
+ public static NotificationProvider getInstance(WithingsSteelHRDeviceSupport support) {
+ if (instance == null) {
+ instance = new NotificationProvider(support);
+ }
+
+ return instance;
+ }
+
+ private NotificationProvider(WithingsSteelHRDeviceSupport support) {
+ this.support = support;
+ }
+
+ public void notifyClient(NotificationSpec spec) {
+ NotificationSource notificationSource = new NotificationSource(spec.getId(),
+ AncsConstants.EVENT_ID_NOTIFICATION_ADDED,
+ AncsConstants.EVENT_FLAGS_IMPORTANT,
+ mapNotificationType(spec.type),
+ (byte)1);
+ pendingNotifications.put(notificationSource.getNotificationUID(), spec);
+ support.sendAncsNotificationSourceNotification(notificationSource);
+ }
+
+ public void handleNotificationAttributeRequest(GetNotificationAttributes request) {
+ logger.debug("Request has ID: " + request.getNotificationUID());
+ NotificationSpec spec = pendingNotifications.get(request.getNotificationUID());
+ if (spec == null) {
+ logger.info("No pending notification with notificationUID " + request.getNotificationUID());
+ NotificationSource notificationSource = new NotificationSource(request.getNotificationUID(),
+ AncsConstants.EVENT_ID_NOTIFICATION_REMOVED,
+ AncsConstants.EVENT_FLAGS_IMPORTANT,
+ (byte)0,
+ (byte)1);
+ support.sendAncsNotificationSourceNotification(notificationSource);
+ return;
+ }
+
+ GetNotificationAttributesResponse response = new GetNotificationAttributesResponse(request.getNotificationUID());
+ List requestedAttributes = request.getAttributes();
+ logger.debug(requestedAttributes.size() + " attributes requested.");
+
+ boolean complete = false;
+
+ for (RequestedNotificationAttribute requestedAttribute : requestedAttributes) {
+ NotificationAttribute attribute = new NotificationAttribute();
+ attribute.setAttributeID(requestedAttribute.getAttributeID());
+ attribute.setAttributeMaxLength(requestedAttribute.getAttributeMaxLength());
+ logger.debug("Handling attribute " + attribute.getAttributeID() + " with maxLength " + attribute.getAttributeLength());
+ String value = "";
+ if (requestedAttribute.getAttributeID() == 0) {
+ value = spec.sourceAppId;
+ }
+ if (requestedAttribute.getAttributeID() == 1) {
+ complete = true;
+ value = spec.sender != null? spec.sender : (spec.phoneNumber != null? spec.phoneNumber : (spec.sourceName != null? spec.sourceName : "Unknown"));
+ }
+ if (requestedAttribute.getAttributeID() == 2) {
+ complete = true;
+ value = spec.title != null? spec.title : (spec.subject != null? spec.subject : " ");
+ }
+ if (requestedAttribute.getAttributeID() == 3) {
+ complete = true;
+ value = (spec.body != null? spec.body : " ");
+ }
+
+ if (value != null) {
+ // Remove linefeed and carriage returns as the watch cannot display this:
+ value = value.replace("\n", " ");
+ value = value.replace("\r", " ");
+ if (requestedAttribute.getAttributeMaxLength() == 0 || requestedAttribute.getAttributeMaxLength() >= value.length()) {
+ attribute.setValue(value);
+ } else {
+ attribute.setValue(value.substring(0, requestedAttribute.getAttributeMaxLength()));
+ }
+ }
+
+ logger.debug("Sending attribute " + attribute.getAttributeID() + " with value " + attribute.getValue());
+ response.addAttribute(attribute);
+ }
+
+ support.sendAncsDataSourceNotification(response);
+
+ if (complete) {
+ pendingNotifications.remove(request.getNotificationUID());
+ }
+ }
+
+ public NotificationSpec getNotificationSpecForSourceAppId(String sourceAppId) {
+ for (NotificationSpec notificationSpec : pendingNotifications.values()) {
+ if (notificationSpec.sourceAppId != null && notificationSpec.sourceAppId.equalsIgnoreCase(sourceAppId)) {
+ return notificationSpec;
+ }
+ }
+
+ return null;
+ }
+
+ private byte mapNotificationType(NotificationType type) {
+ switch (type) {
+ case GENERIC_ALARM_CLOCK:
+ case BUSINESS_CALENDAR:
+ case GENERIC_CALENDAR:
+ return AncsConstants.CATEGORY_ID_SCHEDULE;
+ case GENERIC_EMAIL:
+ case YAHOO_MAIL:
+ case GOOGLE_INBOX:
+ case GMAIL:
+ case OUTLOOK:
+ return AncsConstants.CATEGORY_ID_EMAIL;
+ case GENERIC_NAVIGATION:
+ return AncsConstants.CATEGORY_ID_LOCATION;
+ case GENERIC_PHONE:
+ return AncsConstants.CATEGORY_ID_INCOMING_CALL;
+ case MAILBOX:
+ return AncsConstants.CATEGORY_ID_MISSED_CALL;
+ case LINE:
+ case RIOT:
+ case SIGNAL:
+ case WIRE:
+ case SKYPE:
+ case SLACK:
+ case SNAPCHAT:
+ case TELEGRAM:
+ case THREEMA:
+ case KONTALK:
+ case ANTOX:
+ case DISCORD:
+ case TRANSIT:
+ case TWITTER:
+ case VIBER:
+ case WECHAT:
+ case WHATSAPP:
+ case FACEBOOK:
+ case FACEBOOK_MESSENGER:
+ case LINKEDIN:
+ case HIPCHAT:
+ case INSTAGRAM:
+ case KAKAO_TALK:
+ case GENERIC_SMS:
+ case GOOGLE_MESSENGER:
+ case GOOGLE_HANGOUTS:
+ return AncsConstants.CATEGORY_ID_SOCIAL;
+ default:
+ return AncsConstants.CATEGORY_ID_OTHER;
+ }
+ }
+
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationSource.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationSource.java
new file mode 100644
index 000000000..e18476fb9
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationSource.java
@@ -0,0 +1,55 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification;
+
+import java.nio.ByteBuffer;
+import java.util.Locale;
+import java.util.Random;
+
+public class NotificationSource {
+ private byte eventID;
+ private byte eventFlags;
+ private byte categoryId;
+ private byte categoryCount;
+ private int notificationUID;
+
+ public NotificationSource(int notificationUID, byte eventID, byte eventFlags, byte categoryId, byte categoryCount) {
+ this.eventID = eventID;
+ this.eventFlags = eventFlags;
+ this.categoryId = categoryId;
+ this.categoryCount = categoryCount;
+ this.notificationUID = Integer.valueOf(new Random().nextInt());
+ }
+
+ public int getNotificationUID() {
+ return notificationUID;
+ }
+
+ void setNotificationUID(int notificationUID) {
+ this.notificationUID = notificationUID;
+ }
+
+ public byte[] serialize() {
+ ByteBuffer buffer = ByteBuffer.allocate(8);
+ buffer.put(eventID);
+ buffer.put(eventFlags);
+ buffer.put(categoryId);
+ buffer.put(categoryCount);
+ buffer.putInt(notificationUID);
+ return buffer.array();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/RequestedNotificationAttribute.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/RequestedNotificationAttribute.java
new file mode 100644
index 000000000..ffd84229f
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/RequestedNotificationAttribute.java
@@ -0,0 +1,58 @@
+/* Copyright (C) 2021 Frank Ertl
+
+ This file is part of Gadgetbridge.
+
+ Gadgetbridge is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published
+ by the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Gadgetbridge is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+public class RequestedNotificationAttribute {
+ private byte attributeID;
+ private short attributeMaxLength;
+
+ public byte getAttributeID() {
+ return attributeID;
+ }
+
+ public void setAttributeID(byte attributeID) {
+ this.attributeID = attributeID;
+ }
+
+ public short getAttributeMaxLength() {
+ return attributeMaxLength;
+ }
+
+ public void setAttributeMaxLength(short attributeMaxLength) {
+ this.attributeMaxLength = attributeMaxLength;
+ }
+
+ public byte[] serialize() {
+ ByteBuffer buffer = ByteBuffer.allocate(3);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+ buffer.put(attributeID);
+ buffer.putShort(attributeMaxLength);
+ return buffer.array();
+ }
+
+ public void deserialize(byte[] rawData) {
+ ByteBuffer buffer = ByteBuffer.wrap(rawData);
+ buffer.order(ByteOrder.LITTLE_ENDIAN);
+ attributeID = buffer.get();
+ if (buffer.capacity() >= 3) {
+ attributeMaxLength = buffer.getShort();
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java
index c16ef4461..7faffc1d8 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java
@@ -146,6 +146,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWF1000XM3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWF1000XM4Coordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsSteelHRDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xwatch.XWatchCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
@@ -379,7 +380,7 @@ public class DeviceHelper {
result.add(new AsteroidOSDeviceCoordinator());
result.add(new SoFlowCoordinator());
result.add(new VivomoveHrCoordinator());
-
+ result.add(new WithingsSteelHRDeviceCoordinator());
return result;
}
diff --git a/app/src/main/res/layout/activity_withings_calibration.xml b/app/src/main/res/layout/activity_withings_calibration.xml
new file mode 100644
index 000000000..c6fa3469e
--- /dev/null
+++ b/app/src/main/res/layout/activity_withings_calibration.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 0802bd968..ad96377d8 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -1112,6 +1112,93 @@
indoor_ice_skating
+
+ @string/activity_type_outdoor_running
+ @string/activity_type_hiking
+ @string/activity_type_biking
+ @string/activity_type_swimming
+ @string/activity_type_surfing
+ @string/activity_type_windsurfing
+ @string/activity_type_kitesurfing
+ @string/activity_type_tennis
+ @string/activity_type_pingpong
+ @string/activity_type_squash
+ @string/activity_type_badminton
+ @string/activity_type_basketball
+ @string/activity_type_soccer
+ @string/activity_type_football
+ @string/activity_type_rugby
+ @string/activity_type_volleyball
+ @string/activity_type_baseball
+ @string/activity_type_handball
+ @string/activity_type_golf
+ @string/activity_type_gymnastics
+ @string/activity_type_elliptical
+ @string/activity_type_pilates
+ @string/activity_type_yoga
+ @string/activity_type_dancing
+ @string/activity_type_zumba
+ @string/activity_type_weightlifting
+ @string/activity_type_boxing
+ @string/activity_type_skiing
+ @string/activity_type_snowboarding
+ @string/activity_type_rowing_machine
+ @string/activity_type_hockey
+ @string/activity_type_icehockey
+ @string/activity_type_climbing
+ @string/activity_type_iceskating
+ @string/activity_type_riding
+ @string/activity_type_other
+
+
+
+ running
+ hiking
+ biking
+ swimming
+ surfing
+ windsurfing
+ kitesurfing
+ tennis
+ pingpong
+ squash
+ badminton
+ basketball
+ soccer
+ football
+ rugby
+ volleyball
+ baseball
+ handball
+ golf
+ gymnastics
+ elliptical
+ pilates
+ yoga
+ dancing
+ zumba
+ weightlifting
+ boxing
+ skiing
+ snowboarding
+ rowing_machine
+ hockey
+ icehockey
+ climbing
+ iceskating
+ riding
+ other
+
+
+
+ running
+ hiking
+ biking
+ swimming
+ climbing
+ other
+
+
@string/menuitem_pai@string/menuitem_dnd
diff --git a/app/src/main/res/values/rc_attrs.xml b/app/src/main/res/values/rc_attrs.xml
new file mode 100644
index 000000000..d98b25ef2
--- /dev/null
+++ b/app/src/main/res/values/rc_attrs.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 569497aea..97bbca478 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1170,6 +1170,9 @@
Outdoor RunningWalkingIndoor Walking
+ Surfing
+ Windsurfing
+ KitesurfingFreestyleHikingClimbing
@@ -1183,12 +1186,19 @@
Jumping RopeYogaSoccer
+ Football
+ RugbyRowing MachineRowingCricket
+ BaseballBasketball
+ Handball
+ TennisPing Pong
+ SquashBadminton
+ WeightliftingStrength TrainingDanceIndoor Fitness
@@ -1206,6 +1216,15 @@
Street DanceZumbaIndoor Ice Skating
+ Dancing
+ Skiing
+ Snowboarding
+ Horseback Riding
+ Hockey
+ Icehockey
+ Ice Skating
+ Golfing
+ OtherUnknown activitySport ActivitiesSport Activity Detail
@@ -2038,6 +2057,7 @@
On-device pairing confirmations can get annoying. Disabling them might lose you functionality.VESCBose QC35
+ Withings Steel HRDisabledMedia PlayMedia Pause
@@ -2192,4 +2212,9 @@
Gadgetbridge needs read permissions on Catima cards to sync them. Tap this button to grant them.Loyalty CardsSyncing %d loyalty cards to device
+ Please use the dial below to align the hour hand to the 12.
+ Now use the dial to align the minute hand to the 12.
+ Finally align the activity hand to 100%. Please be aware that this hand only moves clockwise.
+ Previous
+ Next
diff --git a/app/src/main/res/xml/devicesettings_withingssteelhr.xml b/app/src/main/res/xml/devicesettings_withingssteelhr.xml
new file mode 100644
index 000000000..8a49c46a6
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_withingssteelhr.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationServiceTestCase.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationServiceTestCase.java
index 85cbca92b..ebd87b2ff 100644
--- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationServiceTestCase.java
+++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationServiceTestCase.java
@@ -1,5 +1,9 @@
package nodomain.freeyourgadget.gadgetbridge.service;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_BODY;
+
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -17,10 +21,6 @@ import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.test.TestBase;
import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
-import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_BODY;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
public class DeviceCommunicationServiceTestCase extends TestBase {
private static final java.lang.String TEST_DEVICE_ADDRESS = TestDeviceSupport.class.getName();
diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/MessageBuilderTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/MessageBuilderTest.java
new file mode 100644
index 000000000..14d2c537f
--- /dev/null
+++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/MessageBuilderTest.java
@@ -0,0 +1,151 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.BatteryValues;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.MessageBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.MessageFactory;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessage;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class MessageBuilderTest {
+
+ @Mock
+ private MessageFactory messageFactoryMock;
+
+ @Mock
+ private WithingsSteelHRDeviceSupport supportMock;
+
+ private MessageBuilder messageBuilder2Test;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ messageBuilder2Test = new MessageBuilder(supportMock, messageFactoryMock);
+ }
+
+ @Test
+ public void testUnresolveableMessage() {
+ // arrange
+ byte[] data = GB.hexStringToByteArray("143fbcce");
+ when(messageFactoryMock.createMessageFromRawData(data)).thenReturn(null);
+
+ // act
+ boolean result = messageBuilder2Test.buildMessage(data);
+
+ // assert;
+ assertFalse(result);
+ verifyZeroInteractions(supportMock);
+ }
+
+ @Test
+ public void testUnknownMessageType() {
+ // arrange
+ byte[] data = GB.hexStringToByteArray("0103e7000456abcdef");
+ when(messageFactoryMock.createMessageFromRawData(data)).thenReturn(new WithingsMessage((short)999));
+
+ // act
+ boolean result = messageBuilder2Test.buildMessage(data);
+
+ // assert;
+ assertTrue(result);
+ verify(messageFactoryMock, times(1)).createMessageFromRawData(data);
+ verifyZeroInteractions(supportMock);
+ }
+
+ @Test
+ public void testIncompleteMessage() {
+ // arrange
+ byte[] data = GB.hexStringToByteArray("0103e7000856abcdef");
+ when(messageFactoryMock.createMessageFromRawData(data)).thenReturn(new WithingsMessage((short)1286));
+
+ // act
+ boolean result = messageBuilder2Test.buildMessage(data);
+
+ // assert;
+ assertFalse(result);
+ verify(messageFactoryMock, never()).createMessageFromRawData(data);
+ verifyZeroInteractions(supportMock);
+ }
+
+ @Test
+ public void testUnknownMessageInTwoChunks() {
+ // arrange
+ byte[] data1 = GB.hexStringToByteArray("0103e7000856abcdef");
+ byte[] data2 = GB.hexStringToByteArray("56abcdef");
+ byte[] dataComplete= GB.hexStringToByteArray("0103e7000856abcdef56abcdef");
+ when(messageFactoryMock.createMessageFromRawData(dataComplete)).thenReturn(new WithingsMessage((short)1286));
+
+ // act
+ boolean result1 = messageBuilder2Test.buildMessage(data1);
+ boolean result2 = messageBuilder2Test.buildMessage(data2);
+
+ // assert;
+ assertFalse(result1);
+ assertTrue(result2);
+ verify(messageFactoryMock, never()).createMessageFromRawData(data1);
+ verify(messageFactoryMock, never()).createMessageFromRawData(data2);
+ verify(messageFactoryMock, times(1)).createMessageFromRawData(dataComplete);
+ verifyZeroInteractions(supportMock);
+ }
+
+ @Test
+ public void testKnownMessageInTwoChunks() {
+ // arrange
+ byte[] data1 = GB.hexStringToByteArray("010504000856abcdef");
+ byte[] data2 = GB.hexStringToByteArray("56abcdef");
+ byte[] dataComplete= GB.hexStringToByteArray("010504000856abcdef56abcdef");
+ when(messageFactoryMock.createMessageFromRawData(dataComplete)).thenReturn(new WithingsMessage((short)1284));
+
+ // act
+ boolean result1 = messageBuilder2Test.buildMessage(data1);
+ boolean result2 = messageBuilder2Test.buildMessage(data2);
+
+ // assert;
+ assertFalse(result1);
+ assertTrue(result2);
+ verify(messageFactoryMock, never()).createMessageFromRawData(data1);
+ verify(messageFactoryMock, never()).createMessageFromRawData(data2);
+ verify(messageFactoryMock, times(1)).createMessageFromRawData(dataComplete);
+ verifyZeroInteractions(supportMock);
+ }
+
+ @Test
+ public void testKnownMessageWithValidDataInTwoChunks() {
+ // arrange
+ byte[] data1 = GB.hexStringToByteArray("010504000e0504000a4702");
+ byte[] data2 = GB.hexStringToByteArray("00000f0100000000");
+ byte[] dataComplete= GB.hexStringToByteArray("010504000e0504000a470200000f0100000000");
+ Message message = new WithingsMessage((short)1284);
+ BatteryValues batteryValues = new BatteryValues();
+ message.addDataStructure(batteryValues);
+ when(messageFactoryMock.createMessageFromRawData(dataComplete)).thenReturn(message);
+
+ // act
+ boolean result1 = messageBuilder2Test.buildMessage(data1);
+ boolean result2 = messageBuilder2Test.buildMessage(data2);
+
+ // assert;
+ assertFalse(result1);
+ assertTrue(result2);
+ verify(messageFactoryMock, never()).createMessageFromRawData(data1);
+ verify(messageFactoryMock, never()).createMessageFromRawData(data2);
+ verify(messageFactoryMock, times(1)).createMessageFromRawData(dataComplete);
+ verifyNoMoreInteractions(supportMock);
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ChallengeTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ChallengeTest.java
new file mode 100644
index 000000000..023a1b662
--- /dev/null
+++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ChallengeTest.java
@@ -0,0 +1,67 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class ChallengeTest {
+
+ @Test
+ public void testFillFromRawData() {
+ // arrange
+ byte[] rawData = GB.hexStringToByteArray("1130303a32343a65343a36653a34633a38611082f3d9e121f16a5a3cf0ba94261e8ff6");
+ byte[] expectedChallengeBytes = GB.hexStringToByteArray("82f3d9e121f16a5a3cf0ba94261e8ff6");
+ Challenge challenge2Test = new Challenge();
+
+ // act
+ challenge2Test.fillFromRawData(rawData);
+
+ // assert
+ assertEquals("00:24:e4:6e:4c:8a", challenge2Test.getMacAddress());
+ assertArrayEquals(expectedChallengeBytes, challenge2Test.getChallenge());
+ }
+
+ @Test
+ public void testToRawData() {
+ // arrange
+ Challenge challenge2Test = new Challenge();
+ challenge2Test.setMacAddress("00:24:e4:6e:4c:8a");
+ challenge2Test.setChallenge(GB.hexStringToByteArray("82f3d9e121f16a5a3cf0ba94261e8ff6"));
+
+ // act
+ byte[] result = challenge2Test.getRawData();
+
+ // assert
+ assertArrayEquals(GB.hexStringToByteArray("012200231130303a32343a65343a36653a34633a38611082f3d9e121f16a5a3cf0ba94261e8ff6"), result);
+ }
+
+ @Test
+ public void testToRawDataNoMacAddress() {
+ // arrange
+ Challenge challenge2Test = new Challenge();
+ challenge2Test.setChallenge(GB.hexStringToByteArray("82f3d9e121f16a5a3cf0ba94261e8ff6"));
+
+ // act
+ byte[] result = challenge2Test.getRawData();
+
+ // assert
+ assertArrayEquals(GB.hexStringToByteArray("01220012001082f3d9e121f16a5a3cf0ba94261e8ff6"), result);
+ }
+
+ @Test
+ public void testToRawDataNoChallengeBytes() {
+ // arrange
+ Challenge challenge2Test = new Challenge();
+ challenge2Test.setMacAddress("00:24:e4:6e:4c:8a");
+
+ // act
+ byte[] result = challenge2Test.getRawData();
+
+ // assert
+ assertArrayEquals(GB.hexStringToByteArray("012200131130303a32343a65343a36653a34633a386100"), result);
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/DataStructureFactoryTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/DataStructureFactoryTest.java
new file mode 100644
index 000000000..3efe68b0d
--- /dev/null
+++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/DataStructureFactoryTest.java
@@ -0,0 +1,133 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import static org.junit.Assert.*;
+
+import org.bouncycastle.util.encoders.Hex;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
+import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
+
+public class DataStructureFactoryTest {
+
+ private DataStructureFactory factory2Test;
+
+ @Before
+ public void setUp() {
+ factory2Test = new DataStructureFactory();
+ }
+
+ @Test
+ public void testEmptyData() {
+ // arrange
+
+ // act
+ List result = factory2Test.createStructuresFromRawData(new byte[0]);
+
+ // assert
+ assertTrue(result.isEmpty());
+ }
+
+
+ @Test
+ public void testNullData() {
+ // arrange
+
+ // act
+ List result = factory2Test.createStructuresFromRawData(null);
+
+ // assert
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void testOneStructure() {
+ // arrange
+ String dataString = "0504000a470200000f0100000000";
+ byte[] data = Hex.decode(dataString);
+
+ // act
+ List result = factory2Test.createStructuresFromRawData(data);
+
+ // assert
+ assertEquals(1, result.size());
+ BatteryValues batteryValues = (BatteryValues)result.get(0);
+ assertEquals(2, batteryValues.getStatus());
+ assertEquals(71, batteryValues.getPercent());
+ assertEquals(3841, batteryValues.getVolt());
+ }
+
+ @Test
+ public void testTwoStructures() {
+ // arrange
+ String dataString = "0504000a470200000f01000000000504000a350100000e0200000000";
+ byte[] data = Hex.decode(dataString);
+
+ // act
+ List result = factory2Test.createStructuresFromRawData(data);
+
+ // assert
+ assertEquals(2, result.size());
+ BatteryValues batteryValues = (BatteryValues)result.get(0);
+ assertEquals(2, batteryValues.getStatus());
+ assertEquals(71, batteryValues.getPercent());
+ assertEquals(3841, batteryValues.getVolt());
+ batteryValues = (BatteryValues)result.get(1);
+ assertEquals(1, batteryValues.getStatus());
+ assertEquals(53, batteryValues.getPercent());
+ assertEquals(3586, batteryValues.getVolt());
+ }
+
+ @Test
+ public void testTwoStructuresWithAdditionalBytes() {
+ // arrange
+ String dataString = "0504000a470200000f01000000000504000a350100000e0200000000abcdef1234";
+ byte[] data = Hex.decode(dataString);
+
+ // act
+ List result = factory2Test.createStructuresFromRawData(data);
+
+ // assert
+ assertEquals(2, result.size());
+ BatteryValues batteryValues = (BatteryValues)result.get(0);
+ assertEquals(2, batteryValues.getStatus());
+ assertEquals(71, batteryValues.getPercent());
+ assertEquals(3841, batteryValues.getVolt());
+ batteryValues = (BatteryValues)result.get(1);
+ assertEquals(1, batteryValues.getStatus());
+ assertEquals(53, batteryValues.getPercent());
+ assertEquals(3586, batteryValues.getVolt());
+ }
+
+
+
+ @Test
+ public void testMovementData() {
+ // arrange
+ String dataString = "0601000463fbb15806020002003c060a00040008002e0603000e0010000004cf0000000000000000060400020000060b000433971a5a0601000463fbb19406020002003c060a00040006001d0603000e000b000003400000000000000000060400020000060b000435971c5a";
+ byte[] data = Hex.decode(dataString);
+
+ // act
+ List result = factory2Test.createStructuresFromRawData(data);
+
+ // assert
+ assertEquals(12, result.size());
+ assertTrue(result.get(0) instanceof ActivitySampleTime);
+ assertTrue(result.get(1) instanceof ActivitySampleDuration);
+ assertTrue(result.get(2) instanceof ActivitySampleCalories2);
+ assertTrue(result.get(3) instanceof ActivitySampleMovement);
+ assertTrue(result.get(4) instanceof ActivitySampleWalk);
+ assertTrue(result.get(5) instanceof ActivitySampleUnknown);
+ assertTrue(result.get(6) instanceof ActivitySampleTime);
+ assertTrue(result.get(7) instanceof ActivitySampleDuration);
+ assertTrue(result.get(8) instanceof ActivitySampleCalories2);
+ assertTrue(result.get(9) instanceof ActivitySampleMovement);
+ assertTrue(result.get(10) instanceof ActivitySampleWalk);
+ assertTrue(result.get(11) instanceof ActivitySampleUnknown);
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeReplyTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeReplyTest.java
new file mode 100644
index 000000000..ecc6bae94
--- /dev/null
+++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeReplyTest.java
@@ -0,0 +1,32 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class ProbeReplyTest {
+
+ @Test
+ public void testFillFromRawData() {
+ // arrange
+ // this data is a real world example:
+ byte[] rawData = GB.hexStringToByteArray("0000000008537465656c2048521130303a32343a65343a36653a34633a3861103433303765303861643433383531616500ffffff0830303132303132320000001b00001b8100ffffff");
+ ProbeReply probeReply2Test = new ProbeReply();
+
+ // act
+ probeReply2Test.fillFromRawData(rawData);
+
+ // assert
+ assertEquals(0, probeReply2Test.getYetUnknown1());
+ assertEquals("Steel HR", probeReply2Test.getName());
+ assertEquals("00:24:e4:6e:4c:8a", probeReply2Test.getMac());
+ assertEquals("4307e08ad43851ae", probeReply2Test.getSecret());
+ assertEquals(16777215, probeReply2Test.getYetUnknown2());
+ assertEquals("00120122", probeReply2Test.getmId());
+ assertEquals(27, probeReply2Test.getYetUnknown3());
+ assertEquals(7041, probeReply2Test.getFirmwareVersion());
+ assertEquals(16777215, probeReply2Test.getYetUnknown4());
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/TimeTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/TimeTest.java
new file mode 100644
index 000000000..49e0c2407
--- /dev/null
+++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/TimeTest.java
@@ -0,0 +1,33 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.threeten.bp.Instant;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
+
+public class TimeTest {
+
+ @Test
+ public void testGetRawDataWithValues() throws Exception {
+ // arrange
+ Time time = new Time();
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss 'GMT'Z");
+ Date date = sdf.parse("2000-04-01 12:00:00 GMT+0200");
+ time.setNow(Instant.ofEpochMilli(date.getTime()));
+ time.setTimeOffsetInSeconds(7200);
+ date = sdf.parse("2000-10-26 02:00:00 GMT+0200");
+ time.setNextDaylightSavingTransition(Instant.ofEpochMilli(date.getTime()));
+ time.setNextDaylightSavingTransitionOffsetInSeconds(2400);
+
+ // act
+ byte[] rawData = time.getRawData();
+
+ // assert
+ assertEquals("0501001038e5c8a000001c2039f7740000000960", StringUtils.bytesToHex(rawData).toLowerCase());
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsTestStructure.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsTestStructure.java
new file mode 100644
index 000000000..745ec8bc2
--- /dev/null
+++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsTestStructure.java
@@ -0,0 +1,30 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+public class WithingsTestStructure extends WithingsStructure {
+ @Override
+ public short getLength() {
+ return 6;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+
+ }
+
+ @Override
+ public byte[] getRawData() {
+ ByteBuffer rawDataBuffer = ByteBuffer.allocate(getLength());
+ rawDataBuffer.putShort(getType());
+ rawDataBuffer.put("Test".getBytes(StandardCharsets.UTF_8));
+ return rawDataBuffer.array();
+ }
+
+ @Override
+ public short getType() {
+ return 99;
+ }
+}
diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/AbstractMessageTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/AbstractMessageTest.java
new file mode 100644
index 000000000..a4e0aab4d
--- /dev/null
+++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/AbstractMessageTest.java
@@ -0,0 +1,60 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsTestStructure;
+import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
+
+public class AbstractMessageTest {
+
+ @Test
+ public void testGetRawDataNoData() {
+ // arrange
+ Message testMessage = createTestcommand();
+
+ // act
+ byte[] rawData = testMessage.getRawData();
+
+ // assert
+ assertEquals("0100630000", StringUtils.bytesToHex(rawData));
+ }
+
+ @Test
+ public void testGetRawDataWithData() {
+ // arrange
+ Message testMessage = createTestcommand();
+ testMessage.addDataStructure(new WithingsTestStructure());
+
+ // act
+ byte[] rawData = testMessage.getRawData();
+
+ // assert
+ assertEquals("0100630006006354657374", StringUtils.bytesToHex(rawData));
+ }
+
+ private Message createTestcommand() {
+ return new AbstractMessage(){
+ @Override
+ public short getType() {
+ return 99;
+ }
+
+ @Override
+ public boolean needsResponse() {
+ return false;
+ }
+
+ @Override
+ public boolean needsEOT() {
+ return false;
+ }
+
+ @Override
+ public boolean isIncomingMessage() {
+ return false;
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationAttributeTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationAttributeTest.java
new file mode 100644
index 000000000..4bd623f64
--- /dev/null
+++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationAttributeTest.java
@@ -0,0 +1,56 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+import java.nio.charset.StandardCharsets;
+
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class NotificationAttributeTest {
+
+ @Test
+ public void testSerializeDeserialize() {
+ // arrange
+ NotificationAttribute attribute = new NotificationAttribute();
+ attribute.setAttributeID((byte)4);
+ String value = "TestNotificationAttribute";
+ attribute.setValue(value);
+ byte[] rawValue = value.getBytes(StandardCharsets.UTF_8);
+ short expectedLength = (short) rawValue.length;
+
+ // act
+ byte[] result = attribute.serialize();
+
+ // assert
+ NotificationAttribute attribute2 = new NotificationAttribute();
+ attribute2.deserialize(result);
+ assertEquals((byte)4, attribute2.getAttributeID());
+ assertEquals(expectedLength, attribute2.getAttributeLength());
+ assertEquals(value, attribute2.getValue());
+ }
+
+ @Test
+ public void testSerializeDeserializeValueTooLong() {
+ // arrange
+ NotificationAttribute attribute = new NotificationAttribute();
+ attribute.setAttributeID((byte)4);
+ attribute.setAttributeMaxLength((short)4);
+ String value = "TestNotificationAttribute";
+ attribute.setValue(value);
+ byte[] rawValue = value.getBytes(StandardCharsets.UTF_8);
+ short expectedLength = (short) rawValue.length;
+
+ // act
+ byte[] result = attribute.serialize();
+
+ // assert
+ NotificationAttribute attribute2 = new NotificationAttribute();
+ attribute2.deserialize(result);
+ assertEquals((byte)4, attribute2.getAttributeID());
+ assertEquals(4, attribute2.getAttributeLength());
+ assertEquals("Test", attribute2.getValue());
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/RequestedNotificationAttributeTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/RequestedNotificationAttributeTest.java
new file mode 100644
index 000000000..87468c86b
--- /dev/null
+++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/RequestedNotificationAttributeTest.java
@@ -0,0 +1,29 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+
+public class RequestedNotificationAttributeTest {
+
+ @Test
+ public void testSerializeDeserialize() {
+ // arrange
+ RequestedNotificationAttribute attribute = new RequestedNotificationAttribute();
+ attribute.setAttributeID((byte)4);
+ attribute.setAttributeMaxLength((short)19);
+
+ // act
+ byte[] result = attribute.serialize();
+
+ // assert
+ String test = GB.hexdump(result);
+ RequestedNotificationAttribute attribute2 = new RequestedNotificationAttribute();
+ attribute2.deserialize(result);
+ assertEquals((byte)4, attribute2.getAttributeID());
+ assertEquals(19, attribute2.getAttributeMaxLength());
+ }
+
+}
\ No newline at end of file