From ec5a4ad10061fd2cdb025144ab17d115e4de8145 Mon Sep 17 00:00:00 2001 From: trentsuzuki Date: Tue, 6 May 2025 11:46:35 -0400 Subject: [PATCH] Add average day for body energy --- .../activities/charts/BodyEnergyFragment.java | 162 ++++++++++++++++-- app/src/main/res/values/strings.xml | 1 + 2 files changed, 148 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/BodyEnergyFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/BodyEnergyFragment.java index 796ab77f90..aaf89c3ef0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/BodyEnergyFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/charts/BodyEnergyFragment.java @@ -59,6 +59,11 @@ public class BodyEnergyFragment extends AbstractChartFragment samples = getBodyEnergySamples(db, device, getTSStart(), getTSEnd()); - return new BodyEnergyData(samples); + List todaySamples = getBodyEnergySamples(db, device, getTSStart(), getTSEnd()); + List> historicalData = getHistoricalBodyEnergyData(db, device, DAYS_FOR_AVERAGE); + return new BodyEnergyData(todaySamples, historicalData); } @Override @@ -107,16 +114,19 @@ public class BodyEnergyFragment extends AbstractChartFragment lineEntries = new ArrayList<>(); + List averageLineEntries = new ArrayList<>(); final List lineDataSets = new ArrayList<>(); final AtomicInteger gainedValue = new AtomicInteger(0); final AtomicInteger drainedValue = new AtomicInteger(0); int newestValue = 0; long referencedTimestamp; - if (!bodyEnergyData.samples.isEmpty()) { - newestValue = bodyEnergyData.samples.get(bodyEnergyData.samples.size() - 1).getEnergy(); - referencedTimestamp = bodyEnergyData.samples.get(0).getTimestamp(); + + if (!bodyEnergyData.todaySamples.isEmpty()) { + // Process today's samples + newestValue = bodyEnergyData.todaySamples.get(bodyEnergyData.todaySamples.size() - 1).getEnergy(); + referencedTimestamp = bodyEnergyData.todaySamples.get(0).getTimestamp(); final AtomicInteger[] lastValue = {new AtomicInteger(0)}; - bodyEnergyData.samples.forEach((sample) -> { + bodyEnergyData.todaySamples.forEach((sample) -> { if (sample.getEnergy() < lastValue[0].intValue()) { drainedValue.set(drainedValue.get() + lastValue[0].intValue() - sample.getEnergy()); } else if (lastValue[0].intValue() > 0 && sample.getEnergy() > lastValue[0].intValue()) { @@ -128,29 +138,64 @@ public class BodyEnergyFragment extends AbstractChartFragment legendEntries = new ArrayList<>(2); - List legendEntries = new ArrayList<>(1); LegendEntry activityEntry = new LegendEntry(); activityEntry.label = getString(R.string.body_energy_legend_level); activityEntry.formColor = getResources().getColor(R.color.body_energy_level_color); legendEntries.add(activityEntry); + + // Add average legend entry if we have average data + if (!averageLineEntries.isEmpty()) { + LegendEntry averageEntry = new LegendEntry(); + averageEntry.label = getString(R.string.body_energy_legend_average); + averageEntry.formColor = AVERAGE_LINE_COLOR; + legendEntries.add(averageEntry); + } + bodyEnergyChart.getLegend().setTextColor(LEGEND_TEXT_COLOR); bodyEnergyChart.getLegend().setCustom(legendEntries); - lineDataSets.add(lineDataSet); final LineData lineData = new LineData(lineDataSets); bodyEnergyChart.setData(lineData); @@ -176,7 +221,9 @@ public class BodyEnergyFragment extends AbstractChartFragment getBodyEnergySamples(final DBHandler db, final GBDevice device, int tsFrom, int tsTo) { Calendar day = Calendar.getInstance(); day.setTimeInMillis(tsTo * 1000L); //we need today initially, which is the end of the time range @@ -191,6 +238,89 @@ public class BodyEnergyFragment extends AbstractChartFragment> getHistoricalBodyEnergyData(final DBHandler db, final GBDevice device, int daysCount) { + List> historicalData = new ArrayList<>(); + + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(getTSEnd() * 1000L); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + + // Go back one more day to exclude today + cal.add(Calendar.DAY_OF_YEAR, -1); + + final DeviceCoordinator coordinator = device.getDeviceCoordinator(); + final TimeSampleProvider sampleProvider = coordinator.getBodyEnergySampleProvider(device, db.getDaoSession()); + + for (int i = 0; i < daysCount; i++) { + long dayStart = (int) (cal.getTimeInMillis() / 1000); + long dayEnd = dayStart + 24 * 60 * 60 - 1; + + List daySamples = sampleProvider.getAllSamples(dayStart * 1000L, dayEnd * 1000L); + historicalData.add(daySamples); + + // Move to the previous day + cal.add(Calendar.DAY_OF_YEAR, -1); + } + + return historicalData; + } + + /** + * Build an binned average body energy curve with an arbitrary bin size. + * + * @param historicDays list of samples maps (timestamp -> energy) + * @param binSizeMinutes width of a bin in minutes (must divide 24 hours evenly) + * @return MPAndroidChart entries (x = seconds from local midnight, y = avg body energy) + */ + private List buildAverageEntries(List> historicDays, int binSizeMinutes) { + if (binSizeMinutes <= 0 || 24 * 60 % binSizeMinutes != 0) { + throw new IllegalArgumentException("binSizeMinutes must be a positive divisor of 24 hours"); + } + + final int binSizeSeconds = binSizeMinutes * 60; + final int binsPerDay = 24 * 60 * 60 / binSizeSeconds; + + long[] sum = new long[binsPerDay]; + int[] count = new int[binsPerDay]; + + TimeZone tz = TimeZone.getDefault(); + + // Sum body energy in bins over the provided history + for (List day : historicDays) { + for (BodyEnergySample sample : day) { + long ts = sample.getTimestamp(); + int offsetSec = tz.getOffset(ts) / 1000; // Time zone + DST in seconds + long localSec = ts / 1000 + offsetSec; // Epoch seconds in local time + int bin = (int) ((localSec / binSizeSeconds) % binsPerDay); + + sum[bin] += sample.getEnergy(); + count[bin] += 1; + } + } + + // Calculate the average in each bin and make plot entries + List avgEntries = new ArrayList<>(binsPerDay); + for (int bin = 0; bin < binsPerDay; bin++) { + if (count[bin] != 0) { + float x = bin * (float) binSizeSeconds; // seconds from local midnight + float avg = (float) sum[bin] / count[bin]; + avgEntries.add(new Entry(x, avg)); + } + } + + // Add one extra entry to make the graph wrap at the end of the day + float x = binsPerDay * (float) binSizeSeconds; + float avg = (float) sum[0] / count[0]; + avgEntries.add(new Entry(x, avg)); + + return avgEntries; + } + protected void setupLegend(Chart chart) {} Bitmap drawGauge(int width, int barWidth, @ColorInt int filledColor, int value, int maxValue) { @@ -296,10 +426,12 @@ public class BodyEnergyFragment extends AbstractChartFragment samples; + private final List todaySamples; + private final List> historicalData; - protected BodyEnergyData(List samples) { - this.samples = samples; + protected BodyEnergyData(List todaySamples, List> historicalData) { + this.todaySamples = todaySamples; + this.historicalData = historicalData; } } } \ 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 3fc3e6c356..01222ffb80 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3942,4 +3942,5 @@ Get reminders about Activity Ring progress Goal Reached Get a reminder when you complete rings + 30-Day Average