Marstek B2500: add status activity which shows current inputs, outputs and charge level

This commit is contained in:
Andreas Shimokawa 2024-12-21 23:13:18 +01:00
parent f77a60a14c
commit 6c8950209d
5 changed files with 228 additions and 3 deletions

View File

@ -868,6 +868,11 @@
android:name=".devices.supercars.ControlActivity"
android:exported="true" />
<activity
android:name=".devices.marstek.SolarEquipmentStatusActivity"
android:label="Solar Equipment Status"
android:exported="true" />
<activity
android:name=".devices.binary_sensor.activity.DataActivity"
android:exported="true" />

View File

@ -17,14 +17,15 @@
package nodomain.freeyourgadget.gadgetbridge.devices.marstek;
import androidx.annotation.NonNull;
import android.app.Activity;
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.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
@ -69,6 +70,16 @@ public class MarstekB2500DeviceCoordinator extends AbstractDeviceCoordinator {
}
@Override
public boolean supportsAppsManagement(final GBDevice device) {
return true;
}
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return SolarEquipmentStatusActivity.class;
}
@Override
public int getBondingStyle() {
return BONDING_STYLE_NONE;

View File

@ -0,0 +1,170 @@
package nodomain.freeyourgadget.gadgetbridge.devices.marstek;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.core.content.ContextCompat;
import androidx.gridlayout.widget.GridLayout;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.card.MaterialCardView;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.dashboard.GaugeDrawer;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class SolarEquipmentStatusActivity extends AbstractGBActivity {
public static String ACTION_SEND_SOLAR_EQUIPMENT_STATUS = "send_solar_equipment_status";
public static String EXTRA_BATTERY_PCT = "battery_pct";
public static String EXTRA_PANEL1_WATT = "panel1_watt";
public static String EXTRA_PANEL2_WATT = "panel2_watt";
public static String EXTRA_OUTPUT1_WATT = "output1_watt";
public static String EXTRA_OUTPUT2_WATT = "output2_watt";
private final Map<String, View> widgetMap = new HashMap<>();
private GridLayout gridLayout;
private SwipeRefreshLayout swipeLayout;
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (Objects.requireNonNull(action).equals(ACTION_SEND_SOLAR_EQUIPMENT_STATUS)) {
Bundle extras = intent.getExtras();
if (extras != null) {
int battery_pct = extras.getInt(EXTRA_BATTERY_PCT);
int panel1_watt = extras.getInt(EXTRA_PANEL1_WATT);
int panel2_watt = extras.getInt(EXTRA_PANEL2_WATT);
int output1_watt = extras.getInt(EXTRA_OUTPUT1_WATT);
int output2_watt = extras.getInt(EXTRA_OUTPUT2_WATT);
updateWidget("battery", battery_pct + "%", (float) (battery_pct / 100.0));
updateWidget("panel1", panel1_watt + "W", (float) (panel1_watt / 380.0));
updateWidget("panel2", panel2_watt + "W", (float) (panel1_watt / 380.0));
updateWidget("output1", output1_watt + "W", (float) (output1_watt / 400.0));
updateWidget("output2", output2_watt + "W", (float) (output1_watt / 400.0));
swipeLayout.setRefreshing(false);
}
}
}
};
private GBDevice gBDevice = null;
public static int[] getColors() {
return new int[]{
ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_poor_color),
ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_fair_color),
ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_good_color),
};
}
public static int[] getColorsOutput() {
return new int[]{
ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_good_color),
ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_fair_color),
ContextCompat.getColor(GBApplication.getContext(), R.color.vo2max_value_poor_color),
};
}
public static float[] getSegments() {
return new float[]{
0.1f,
0.2f,
0.7f,
};
}
public static float[] getSegmentsOutput() {
return new float[]{
0.33f,
0.33f,
0.34f,
};
}
private void updateWidget(String name, String value, float gaugeFill) {
View view = widgetMap.get(name);
if (view != null) {
TextView gaugeValue = view.findViewById(R.id.gauge_value);
gaugeValue.setText(value);
GaugeDrawer gaugeDrawer = new GaugeDrawer();
ImageView gaugeBar = view.findViewById(R.id.gauge_bar);
gaugeDrawer.drawSegmentedGauge(gaugeBar, name.startsWith("output") ? getColorsOutput() : getColors(), name.startsWith("output") ? getSegmentsOutput() : getSegments(), gaugeFill, true, false);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
Bundle extras = getIntent().getExtras();
if (extras != null) {
gBDevice = extras.getParcelable(GBDevice.EXTRA_DEVICE);
}
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_solar_equipment_status);
gridLayout = findViewById(R.id.solarequipmentview_gridlayout);
createWidget("panel1", "Panel 1", 1);
createWidget("panel2", "Panel 2", 1);
createWidget("battery", "Battery", 2);
createWidget("output1", "Output 1", 1);
createWidget("output2", "Output 2", 1);
// Set pull-down-to-refresh action
swipeLayout = findViewById(R.id.solarequipmentview_swipe_layout);
swipeLayout.setOnRefreshListener(() -> {
if (gBDevice == null || !gBDevice.isInitialized()) {
swipeLayout.setRefreshing(false);
GB.toast(getString(R.string.info_no_devices_connected), Toast.LENGTH_LONG, GB.WARN);
return;
}
GBApplication.deviceService(gBDevice).onFetchRecordedData(0);
});
IntentFilter filterLocal = new IntentFilter();
filterLocal.addAction(ACTION_SEND_SOLAR_EQUIPMENT_STATUS);
LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal);
}
private void createWidget(String name, String label, int columnSpan) {
final float scale = getResources().getDisplayMetrics().density;
GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(
GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f),
GridLayout.spec(GridLayout.UNDEFINED, columnSpan, GridLayout.FILL, 1f)
);
layoutParams.width = 0;
int pixels_8dp = (int) (8 * scale + 0.5f);
layoutParams.setMargins(pixels_8dp, pixels_8dp, pixels_8dp, pixels_8dp);
MaterialCardView card = new MaterialCardView(this);
int pixels_4dp = (int) (4 * scale + 0.5f);
card.setRadius(pixels_4dp);
card.setCardElevation(pixels_4dp);
card.setContentPadding(pixels_4dp, pixels_4dp, pixels_4dp, pixels_4dp);
card.setLayoutParams(layoutParams);
LayoutInflater inflater = getLayoutInflater();
final View gaugeView = inflater.inflate(R.layout.dashboard_widget_generic_gauge, card, false);
final TextView gaugeLabel = gaugeView.findViewById(R.id.gauge_label);
gaugeLabel.setText(label);
card.addView(gaugeView);
widgetMap.put(name, gaugeView);
gridLayout.addView(card);
}
}

View File

@ -23,8 +23,11 @@ import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.Dev
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Intent;
import android.content.SharedPreferences;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -36,6 +39,7 @@ import java.util.SimpleTimeZone;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.marstek.SolarEquipmentStatusActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
@ -148,6 +152,11 @@ public class MarstekB2500DeviceSupport extends AbstractBTLEDeviceSupport {
sendCommand("set time", encodeSetCurrentTime());
}
@Override
public void onFetchRecordedData(int dataTypes) {
sendCommand("get infos 1", COMMAND_GET_INFOS1);
}
@Override
public void onSendConfiguration(final String config) {
Prefs devicePrefs = new Prefs(GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()));
@ -181,7 +190,7 @@ public class MarstekB2500DeviceSupport extends AbstractBTLEDeviceSupport {
ByteBuffer buf = ByteBuffer.wrap(value);
buf.order(ByteOrder.LITTLE_ENDIAN);
buf.position(4); // skip header
boolean p1_active = buf.get() != 0x00; // TODO: active = connected, or power incoming?
boolean p1_active = buf.get() != 0x00;
boolean p2_active = buf.get() != 0x00;
int p1_watt = buf.getShort();
int p2_watt = buf.getShort();
@ -216,6 +225,15 @@ public class MarstekB2500DeviceSupport extends AbstractBTLEDeviceSupport {
int battery_percentage = (int) Math.ceil((battery_charge_kwh / 2240.0f) * 100);
getDevice().setBatteryLevel(battery_percentage);
getDevice().sendDeviceUpdateIntent(getContext());
Intent intent = new Intent(SolarEquipmentStatusActivity.ACTION_SEND_SOLAR_EQUIPMENT_STATUS);
intent.putExtra(SolarEquipmentStatusActivity.EXTRA_BATTERY_PCT, battery_percentage);
intent.putExtra(SolarEquipmentStatusActivity.EXTRA_PANEL1_WATT, p1_watt);
intent.putExtra(SolarEquipmentStatusActivity.EXTRA_PANEL2_WATT, p2_watt);
intent.putExtra(SolarEquipmentStatusActivity.EXTRA_OUTPUT1_WATT, output_to_inverter_1_watt);
intent.putExtra(SolarEquipmentStatusActivity.EXTRA_OUTPUT2_WATT, output_to_inverter_2_watt);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
getContext().sendBroadcast(intent);
}
private void decodeDischargeIntervalsToPreferences(byte[] value) {

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/solarequipmentview_swipe_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".devices.marstek.SolarEquipmentStatusActivity">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.gridlayout.widget.GridLayout
android:id="@+id/solarequipmentview_gridlayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:columnCount="2" />
</androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>