From f79930fad998743c64c5dc6c752a8fda23f504f5 Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sun, 15 Dec 2024 00:34:46 +0100 Subject: [PATCH] Marstek B2500: initial support Supported - setting the time - setting/getting the minimum charge of the battery that shall remain - setting/getting 5 time intervals with output wattage and enable/disable switch - getting the firmware version TODO: - check if times overlap - check if we can output less than 80W (offical app only supports 80-800) - research about the "automatic mode" - research about unknown values we get from the two info calls This has been tested with Firmware V220 I only have a very brief capture of V210 which obviously had less feature. I tried to somehow support it but I am not sure --- .../AbstractPreferenceFragment.java | 13 + .../DeviceSettingsPreferenceConst.java | 9 + .../DeviceSpecificSettingsFragment.java | 19 +- .../MarstekB2500DeviceCoordinator.java | 68 +++++ .../gadgetbridge/model/DeviceType.java | 2 + .../marstek/MarstekB2500DeviceSupport.java | 246 ++++++++++++++++++ app/src/main/res/values/strings.xml | 12 + .../devicesettings_battery_discharge_5.xml | 169 ++++++++++++ .../devicesettings_battery_minimum_charge.xml | 11 + 9 files changed, 548 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/marstek/MarstekB2500DeviceCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/marstek/MarstekB2500DeviceSupport.java create mode 100644 app/src/main/res/xml/devicesettings_battery_discharge_5.xml create mode 100644 app/src/main/res/xml/devicesettings_battery_minimum_charge.xml diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractPreferenceFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractPreferenceFragment.java index d35aeb9e3..9824d13c5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractPreferenceFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AbstractPreferenceFragment.java @@ -18,6 +18,7 @@ package nodomain.freeyourgadget.gadgetbridge.activities; import android.content.SharedPreferences; import android.os.Bundle; +import android.text.InputType; import android.text.TextUtils; import androidx.annotation.NonNull; @@ -53,6 +54,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.XTimePreferenceFragment; import nodomain.freeyourgadget.gadgetbridge.util.dialogs.MaterialEditTextPreferenceDialogFragment; import nodomain.freeyourgadget.gadgetbridge.util.dialogs.MaterialListPreferenceDialogFragment; import nodomain.freeyourgadget.gadgetbridge.util.dialogs.MaterialMultiSelectListPreferenceDialogFragment; +import nodomain.freeyourgadget.gadgetbridge.util.preferences.MinMaxTextWatcher; public abstract class AbstractPreferenceFragment extends PreferenceFragmentCompat { protected static final Logger LOG = LoggerFactory.getLogger(AbstractPreferenceFragment.class); @@ -142,6 +144,17 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragmentCompa } } + public void setNumericInputTypeWithRangeFor(final String preferenceKey, int min, int max, boolean allowEmpty) { + final EditTextPreference textPreference = findPreference(preferenceKey); + if (textPreference != null) { + textPreference.setOnBindEditTextListener(editText -> { + editText.setInputType(InputType.TYPE_CLASS_NUMBER); + editText.addTextChangedListener(new MinMaxTextWatcher(editText, min, max, allowEmpty)); + editText.setSelection(editText.getText().length()); + }); + } + } + /** * Reload the preferences in the current screen. This is needed when the user enters or exists a PreferenceScreen, * otherwise the settings won't be reloaded by the {@link SharedPreferencesChangeHandler}, as the preferences return diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java index 9e6fb8056..4668026bd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java @@ -589,4 +589,13 @@ public class DeviceSettingsPreferenceConst { public static final String PREFS_KEY_DEVICE_BLE_API_DEVICE_READ_WRITE = "prefs_device_ble_api_characteristic_read_write"; public static final String PREFS_KEY_DEVICE_BLE_API_DEVICE_NOTIFY = "prefs_device_ble_api_characteristic_notify"; public static final String PREFS_KEY_DEVICE_BLE_API_PACKAGE = "prefs_device_ble_api_package"; + + public static final String PREF_BATTERY_DISCHARGE_INTERVAL1_WATT = "battery_discharge_interval1_watt"; + public static final String PREF_BATTERY_DISCHARGE_INTERVAL2_WATT = "battery_discharge_interval2_watt"; + public static final String PREF_BATTERY_DISCHARGE_INTERVAL3_WATT = "battery_discharge_interval3_watt"; + public static final String PREF_BATTERY_DISCHARGE_INTERVAL4_WATT = "battery_discharge_interval4_watt"; + public static final String PREF_BATTERY_DISCHARGE_INTERVAL5_WATT = "battery_discharge_interval5_watt"; + public static final String PREF_BATTERY_DISCHARGE_INTERVALS_SET = "battery_discharge_intervals_set"; + public static final String PREF_BATTERY_DISCHARGE_MANAUAL = "battery_discharge_manual"; + public static final String PREF_BATTERY_MINIMUM_CHARGE = "battery_minimum_charge"; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java index aea295647..7d11b87d5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java @@ -42,6 +42,7 @@ import android.media.AudioManager; import android.os.Bundle; import android.text.InputType; +import androidx.annotation.NonNull; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.preference.EditTextPreference; import androidx.preference.ListPreference; @@ -882,6 +883,16 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i addPreferenceHandlerFor("lock"); + addPreferenceHandlerFor(PREF_BATTERY_MINIMUM_CHARGE); + + final Preference dischargeIntervalsSet = findPreference(PREF_BATTERY_DISCHARGE_INTERVALS_SET); + if (dischargeIntervalsSet != null) { + dischargeIntervalsSet.setOnPreferenceClickListener(preference -> { + notifyPreferenceChanged(PREF_BATTERY_DISCHARGE_INTERVALS_SET); + return true; + }); + } + String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF); boolean sleepTimeScheduled = sleepTimeState.equals(PREF_DO_NOT_DISTURB_SCHEDULED); @@ -1203,6 +1214,12 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i setInputTypeFor(DeviceSettingsPreferenceConst.PREF_BANGLEJS_TEXT_BITMAP_SIZE, InputType.TYPE_CLASS_NUMBER); setInputTypeFor(DeviceSettingsPreferenceConst.PREF_AUTO_REPLY_INCOMING_CALL_DELAY, InputType.TYPE_CLASS_NUMBER); setInputTypeFor("hplus_screentime", InputType.TYPE_CLASS_NUMBER); + setNumericInputTypeWithRangeFor(DeviceSettingsPreferenceConst.PREF_BATTERY_DISCHARGE_INTERVAL1_WATT, 80, 800, false); + setNumericInputTypeWithRangeFor(DeviceSettingsPreferenceConst.PREF_BATTERY_DISCHARGE_INTERVAL2_WATT, 80, 800, false); + setNumericInputTypeWithRangeFor(DeviceSettingsPreferenceConst.PREF_BATTERY_DISCHARGE_INTERVAL3_WATT, 80, 800, false); + setNumericInputTypeWithRangeFor(DeviceSettingsPreferenceConst.PREF_BATTERY_DISCHARGE_INTERVAL4_WATT, 80, 800, false); + setNumericInputTypeWithRangeFor(DeviceSettingsPreferenceConst.PREF_BATTERY_DISCHARGE_INTERVAL5_WATT, 80, 800, false); + setNumericInputTypeWithRangeFor(PREF_BATTERY_MINIMUM_CHARGE, 0, 100, false); new PasswordCapabilityImpl().registerPreferences(getContext(), coordinator.getPasswordCapability(), this); new HeartRateCapability().registerPreferences(getContext(), coordinator.getHeartRateMeasurementIntervals(), this); @@ -1382,7 +1399,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i DeviceSpecificSettingsScreen.DEVELOPER, R.xml.devicesettings_settings_third_party_apps ); - if(coordinator.getConnectionType().usesBluetoothLE()) { + if (coordinator.getConnectionType().usesBluetoothLE()) { deviceSpecificSettings.addRootScreen( DeviceSpecificSettingsScreen.DEVELOPER, R.xml.devicesettings_ble_api diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/marstek/MarstekB2500DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/marstek/MarstekB2500DeviceCoordinator.java new file mode 100644 index 000000000..e4187f600 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/marstek/MarstekB2500DeviceCoordinator.java @@ -0,0 +1,68 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.marstek; + + +import androidx.annotation.NonNull; + + +import java.util.regex.Pattern; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.marstek.MarstekB2500DeviceSupport; + + +public class MarstekB2500DeviceCoordinator extends AbstractDeviceCoordinator { + @Override + public int getDeviceNameResource() { + return R.string.devicetype_marstek_b2500; + } + + @Override + public int getDefaultIconResource() { + return R.drawable.ic_device_vesc; + } + + @Override + public int getDisabledIconResource() { + return R.drawable.ic_device_vesc_disabled; + } + + @Override + public String getManufacturer() { + return "Marstek"; + } + + @NonNull + @Override + public Class getDeviceSupportClass() { + return MarstekB2500DeviceSupport.class; + } + + @Override + protected Pattern getSupportedDeviceName() { + return Pattern.compile("HM_B2500_.*"); + } + + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + + } + + @Override + public int getBondingStyle() { + return BONDING_STYLE_NONE; + } + + @Override + public int[] getSupportedDeviceSpecificSettings(GBDevice device) { + return new int[]{ + R.xml.devicesettings_battery_minimum_charge, + R.xml.devicesettings_battery_discharge_5 + }; + } +} 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 7d39f73a2..4b25f2835 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -218,6 +218,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.lefun.VivitarHrBpMonitorActi import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watchxplus.WatchXPlusDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.marstek.MarstekB2500DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd.MijiaLywsd02Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd.MijiaLywsd03Coordinator; @@ -581,6 +582,7 @@ public enum DeviceType { SCANNABLE(ScannableDeviceCoordinator.class), CYCLING_SENSOR(CyclingSensorCoordinator.class), BLE_GATT_CLIENT(BleGattClientCoordinator.class), + MARSTEK_B2500(MarstekB2500DeviceCoordinator.class), TEST(TestDeviceCoordinator.class); private DeviceCoordinator coordinator; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/marstek/MarstekB2500DeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/marstek/MarstekB2500DeviceSupport.java new file mode 100644 index 000000000..5213c0cf9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/marstek/MarstekB2500DeviceSupport.java @@ -0,0 +1,246 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.marstek; + +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BATTERY_DISCHARGE_INTERVALS_SET; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BATTERY_DISCHARGE_MANAUAL; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_BATTERY_MINIMUM_CHARGE; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.SharedPreferences; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.time.LocalTime; +import java.util.Calendar; +import java.util.SimpleTimeZone; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; + + +public class MarstekB2500DeviceSupport extends AbstractBTLEDeviceSupport { + public static final UUID UUID_CHARACTERISTIC_MAIN = UUID.fromString("0000ff02-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_SERVICE_MAIN = UUID.fromString("0000ff00-0000-1000-8000-00805f9b34fb"); + + private static final byte COMMAND_PREFIX = 0x73; + private static final byte[] COMMAND_GET_INFOS = new byte[]{COMMAND_PREFIX, 0x06, 0x23, 0x03, 0x01, 0x54}; + private static final byte[] COMMAND_GET_INFOS2 = new byte[]{COMMAND_PREFIX, 0x06, 0x23, 0x13, 0x00, 0x45}; + + private static final Logger LOG = LoggerFactory.getLogger(MarstekB2500DeviceSupport.class); + private int firmwareVersion; + + public MarstekB2500DeviceSupport() { + super(LOG); + addSupportedService(UUID_SERVICE_MAIN); + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + super.onCharacteristicChanged(gatt, characteristic); + + UUID characteristicUUID = characteristic.getUuid(); + byte[] value = characteristic.getValue(); + + LOG.info("Characteristic changed UUID: {}", characteristicUUID); + LOG.info("Characteristic changed value: {}", StringUtils.bytesToHex(value)); + + if (value[0] == COMMAND_PREFIX) { + if ((value[1] == 0x10) && (value[2] == 0x23) && (value[3] == 0x03)) { + firmwareVersion = value[12]; + getDevice().setFirmwareVersion("V" + (firmwareVersion & 0xff)); + getDevice().sendDeviceUpdateIntent(getContext()); + + int battery_minimum_charge = 100 - value[18]; + Prefs devicePrefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress())); + SharedPreferences.Editor devicePrefsEdit = devicePrefs.getPreferences().edit(); + devicePrefsEdit.putString(PREF_BATTERY_MINIMUM_CHARGE, String.valueOf(battery_minimum_charge)); + devicePrefsEdit.apply(); + devicePrefsEdit.commit(); + return true; + } else if ((value[1] == 0x3a || value[1] == 0x22) && value[2] == 0x23 && value[3] == 0x13) { + decodeDischargeIntervalsToPreferences(value); + return true; + } + } + return false; + } + + @Override + public void onTestNewFunction() { + sendCommand("test", COMMAND_GET_INFOS); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + getDevice().setFirmwareVersion("N/A"); + getDevice().setFirmwareVersion2("N/A"); + builder.requestMtu(512); + builder.notify(getCharacteristic(UUID_CHARACTERISTIC_MAIN), true); + builder.wait(800); + builder.write(getCharacteristic(UUID_CHARACTERISTIC_MAIN), COMMAND_GET_INFOS); + builder.wait(800); + builder.write(getCharacteristic(UUID_CHARACTERISTIC_MAIN), COMMAND_GET_INFOS2); + builder.wait(800); + builder.write(getCharacteristic(UUID_CHARACTERISTIC_MAIN), encodeSetCurrentTime()); + builder.wait(800); + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); + return builder; + } + + @Override + public boolean useAutoConnect() { + return false; + } + + private void sendCommand(String taskName, byte[] contents) { + TransactionBuilder builder = new TransactionBuilder(taskName); + BluetoothGattCharacteristic characteristic = getCharacteristic(UUID_CHARACTERISTIC_MAIN); + if (characteristic != null && contents != null) { + builder.write(characteristic, contents); + builder.wait(800); + builder.queue(getQueue()); + } + } + + @Override + public void onSetTime() { + sendCommand("set time", encodeSetCurrentTime()); + } + + @Override + public void onSendConfiguration(final String config) { + if (config.equals(PREF_BATTERY_DISCHARGE_INTERVALS_SET)) { + sendCommand("set discharge intervals", encodeDischargeIntervalsFromPreferences()); + return; + } else if (config.equals(PREF_BATTERY_MINIMUM_CHARGE)) { + sendCommand("set minimum charge", encodeMinimumChargeFromPreferences()); + return; + } + + LOG.warn("Unknown config changed: {}", config); + } + + private void decodeDischargeIntervalsToPreferences(byte[] values) { + Prefs devicePrefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress())); + SharedPreferences.Editor devicePrefsEdit = devicePrefs.getPreferences().edit(); + ByteBuffer buf = ByteBuffer.wrap(values); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.position(5); // skip + for (int i = 1; i <= 5; i++) { + boolean enabled = buf.get() != 0x00; + int startHour = buf.get(); + int startMinute = buf.get(); + int endHour = buf.get(); + int endMinute = buf.get(); + int watt = buf.getShort(); + devicePrefsEdit.putBoolean("battery_discharge_interval" + i + "_enabled", enabled); + devicePrefsEdit.putString("battery_discharge_interval" + i + "_start", DateTimeUtils.formatTime(startHour, startMinute)); + devicePrefsEdit.putString("battery_discharge_interval" + i + "_end", DateTimeUtils.formatTime(endHour, endMinute)); + devicePrefsEdit.putString("battery_discharge_interval" + i + "_watt", String.valueOf(watt)); + + if (i == 3) { + if (values.length == 0x22) // old fw only seems to return 3 settings and has 7 trailing bytes + break; + buf.position(buf.position() + 17); // skip 17 bytes, there is a hole with unknown data + } + } + devicePrefsEdit.apply(); + devicePrefsEdit.commit(); + } + + private byte[] encodeMinimumChargeFromPreferences() { + Prefs devicePrefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress())); + int minimum_charge = devicePrefs.getInt(PREF_BATTERY_MINIMUM_CHARGE, 10); + int maximum_use = 100 - minimum_charge; + + byte length = 6; + ByteBuffer buf = ByteBuffer.allocate(length); + buf.order(ByteOrder.LITTLE_ENDIAN); + + buf.put(COMMAND_PREFIX); + buf.put(length); + buf.put((byte) 0x23); + buf.put((byte) 0x0b); + buf.put((byte) maximum_use); + buf.put(getXORChecksum(buf.array())); + + return buf.array(); + } + + private byte[] encodeSetCurrentTime() { + long ts = System.currentTimeMillis(); + long ts_offset = (SimpleTimeZone.getDefault().getOffset(ts)); + + byte length = 13; + ByteBuffer buf = ByteBuffer.allocate(length); + buf.order(ByteOrder.LITTLE_ENDIAN); + + buf.put(COMMAND_PREFIX); + buf.put(length); + buf.put((byte) 0x23); + buf.put((byte) 0x14); + + final Calendar calendar = DateTimeUtils.getCalendarUTC(); + buf.put((byte) ((calendar.get(Calendar.YEAR) - 1900) & 0xff)); + buf.put((byte) calendar.get(Calendar.MONTH)); + buf.put((byte) calendar.get(Calendar.DAY_OF_MONTH)); + buf.put((byte) calendar.get(Calendar.HOUR_OF_DAY)); + buf.put((byte) calendar.get(Calendar.MINUTE)); + buf.put((byte) calendar.get(Calendar.SECOND)); + buf.putShort((short) (ts_offset / 60000)); + + buf.put(getXORChecksum(buf.array())); + return buf.array(); + } + + private byte[] encodeDischargeIntervalsFromPreferences() { + Prefs devicePrefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress())); + if (devicePrefs.getBoolean(PREF_BATTERY_DISCHARGE_MANAUAL, true)) { + int nr_invervals = (firmwareVersion >= 220) ? 5 : 3; // old firmware V210 only had 3 intervals it seems, so set only 3 + int length = 5 + nr_invervals * 7; + + ByteBuffer buf = ByteBuffer.allocate(length); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.put(COMMAND_PREFIX); + buf.put((byte) length); + buf.put((byte) 0x23); // set power parameters ? + buf.put((byte) 0x12); // set discharge power timers ? + for (int i = 1; i <= nr_invervals; i++) { + boolean enabled = devicePrefs.getBoolean("battery_discharge_interval" + i + "_enabled", false); + LocalTime startTime = devicePrefs.getLocalTime("battery_discharge_interval" + i + "_start", "00:00"); + LocalTime endTime = devicePrefs.getLocalTime("battery_discharge_interval" + i + "_end", "00:00"); + short watt = (short) devicePrefs.getInt("battery_discharge_interval" + i + "_watt", 80); + buf.put((byte) (enabled ? 0x01 : 0x00)); + buf.put((byte) startTime.getHour()); + buf.put((byte) startTime.getMinute()); + buf.put((byte) endTime.getHour()); + buf.put((byte) endTime.getMinute()); + buf.putShort(watt); + } + buf.put(getXORChecksum(buf.array())); + + return buf.array(); + } + return null; + } + + private byte getXORChecksum(byte[] command) { + byte checksum = 0; + for (byte b : command) { + checksum ^= b; + } + return checksum; + } + +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9b0aa94c0..6b2dffa70 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3557,4 +3557,16 @@ Query all packages Reading names and icons of all installed apps Build details copied to clipboard + Marstek B2500 + Battery Discharge Intervals + Discharge Interval 1 + Discharge Interval 2 + Discharge Interval 3 + Discharge Interval 4 + Discharge Interval 5 + Power in W + when disabled, this assumes intelligent discharge controlled by an external power meter (not supported by Gadgetbridge) + Manual Discharge Intervals + Send configuration below to device + Minimum allowed charge in % diff --git a/app/src/main/res/xml/devicesettings_battery_discharge_5.xml b/app/src/main/res/xml/devicesettings_battery_discharge_5.xml new file mode 100644 index 000000000..031cf96a7 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_battery_discharge_5.xml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/devicesettings_battery_minimum_charge.xml b/app/src/main/res/xml/devicesettings_battery_minimum_charge.xml new file mode 100644 index 000000000..a750a1086 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_battery_minimum_charge.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file