mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-28 22:34:25 +02:00
Victron SmartShunt: Initial support
This commit is contained in:
Generated
+3
@@ -44,6 +44,7 @@
|
||||
<w>dreamwalker</w>
|
||||
<w>drobnič</w>
|
||||
<w>dulfo</w>
|
||||
<w>duml</w>
|
||||
<w>elagin</w>
|
||||
<w>encryptable</w>
|
||||
<w>eomäe</w>
|
||||
@@ -185,6 +186,7 @@
|
||||
<w>tablename</w>
|
||||
<w>takraw</w>
|
||||
<w>teclast</w>
|
||||
<w>teufel</w>
|
||||
<w>tiparega</w>
|
||||
<w>toleda</w>
|
||||
<w>tomasz</w>
|
||||
@@ -203,6 +205,7 @@
|
||||
<w>vebryn</w>
|
||||
<w>veneziano</w>
|
||||
<w>vibratissimo</w>
|
||||
<w>victron</w>
|
||||
<w>vivomove</w>
|
||||
<w>wakeboarding</w>
|
||||
<w>wakesurfing</w>
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.victron
|
||||
|
||||
import android.content.Context
|
||||
import nodomain.freeyourgadget.gadgetbridge.R
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCardAction
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.victron.VictronSmartShuntSupport
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class VictronSmartShuntCoordinator : AbstractBLEDeviceCoordinator() {
|
||||
override fun getSupportedDeviceName(): Pattern {
|
||||
return Pattern.compile("^SmartShunt [A-Z0-9]+$")
|
||||
}
|
||||
|
||||
override fun getManufacturer(): String {
|
||||
return "Victron"
|
||||
}
|
||||
|
||||
override fun getDeviceSupportClass(device: GBDevice): Class<out DeviceSupport> {
|
||||
return VictronSmartShuntSupport::class.java
|
||||
}
|
||||
|
||||
override fun getDeviceNameResource(): Int {
|
||||
return R.string.devicetype_victron_smartshunt
|
||||
}
|
||||
|
||||
override fun suggestUnbindBeforePair(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getBondingStyle(): Int {
|
||||
// Must be paired for the service to show up
|
||||
return BONDING_STYLE_BOND
|
||||
}
|
||||
|
||||
override fun getDefaultIconResource(): Int {
|
||||
return R.drawable.ic_device_car
|
||||
}
|
||||
|
||||
override fun getDeviceKind(device: GBDevice): DeviceCoordinator.DeviceKind {
|
||||
return DeviceCoordinator.DeviceKind.BATTERY_MONITOR
|
||||
}
|
||||
|
||||
override fun getCustomActions(): List<DeviceCardAction> {
|
||||
return listOf(
|
||||
// Consumed
|
||||
object : DeviceCardAction {
|
||||
override fun getIcon(device: GBDevice): Int {
|
||||
return R.drawable.ic_bolt
|
||||
}
|
||||
|
||||
override fun isVisible(device: GBDevice): Boolean {
|
||||
val value = device.getExtraInfo(EXTRA_CONSUMED) as? String
|
||||
return device.isConnected && !value.isNullOrBlank()
|
||||
}
|
||||
|
||||
override fun getDescription(device: GBDevice, context: Context): String {
|
||||
return context.getString(R.string.consumed_electrical_energy)
|
||||
}
|
||||
|
||||
override fun getLabel(device: GBDevice, context: Context): String {
|
||||
return device.getExtraInfo(EXTRA_CONSUMED) as? String ?: ""
|
||||
}
|
||||
|
||||
override fun onClick(device: GBDevice, context: Context) {
|
||||
// No UI for this yet
|
||||
}
|
||||
},
|
||||
|
||||
// Power
|
||||
object : DeviceCardAction {
|
||||
override fun getIcon(device: GBDevice): Int {
|
||||
return R.drawable.ic_bolt
|
||||
}
|
||||
|
||||
override fun isVisible(device: GBDevice): Boolean {
|
||||
val value = device.getExtraInfo(EXTRA_POWER) as? String
|
||||
return device.isConnected && !value.isNullOrBlank()
|
||||
}
|
||||
|
||||
override fun getDescription(device: GBDevice, context: Context): String {
|
||||
return context.getString(R.string.power_w)
|
||||
}
|
||||
|
||||
override fun getLabel(device: GBDevice, context: Context): String {
|
||||
return device.getExtraInfo(EXTRA_POWER) as? String ?: ""
|
||||
}
|
||||
|
||||
override fun onClick(device: GBDevice, context: Context) {
|
||||
// No UI for this yet
|
||||
}
|
||||
},
|
||||
|
||||
// Current
|
||||
object : DeviceCardAction {
|
||||
override fun getIcon(device: GBDevice): Int {
|
||||
return R.drawable.ic_bolt
|
||||
}
|
||||
|
||||
override fun isVisible(device: GBDevice): Boolean {
|
||||
val value = device.getExtraInfo(EXTRA_CURRENT) as? String
|
||||
return device.isConnected && !value.isNullOrBlank()
|
||||
}
|
||||
|
||||
override fun getDescription(device: GBDevice, context: Context): String {
|
||||
return context.getString(R.string.electrical_current)
|
||||
}
|
||||
|
||||
override fun getLabel(device: GBDevice, context: Context): String {
|
||||
return device.getExtraInfo(EXTRA_CURRENT) as? String ?: ""
|
||||
}
|
||||
|
||||
override fun onClick(device: GBDevice, context: Context) {
|
||||
// No UI for this yet
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_CONSUMED = "consumed"
|
||||
const val EXTRA_POWER = "power"
|
||||
const val EXTRA_CURRENT = "current"
|
||||
}
|
||||
}
|
||||
+2
@@ -255,6 +255,8 @@ public class ActivitySummaryEntries {
|
||||
public static final String UNIT_MM = "unit_millimeter";
|
||||
public static final String UNIT_WATT = "unit_watt";
|
||||
public static final String UNIT_JOULE = "unit_joule";
|
||||
public static final String UNIT_AMPERE = "unit_ampere";
|
||||
public static final String UNIT_AMPERE_HOUR = "unit_ampere_hour";
|
||||
public static final String UNIT_MINUTES_PER_100_METERS = "minutes_100m";
|
||||
public static final String UNIT_SECONDS_PER_100_METERS = "seconds_100m";
|
||||
public static final String UNIT_MINUTES_PER_100_YARDS = "minutes_100yd";
|
||||
|
||||
@@ -198,6 +198,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.gloryfit.watches.Y6Coordinat
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.gree.GreeAcCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.hama.fit6900.HamaFit6900DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.watches.MiBand10ProCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.victron.VictronSmartShuntCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xplora.XploraXmoveDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.EXRIZUK8Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.hplus.HPlusCoordinator;
|
||||
@@ -831,6 +832,7 @@ public enum DeviceType {
|
||||
PIXEL_BUDS_A(PixelBudsACoordinator.class),
|
||||
SHOKZ_OPENSWIM_PRO(ShokzOpenSwimProCoordinator.class),
|
||||
BM2_BATTERY_MONITOR(Bm2Coordinator.class),
|
||||
VICTRON_SMARTSHUNT(VictronSmartShuntCoordinator.class),
|
||||
SINILINK(SinilinkCoordinator.class),
|
||||
ONETOUCH(OneTouchCoordinator.class),
|
||||
SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class),
|
||||
|
||||
+7
@@ -607,6 +607,7 @@ public class BleNamesResolver {
|
||||
mServices.put("6e40fff0-b5a3-f393-e0a9-e50e24dcca9e", "(Propr: NUS - Nordic UART Service)");
|
||||
mServices.put("de5bf728-d711-4e47-af26-65e3012a5dc7", "(Propr: Yawell Serial)");
|
||||
mServices.put("0000ae00-0000-1000-8000-00805f9b34fb", "(Propr: Sinilink-APP)");
|
||||
mServices.put("65970000-4bda-4c1e-af4b-551c4cf74769", "(Propr: Victron)");
|
||||
|
||||
// source https://bitbucket.org/bluetooth-SIG/public/src/main/assigned_numbers/uuids/characteristic_uuids.yaml
|
||||
mCharacteristics.put("00002a00-0000-1000-8000-00805f9b34fb", "Device Name");
|
||||
@@ -1205,6 +1206,12 @@ public class BleNamesResolver {
|
||||
mCharacteristics.put("de5bf72a-d711-4e47-af26-65e3012a5dc7", "(Propr: Yawell Write)");
|
||||
mCharacteristics.put("0000ae04-0000-1000-8000-00805f9b34fb", "(Propr: Sinilink-APP RX)");
|
||||
mCharacteristics.put("0000ae10-0000-1000-8000-00805f9b34fb", "(Propr: Sinilink-APP TX)");
|
||||
mCharacteristics.put("6597ffff-4bda-4c1e-af4b-551c4cf74769", "(Propr: Victron Keep-alive)");
|
||||
mCharacteristics.put("6597eeff-4bda-4c1e-af4b-551c4cf74769", "(Propr: Victron Consumed Ah)");
|
||||
mCharacteristics.put("6597ed8e-4bda-4c1e-af4b-551c4cf74769", "(Propr: Victron Power)");
|
||||
mCharacteristics.put("6597ed8d-4bda-4c1e-af4b-551c4cf74769", "(Propr: Victron Voltage)");
|
||||
mCharacteristics.put("6597ed8c-4bda-4c1e-af4b-551c4cf74769", "(Propr: Victron Current)");
|
||||
mCharacteristics.put("65970fff-4bda-4c1e-af4b-551c4cf74769", "(Propr: Victron Charge)");
|
||||
|
||||
mValueFormats.put(52, "32bit float");
|
||||
mValueFormats.put(50, "16bit float");
|
||||
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.victron
|
||||
|
||||
import android.bluetooth.BluetoothGatt
|
||||
import android.bluetooth.BluetoothGattCharacteristic
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.WorkoutValueFormatter
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.victron.VictronSmartShuntCoordinator
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_AMPERE
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_AMPERE_HOUR
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_WATT
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDeviceSupport
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.util.UUID
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class VictronSmartShuntSupport : AbstractBTLESingleDeviceSupport(LOG) {
|
||||
private val batteryEvent = GBDeviceEventBatteryInfo()
|
||||
private val valueFormatter = WorkoutValueFormatter()
|
||||
|
||||
init {
|
||||
addSupportedService(UUID_SERVICE_VICTRON)
|
||||
}
|
||||
|
||||
override fun useAutoConnect(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun initializeDevice(builder: TransactionBuilder): TransactionBuilder {
|
||||
batteryEvent.level = -1
|
||||
batteryEvent.voltage = -1f
|
||||
device.resetExtraInfos()
|
||||
|
||||
builder.setDeviceState(GBDevice.State.INITIALIZING)
|
||||
builder.notify(UUID_CHARACTERISTIC_CONSUMED, true)
|
||||
builder.notify(UUID_CHARACTERISTIC_POWER, true)
|
||||
builder.notify(UUID_CHARACTERISTIC_VOLTAGE, true)
|
||||
builder.notify(UUID_CHARACTERISTIC_CURRENT, true)
|
||||
builder.notify(UUID_CHARACTERISTIC_CHARGE, true)
|
||||
builder.setDeviceState(GBDevice.State.INITIALIZED)
|
||||
return builder
|
||||
}
|
||||
|
||||
override fun onCharacteristicChanged(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic,
|
||||
value: ByteArray
|
||||
): Boolean {
|
||||
if (handleCharacteristic(characteristic.uuid, value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return super.onCharacteristicChanged(gatt, characteristic, value)
|
||||
}
|
||||
|
||||
override fun onCharacteristicRead(
|
||||
gatt: BluetoothGatt,
|
||||
characteristic: BluetoothGattCharacteristic,
|
||||
value: ByteArray,
|
||||
status: Int
|
||||
): Boolean {
|
||||
if (status != BluetoothGatt.GATT_SUCCESS) {
|
||||
return super.onCharacteristicRead(gatt, characteristic, value, status)
|
||||
}
|
||||
|
||||
if (handleCharacteristic(characteristic.uuid, value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return super.onCharacteristicRead(gatt, characteristic, value, status)
|
||||
}
|
||||
|
||||
fun handleCharacteristic(characteristicUUID: UUID, value: ByteArray): Boolean {
|
||||
val buf = ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN)
|
||||
when (characteristicUUID) {
|
||||
UUID_CHARACTERISTIC_KEEP_ALIVE -> {
|
||||
// type=un16, scale=0.001, unit=seconds
|
||||
val raw = buf.short.toInt() and 0xFFFF
|
||||
if (raw == KEEP_ALIVE_FOREVER) {
|
||||
LOG.debug("Keep-alive: forever")
|
||||
} else {
|
||||
LOG.debug("Keep-alive: {} s", raw * 0.001)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
UUID_CHARACTERISTIC_CONSUMED -> {
|
||||
// type=sn32, scale=0.1, unit=Ah
|
||||
val raw = buf.int
|
||||
val consumedAh = raw * 0.1
|
||||
LOG.debug("Consumed: {} Ah", consumedAh)
|
||||
device.setExtraInfo(VictronSmartShuntCoordinator.EXTRA_CONSUMED, valueFormatter.formatValue(consumedAh, UNIT_AMPERE_HOUR))
|
||||
device.sendDeviceUpdateIntent(context)
|
||||
return true
|
||||
}
|
||||
|
||||
UUID_CHARACTERISTIC_POWER -> {
|
||||
// type=sb16, scale=1, unit=W
|
||||
val raw = buf.short.toInt()
|
||||
if (raw == POWER_NOT_AVAILABLE) {
|
||||
LOG.debug("Power: N/A")
|
||||
device.setExtraInfo(VictronSmartShuntCoordinator.EXTRA_POWER, "")
|
||||
} else {
|
||||
LOG.debug("Power: {} W", raw)
|
||||
device.setExtraInfo(VictronSmartShuntCoordinator.EXTRA_POWER, valueFormatter.formatValue(raw, UNIT_WATT))
|
||||
}
|
||||
device.sendDeviceUpdateIntent(context)
|
||||
return true
|
||||
}
|
||||
|
||||
UUID_CHARACTERISTIC_VOLTAGE -> {
|
||||
// type=sn16, scale=0.01, unit=V
|
||||
val raw = buf.short.toInt()
|
||||
if (raw == VOLTAGE_NOT_AVAILABLE) {
|
||||
LOG.debug("Voltage: N/A")
|
||||
batteryEvent.voltage = -1f
|
||||
} else {
|
||||
val volts = raw * 0.01
|
||||
LOG.debug("Voltage: {} V", volts)
|
||||
batteryEvent.voltage = volts.toFloat()
|
||||
}
|
||||
device.sendDeviceUpdateIntent(context)
|
||||
return true
|
||||
}
|
||||
|
||||
UUID_CHARACTERISTIC_CURRENT -> {
|
||||
// type=sn32, scale=0.001, unit=A
|
||||
val raw = buf.int
|
||||
if (raw == CURRENT_NOT_AVAILABLE) {
|
||||
LOG.debug("Current: N/A")
|
||||
device.setExtraInfo(VictronSmartShuntCoordinator.EXTRA_CURRENT, "")
|
||||
} else {
|
||||
val current = raw * 0.001
|
||||
LOG.debug("Current: {} A", current)
|
||||
device.setExtraInfo(VictronSmartShuntCoordinator.EXTRA_CURRENT, valueFormatter.formatValue(current, UNIT_AMPERE))
|
||||
}
|
||||
device.sendDeviceUpdateIntent(context)
|
||||
return true
|
||||
}
|
||||
|
||||
UUID_CHARACTERISTIC_CHARGE -> {
|
||||
// type=un16, scale=0.01, unit=%
|
||||
val raw = buf.short.toInt() and 0xFFFF
|
||||
if (raw == CHARGE_NOT_AVAILABLE) {
|
||||
LOG.debug("Charge: N/A")
|
||||
batteryEvent.level = GBDevice.BATTERY_UNKNOWN.toInt()
|
||||
} else {
|
||||
LOG.debug("Charge: {} %", raw * 0.01)
|
||||
batteryEvent.level = (raw * 0.01).roundToInt()
|
||||
}
|
||||
evaluateGBDeviceEvent(batteryEvent)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(VictronSmartShuntSupport::class.java)
|
||||
|
||||
/// https://communityarchive.victronenergy.com/questions/93919/victron-bluetooth-ble-protocol-publication.html
|
||||
private val UUID_SERVICE_VICTRON = UUID.fromString("65970000-4bda-4c1e-af4b-551c4cf74769")
|
||||
private val UUID_CHARACTERISTIC_KEEP_ALIVE = UUID.fromString("6597ffff-4bda-4c1e-af4b-551c4cf74769")
|
||||
private val UUID_CHARACTERISTIC_CONSUMED = UUID.fromString("6597eeff-4bda-4c1e-af4b-551c4cf74769")
|
||||
private val UUID_CHARACTERISTIC_POWER = UUID.fromString("6597ed8e-4bda-4c1e-af4b-551c4cf74769")
|
||||
private val UUID_CHARACTERISTIC_VOLTAGE = UUID.fromString("6597ed8d-4bda-4c1e-af4b-551c4cf74769")
|
||||
private val UUID_CHARACTERISTIC_CURRENT = UUID.fromString("6597ed8c-4bda-4c1e-af4b-551c4cf74769")
|
||||
private val UUID_CHARACTERISTIC_CHARGE = UUID.fromString("65970fff-4bda-4c1e-af4b-551c4cf74769")
|
||||
|
||||
private const val KEEP_ALIVE_FOREVER = 0xFFFF
|
||||
private const val POWER_NOT_AVAILABLE = 0x7FFF
|
||||
private const val VOLTAGE_NOT_AVAILABLE = 0x7FFF
|
||||
private const val CURRENT_NOT_AVAILABLE = 0x7FFFFFFF
|
||||
private const val CHARGE_NOT_AVAILABLE = 0xFFFF
|
||||
}
|
||||
}
|
||||
@@ -2705,6 +2705,8 @@
|
||||
<string name="distance_total">Distance Total</string>
|
||||
<string name="activeSeconds">Active</string>
|
||||
<string name="calories_consumed">Calories consumed</string>
|
||||
<string name="consumed_electrical_energy">Consumed energy</string>
|
||||
<string name="electrical_current">Current</string>
|
||||
<string name="mountain_bike_grit_score">Grit score</string>
|
||||
<string name="mountain_bike_flow_score">Flow score</string>
|
||||
<string name="caloriesBurnt">Calories</string>
|
||||
@@ -2896,6 +2898,8 @@
|
||||
<string name="unit_millimeter">mm</string>
|
||||
<string name="unit_watt">Watt</string>
|
||||
<string name="unit_joule">Joule</string>
|
||||
<string name="unit_ampere">A</string>
|
||||
<string name="unit_ampere_hour">Ah</string>
|
||||
<string name="unit_kilojoule">kJ</string>
|
||||
<string name="training_load">Training Load</string>
|
||||
<string name="standing_count">Standing count</string>
|
||||
@@ -4650,6 +4654,7 @@
|
||||
<string name="auth_settings">Authentication settings</string>
|
||||
<string name="devicetype_shokz_openswim_pro" translatable="false">Shokz OpenSwim Pro</string>
|
||||
<string name="devicetype_battery_monitor" translatable="false">Battery Monitor</string>
|
||||
<string name="devicetype_victron_smartshunt" translatable="false">Victron SmartShunt</string>
|
||||
<string name="devicetype_sinilink" translatable="false">Sinilink</string>
|
||||
<string name="devicetype_onetouch" translatable="false">OneTouch Glucose Meter</string>
|
||||
<string name="equalizer_preset_standard">Standard</string>
|
||||
|
||||
@@ -202,6 +202,7 @@ public class BluetoothNameTest extends TestBase {
|
||||
put("BPW4500", DeviceType.BRAUN_BPW4500); // #5886
|
||||
put("MATSON Monitor", DeviceType.BM2_BATTERY_MONITOR); // #6212
|
||||
put("Xiaomi Smart Band 10 Pro AB01", DeviceType.MIBAND10PRO); // #6248
|
||||
put("SmartShunt HQ2303UCHFV", DeviceType.VICTRON_SMARTSHUNT); // #6263
|
||||
}};
|
||||
|
||||
for (Map.Entry<String, DeviceType> e : bluetoothNameToExpectedType.entrySet()) {
|
||||
|
||||
Reference in New Issue
Block a user