diff --git a/app/src/main/java/info/nightscout/androidaps/MainActivity.java b/app/src/main/java/info/nightscout/androidaps/MainActivity.java index 8788020c87..83dbffab35 100644 --- a/app/src/main/java/info/nightscout/androidaps/MainActivity.java +++ b/app/src/main/java/info/nightscout/androidaps/MainActivity.java @@ -15,6 +15,7 @@ import org.slf4j.LoggerFactory; import java.util.List; import info.nightscout.androidaps.plugins.ProfileViewer.ProfileViewerFragment; +import info.nightscout.androidaps.plugins.TempBasals.TempBasalsFragment; import info.nightscout.androidaps.plugins.Treatments.TreatmentsFragment; import info.nightscout.androidaps.tabs.*; import info.nightscout.androidaps.plugins.Objectives.ObjectivesFragment; @@ -23,7 +24,8 @@ import info.nightscout.client.broadcasts.Intents; public class MainActivity extends AppCompatActivity implements ObjectivesFragment.OnFragmentInteractionListener, - TreatmentsFragment.OnFragmentInteractionListener { + TreatmentsFragment.OnFragmentInteractionListener, + TempBasalsFragment.OnFragmentInteractionListener { private static Logger log = LoggerFactory.getLogger(MainActivity.class); private Toolbar toolbar; @@ -40,6 +42,7 @@ public class MainActivity extends AppCompatActivity mAdapter = new TabPageAdapter(getSupportFragmentManager()); mAdapter.registerNewFragment("Test", TestFragment.newInstance()); mAdapter.registerNewFragment("Treatments", TreatmentsFragment.newInstance()); + mAdapter.registerNewFragment("TempBasals", TempBasalsFragment.newInstance()); mAdapter.registerNewFragment("Profile", ProfileViewerFragment.newInstance()); mAdapter.registerNewFragment("Objectives", ObjectivesFragment.newInstance()); diff --git a/app/src/main/java/info/nightscout/androidaps/data/Iob.java b/app/src/main/java/info/nightscout/androidaps/data/Iob.java index ee70699604..92fdae4f7c 100644 --- a/app/src/main/java/info/nightscout/androidaps/data/Iob.java +++ b/app/src/main/java/info/nightscout/androidaps/data/Iob.java @@ -6,10 +6,14 @@ package info.nightscout.androidaps.data; public class Iob { public double iobContrib = 0d; public double activityContrib = 0d; + public double netInsulin = 0d; // for calculations from temp basals only + public double netRatio = 0d; // for calculations from temp basals only public Iob plus(Iob iob) { iobContrib += iob.iobContrib; activityContrib += iob.activityContrib; + netInsulin += iob.netInsulin; + netRatio += iob.netRatio; return this; } } diff --git a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java index 3c9a4886a3..ed04522a71 100644 --- a/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java +++ b/app/src/main/java/info/nightscout/androidaps/db/DatabaseHelper.java @@ -19,7 +19,7 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { public static final String DATABASE_NAME = "AndroidAPSDb"; - private static final int DATABASE_VERSION = 1; + private static final int DATABASE_VERSION = 2; public DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); diff --git a/app/src/main/java/info/nightscout/androidaps/db/TempBasal.java b/app/src/main/java/info/nightscout/androidaps/db/TempBasal.java index 7ecd5b85e0..755e94e6fb 100644 --- a/app/src/main/java/info/nightscout/androidaps/db/TempBasal.java +++ b/app/src/main/java/info/nightscout/androidaps/db/TempBasal.java @@ -10,6 +10,10 @@ import java.util.Calendar; import java.util.Date; import java.util.TimeZone; +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.data.Iob; +import info.nightscout.client.data.NSProfile; + @DatabaseTable(tableName = "TempBasals") public class TempBasal { private static Logger log = LoggerFactory.getLogger(TempBasal.class); @@ -33,10 +37,10 @@ public class TempBasal { public Date timeEnd; @DatabaseField - public int percent; // In % of current basal + public int percent; // In % of current basal. 100% == current basal @DatabaseField - public int absolute; // Absolute value in U + public Double absolute; // Absolute value in U @DatabaseField public int duration; // in minutes @@ -47,59 +51,107 @@ public class TempBasal { @DatabaseField public boolean isAbsolute; // true if if set as absolute value in U -/* - public Iob calcIob() { - Iob iob = new Iob(); - long msAgo = getMillisecondsFromStart(); - Calendar startAdjusted = Calendar.getInstance(TimeZone.getTimeZone("UTC")); - startAdjusted.setTime(this.timeStart); - int minutes = startAdjusted.get(Calendar.MINUTE); - minutes = minutes % 4; - if (startAdjusted.get(Calendar.SECOND) > 0 && minutes == 0) { - minutes += 4; - } - startAdjusted.add(Calendar.MINUTE, minutes); - startAdjusted.set(Calendar.SECOND, 0); + public Iob iobCalc(Date time) { + Iob result = new Iob(); + NSProfile profile = MainApp.getNSProfile(); - IobCalc iobCalc = new IobCalc(); - iobCalc.setTime(new Date()); - iobCalc.setAmount(-1.0d * (baseRatio - tempRatio) / 15.0d / 100.0d); + if (profile == null) + return result; - long timeStartTime = startAdjusted.getTimeInMillis(); - Date currentTimeEnd = timeEnd; - if (currentTimeEnd == null) { - currentTimeEnd = new Date(); - if (getPlannedTimeEnd().getTime() < currentTimeEnd.getTime()) { - currentTimeEnd = getPlannedTimeEnd(); + int realDuration = getRealDuration(); + + if (realDuration > 0) { + Double netBasalRate = 0d; + Double basalRate = profile.getBasal(profile.secondsFromMidnight(time)); + Double tempBolusSize = 0.05; + + if (this.isAbsolute) { + netBasalRate = this.absolute - basalRate; + } else { + netBasalRate = (this.percent - 100) / 100d * basalRate; + } + + result.netRatio = netBasalRate; + Double netBasalAmount = Math.round(netBasalRate * realDuration * 10 / 6) / 100d; + result.netInsulin = netBasalAmount; + if (netBasalAmount < 0.1) { + tempBolusSize = 0.01; + } + if (netBasalRate < 0) { + tempBolusSize = -tempBolusSize; + } + Long tempBolusCount = Math.round(netBasalAmount / tempBolusSize); + if (tempBolusCount > 0) { + Long tempBolusSpacing = realDuration / tempBolusCount; + for (Long j = 0l; j < tempBolusCount; j++) { + Treatment tempBolusPart = new Treatment(); + tempBolusPart.insulin = tempBolusSize; + Long date = this.timeStart.getTime() + j * tempBolusSpacing * 60 * 1000; + tempBolusPart.created_at = new Date(date); + Iob iob = tempBolusPart.iobCalc(time); + result.plus(iob); + } } } - for (long time = timeStartTime; time < currentTimeEnd.getTime(); time += 4 * 60_000) { - Date start = new Date(time); - - iobCalc.setTimeStart(start); - iob.plus(iobCalc.invoke()); - } - - if (Settings.logTempIOBCalculation) { - log.debug("TempIOB start: " + this.timeStart + " end: " + this.timeEnd + " Percent: " + this.percent + " Duration: " + this.duration + " CalcDurat: " + (int) ((currentTimeEnd.getTime() - this.timeStart.getTime()) / 1000 / 60) - + "min minAgo: " + (int) (msAgo / 1000 / 60) + " IOB: " + iob.iobContrib + " Activity: " + iob.activityContrib + " Impact: " + (-0.01d * (baseRatio - tempRatio) * ((currentTimeEnd.getTime() - this.timeStart.getTime()) / 1000 / 60) / 60) - ); - } - - return iob; + return result; } -*/ + + /* + public Iob calcIob() { + Iob iob = new Iob(); + + long msAgo = getMillisecondsFromStart(); + Calendar startAdjusted = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + startAdjusted.setTime(this.timeStart); + int minutes = startAdjusted.get(Calendar.MINUTE); + minutes = minutes % 4; + if (startAdjusted.get(Calendar.SECOND) > 0 && minutes == 0) { + minutes += 4; + } + startAdjusted.add(Calendar.MINUTE, minutes); + startAdjusted.set(Calendar.SECOND, 0); + + IobCalc iobCalc = new IobCalc(); + iobCalc.setTime(new Date()); + iobCalc.setAmount(-1.0d * (baseRatio - tempRatio) / 15.0d / 100.0d); + + long timeStartTime = startAdjusted.getTimeInMillis(); + Date currentTimeEnd = timeEnd; + if (currentTimeEnd == null) { + currentTimeEnd = new Date(); + if (getPlannedTimeEnd().getTime() < currentTimeEnd.getTime()) { + currentTimeEnd = getPlannedTimeEnd(); + } + } + for (long time = timeStartTime; time < currentTimeEnd.getTime(); time += 4 * 60_000) { + Date start = new Date(time); + + iobCalc.setTimeStart(start); + iob.plus(iobCalc.invoke()); + } + + if (Settings.logTempIOBCalculation) { + log.debug("TempIOB start: " + this.timeStart + " end: " + this.timeEnd + " Percent: " + this.percent + " Duration: " + this.duration + " CalcDurat: " + (int) ((currentTimeEnd.getTime() - this.timeStart.getTime()) / 1000 / 60) + + "min minAgo: " + (int) (msAgo / 1000 / 60) + " IOB: " + iob.iobContrib + " Activity: " + iob.activityContrib + " Impact: " + (-0.01d * (baseRatio - tempRatio) * ((currentTimeEnd.getTime() - this.timeStart.getTime()) / 1000 / 60) / 60) + ); + } + + return iob; + } + */ // Determine end of basal public Date getTimeEnd() { Date tempBasalTimePlannedEnd = getPlannedTimeEnd(); + Date now = new Date(); - // End already exists in database - if (timeEnd != null) { - return timeEnd; + if (timeEnd != null && timeEnd.getTime() < tempBasalTimePlannedEnd.getTime()) { + tempBasalTimePlannedEnd = timeEnd; } - // if not return planned time + if (now.getTime() < tempBasalTimePlannedEnd.getTime()) + tempBasalTimePlannedEnd = now; + return tempBasalTimePlannedEnd; } @@ -107,6 +159,11 @@ public class TempBasal { return new Date(timeStart.getTime() + 60 * 1_000 * duration); } + public int getRealDuration() { + Long msecs = getTimeEnd().getTime() - timeStart.getTime(); + return (int) (msecs / 60 / 1000); + } + public long getMillisecondsFromStart() { return new Date().getTime() - timeStart.getTime(); } diff --git a/app/src/main/java/info/nightscout/androidaps/db/Treatment.java b/app/src/main/java/info/nightscout/androidaps/db/Treatment.java index b09714ab62..132e87bf14 100644 --- a/app/src/main/java/info/nightscout/androidaps/db/Treatment.java +++ b/app/src/main/java/info/nightscout/androidaps/db/Treatment.java @@ -57,6 +57,13 @@ public class Treatment { } public Iob iobCalc(Date time) { + Iob resultNow = doIobCalc(time); + Iob resultIn5min = doIobCalc(new Date(time.getTime() + 5 * 60 * 1000)); + resultNow.activityContrib = resultNow.iobContrib - resultIn5min.iobContrib; + return resultNow; + } + + public Iob doIobCalc(Date time) { Iob result = new Iob(); NSProfile profile = MainApp.getNSProfile(); @@ -77,7 +84,7 @@ public class Treatment { Double minAgo = scaleFactor * (time.getTime() - bolusTime) / 1000d / 60d; if (minAgo < peak) { - Double x1 = minAgo / 5 + 1; + Double x1 = minAgo / 5d + 1; result.iobContrib = this.insulin * (1 - 0.001852 * x1 * x1 + 0.001852 * x1); // units: BG (mg/dL) = (BG/U) * U insulin * scalar result.activityContrib = sens * this.insulin * (2 / dia / 60 / peak) * minAgo; diff --git a/app/src/main/java/info/nightscout/androidaps/events/EventTempBasalChange.java b/app/src/main/java/info/nightscout/androidaps/events/EventTempBasalChange.java new file mode 100644 index 0000000000..5d8f961e75 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/events/EventTempBasalChange.java @@ -0,0 +1,7 @@ +package info.nightscout.androidaps.events; + +/** + * Created by mike on 05.06.2016. + */ +public class EventTempBasalChange { +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/TempBasals/TempBasalsFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/TempBasals/TempBasalsFragment.java new file mode 100644 index 0000000000..e9846e149f --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/TempBasals/TempBasalsFragment.java @@ -0,0 +1,282 @@ +package info.nightscout.androidaps.plugins.TempBasals; + +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v7.widget.CardView; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.j256.ormlite.dao.Dao; +import com.j256.ormlite.stmt.PreparedQuery; +import com.j256.ormlite.stmt.QueryBuilder; +import com.squareup.otto.Subscribe; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.SQLException; +import java.text.DateFormat; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.data.Iob; +import info.nightscout.androidaps.db.TempBasal; +import info.nightscout.androidaps.events.EventNewBG; +import info.nightscout.androidaps.events.EventTempBasalChange; + + +public class TempBasalsFragment extends Fragment { + private static Logger log = LoggerFactory.getLogger(TempBasalsFragment.class); + + RecyclerView recyclerView; + LinearLayoutManager llm; + + TextView iobTotal; + TextView activityTotal; + + private static DecimalFormat formatNumber0decimalplaces = new DecimalFormat("0"); + private static DecimalFormat formatNumber2decimalplaces = new DecimalFormat("0.00"); + private static DecimalFormat formatNumber3decimalplaces = new DecimalFormat("0.000"); + + private OnFragmentInteractionListener mListener; + + private List tempBasals; + + private void initializeData() { + try { + Dao dao = MainApp.getDbHelper().getDaoTempBasals(); + + // **************** TESTING CREATE FAKE RECORD ***************** + TempBasal fake = new TempBasal(); + fake.timeStart = new Date(new Date().getTime() - 45 * 40 * 1000); + fake.timeEnd = new Date(new Date().getTime() - new Double(Math.random() * 45d * 40 * 1000).longValue()); + fake.duration = 30; + fake.percent = 150; + fake.isAbsolute = false; + fake.isExtended = false; + dao.create(fake); + // **************** TESTING CREATE FAKE RECORD ***************** + + QueryBuilder queryBuilder = dao.queryBuilder(); + queryBuilder.orderBy("timeIndex", false); + queryBuilder.limit(30l); + PreparedQuery preparedQuery = queryBuilder.prepare(); + tempBasals = dao.query(preparedQuery); + } catch (SQLException e) { + log.debug(e.getMessage(), e); + tempBasals = new ArrayList(); + } + if (recyclerView != null) { + recyclerView.swapAdapter(new RecyclerViewAdapter(tempBasals), false); + } + + + + updateTotalIOB(); + } + + private void updateTotalIOB() { + Iob total = new Iob(); + for (Integer pos = 0; pos < tempBasals.size(); pos++) { + TempBasal t = tempBasals.get(pos); + total.plus(t.iobCalc(new Date())); + } + if (iobTotal != null) + iobTotal.setText(formatNumber2decimalplaces.format(total.iobContrib)); + if (activityTotal != null) + activityTotal.setText(formatNumber3decimalplaces.format(total.activityContrib)); + } + + public static class RecyclerViewAdapter extends RecyclerView.Adapter { + + List tempBasals; + + RecyclerViewAdapter(List tempBasals) { + this.tempBasals = tempBasals; + } + + @Override + public TempBasalsViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { + View v = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.tempbasals_item, viewGroup, false); + TempBasalsViewHolder tempBasalsViewHolder = new TempBasalsViewHolder(v); + return tempBasalsViewHolder; + } + + @Override + public void onBindViewHolder(TempBasalsViewHolder holder, int position) { + // TODO: implement locales + DateFormat df = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, new Locale("cs", "CZ")); + DateFormat enddf = DateFormat.getTimeInstance(DateFormat.SHORT, new Locale("cs", "CZ")); + if (tempBasals.get(position).timeEnd != null) { + holder.date.setText(df.format(tempBasals.get(position).timeStart) + " - " + enddf.format(tempBasals.get(position).timeEnd)); + } else { + holder.date.setText(df.format(tempBasals.get(position).timeStart)); + } + holder.duration.setText(formatNumber0decimalplaces.format(tempBasals.get(position).duration) + " min"); + if (tempBasals.get(position).isAbsolute) { + holder.absolute.setText(formatNumber0decimalplaces.format(tempBasals.get(position).absolute) + " U/h"); + holder.percent.setText(""); + } else { + holder.absolute.setText(""); + holder.percent.setText(formatNumber0decimalplaces.format(tempBasals.get(position).percent) + "%"); + } + holder.realDuration.setText(formatNumber0decimalplaces.format(tempBasals.get(position).getRealDuration()) + " min"); + Iob iob = tempBasals.get(position).iobCalc(new Date()); + holder.iob.setText(formatNumber2decimalplaces.format(iob.iobContrib) + " U"); + holder.activity.setText(formatNumber3decimalplaces.format(iob.activityContrib) + " U"); + holder.netInsulin.setText(formatNumber2decimalplaces.format(iob.netInsulin) + " U"); + holder.netRatio.setText(formatNumber2decimalplaces.format(iob.netRatio) + " U/h"); + } + + @Override + public int getItemCount() { + return tempBasals.size(); + } + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + } + + public static class TempBasalsViewHolder extends RecyclerView.ViewHolder { + CardView cv; + TextView date; + TextView duration; + TextView absolute; + TextView percent; + TextView realDuration; + TextView netRatio; + TextView netInsulin; + TextView iob; + TextView activity; + + TempBasalsViewHolder(View itemView) { + super(itemView); + cv = (CardView) itemView.findViewById(R.id.tempbasals_cardview); + date = (TextView) itemView.findViewById(R.id.tempbasals_date); + duration = (TextView) itemView.findViewById(R.id.tempbasals_duration); + absolute = (TextView) itemView.findViewById(R.id.tempbasals_absolute); + percent = (TextView) itemView.findViewById(R.id.tempbasals_percent); + realDuration = (TextView) itemView.findViewById(R.id.tempbasals_realduration); + netRatio = (TextView) itemView.findViewById(R.id.tempbasals_netratio); + netInsulin = (TextView) itemView.findViewById(R.id.tempbasals_netinsulin); + iob = (TextView) itemView.findViewById(R.id.tempbasals_iob); + activity = (TextView) itemView.findViewById(R.id.tempbasals_activity); + } + } + } + + public TempBasalsFragment() { + super(); + initializeData(); + } + + public static TempBasalsFragment newInstance() { + TempBasalsFragment fragment = new TempBasalsFragment(); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + registerBus(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.tempbasals_fragment, container, false); + + recyclerView = (RecyclerView) view.findViewById(R.id.tempbasals_recyclerview); + recyclerView.setHasFixedSize(true); + llm = new LinearLayoutManager(view.getContext()); + recyclerView.setLayoutManager(llm); + + RecyclerViewAdapter adapter = new RecyclerViewAdapter(tempBasals); + recyclerView.setAdapter(adapter); + + iobTotal = (TextView) view.findViewById(R.id.tempbasals_iobtotal); + activityTotal = (TextView) view.findViewById(R.id.tempbasals_iobactivitytotal); + + return view; + } + /* + // TODO: Rename method, update argument and hook method into UI event + public void onButtonPressed(Uri uri) { + if (mListener != null) { + mListener.onFragmentInteraction(uri); + } + } + */ + + @Override + public void onAttach(Context context) { + super.onAttach(context); + if (context instanceof OnFragmentInteractionListener) { + mListener = (OnFragmentInteractionListener) context; + } else { + throw new RuntimeException(context.toString() + + " must implement OnFragmentInteractionListener"); + } + } + + @Override + public void onDetach() { + super.onDetach(); + mListener = null; + } + + private void registerBus() { + try { + MainApp.bus().unregister(this); + } catch (RuntimeException x) { + // Ignore + } + MainApp.bus().register(this); + } + + @Subscribe + public void onStatusEvent(final EventTempBasalChange ev) { + initializeData(); + } + + @Subscribe + public void onStatusEvent(final EventNewBG ev) { + updateTotalIOB(); + recyclerView.getAdapter().notifyDataSetChanged(); + } + + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + + if (isVisibleToUser) + updateTotalIOB(); + } + + /** + * This interface must be implemented by activities that contain this + * fragment to allow an interaction in this fragment to be communicated + * to the activity and potentially other fragments contained in that + * activity. + *

+ * See the Android Training lesson Communicating with Other Fragments for more information. + */ + public interface OnFragmentInteractionListener { + // TODO: Update argument type and name + void onFragmentInteraction(String param); + } +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/TreatmentsFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/TreatmentsFragment.java index ccf6b71599..6fb32c044d 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/TreatmentsFragment.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Treatments/TreatmentsFragment.java @@ -66,7 +66,6 @@ public class TreatmentsFragment extends Fragment { } if (recyclerView != null) { recyclerView.swapAdapter(new RecyclerViewAdapter(treatments), false); - //recyclerView.getAdapter().notifyDataSetChanged(); } updateTotalIOB(); } diff --git a/app/src/main/res/layout/tempbasals_fragment.xml b/app/src/main/res/layout/tempbasals_fragment.xml new file mode 100644 index 0000000000..404f4ed404 --- /dev/null +++ b/app/src/main/res/layout/tempbasals_fragment.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/tempbasals_item.xml b/app/src/main/res/layout/tempbasals_item.xml new file mode 100644 index 0000000000..5b16dd3caf --- /dev/null +++ b/app/src/main/res/layout/tempbasals_item.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5e866eff2f..5ddc0ed834 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,5 +40,12 @@ Activity: Toal IOB: Total IOB activity: + Dur: + Ratio: + Ins: + IOB: + Activity: + Total IOB: + Total IOB activity: