Files
DexcomBluetoothUploader/lib/nightscout/com/eveningoutpost/dexdrip/Models/Treatments.java
2020-07-18 21:44:27 -04:00

1253 lines
54 KiB
Java

package com.eveningoutpost.dexdrip.Models;
/**
* Created by jamorham on 31/12/15.
*/
import android.content.Context;
import android.provider.BaseColumns;
import android.util.Pair;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import com.activeandroid.query.Delete;
import com.activeandroid.query.Select;
import com.activeandroid.util.SQLiteUtils;
import com.eveningoutpost.dexdrip.GcmActivity;
import com.eveningoutpost.dexdrip.Home;
import com.eveningoutpost.dexdrip.Models.UserError.Log;
import com.eveningoutpost.dexdrip.R;
import com.eveningoutpost.dexdrip.Services.SyncService;
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
import com.eveningoutpost.dexdrip.UtilityModels.UndoRedo;
import com.eveningoutpost.dexdrip.UtilityModels.UploaderQueue;
import com.eveningoutpost.dexdrip.insulin.Insulin;
import com.eveningoutpost.dexdrip.insulin.InsulinManager;
import com.eveningoutpost.dexdrip.insulin.MultipleInsulins;
import com.eveningoutpost.dexdrip.watch.thinjam.BlueJayEntry;
import com.eveningoutpost.dexdrip.xdrip;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.Expose;
import com.google.gson.internal.bind.DateTypeAdapter;
import com.google.gson.reflect.TypeToken;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
import static com.eveningoutpost.dexdrip.UtilityModels.Constants.HOUR_IN_MS;
import static com.eveningoutpost.dexdrip.UtilityModels.Constants.MINUTE_IN_MS;
import static java.lang.StrictMath.abs;
import static com.eveningoutpost.dexdrip.Models.JoH.emptyString;
// TODO Switchable Carb models
// TODO Linear array timeline optimization
@Table(name = "Treatments", id = BaseColumns._ID)
public class Treatments extends Model {
private static final String TAG = "jamorham " + Treatments.class.getSimpleName();
private static final String DEFAULT_EVENT_TYPE = "<none>";
public final static String XDRIP_TAG = "xdrip";
//public static double activityMultipler = 8.4; // somewhere between 8.2 and 8.8
private static Treatments lastCarbs;
private static boolean patched = false;
@Expose
@Column(name = "timestamp", index = true)
public long timestamp;
@Expose
@Column(name = "eventType")
public String eventType;
@Expose
@Column(name = "enteredBy")
public String enteredBy;
@Expose
@Column(name = "notes")
public String notes;
@Expose
@Column(name = "uuid", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE)
public String uuid;
@Expose
@Column(name = "carbs")
public double carbs;
@Expose
@Column(name = "insulin")
public double insulin;
@Expose
@Column(name = "insulinJSON")
public String insulinJSON;
@Expose
@Column(name = "created_at")
public String created_at;
// don't access this directly use getInsulinInjections()
private List<InsulinInjection> insulinInjections = null;
private boolean hasInsulinInjections() {
final List<InsulinInjection> injections = getInsulinInjections();
return ((injections != null) && (injections.size() > 0));
}
public boolean isBasalOnly() {
if (!hasInsulinInjections()) return false;
boolean foundBasal = false;
final List<InsulinInjection> injections = getInsulinInjections();
for (InsulinInjection injection : injections) {
Log.d(TAG,"isBasalOnly: "+injection.isBasal()+" "+injection.getInsulin());
if (!injection.isBasal()) {
return false;
} else {
foundBasal = true;
}
}
return foundBasal;
}
private String getInsulinInjectionsShortString() {
final StringBuilder sb = new StringBuilder();
for (InsulinInjection injection : insulinInjections) {
sb.append(injection.getProfile().getName());
sb.append(" ");
sb.append(injection.getUnits() + "U ");
}
return sb.toString();
}
private void setInsulinInjections(List<InsulinInjection> i)
{
// TODO possiblity here to preserve null if Multiple Injections is not enabled
if (i == null) {
i = new ArrayList<>();
}
insulinInjections = i;
Gson gson = new GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()
// .registerTypeAdapter(Date.class, new DateTypeAdapter())
.serializeSpecialFloatingPointValues()
.create();
insulinJSON = gson.toJson(i);
}
// lazily populate and return InsulinInjection array from json
List<InsulinInjection> getInsulinInjections() {
// Log.d(TAG,"get injections: "+insulinJSON);
if (insulinInjections == null) {
if (insulinJSON != null) {
try {
insulinInjections = new Gson().fromJson(insulinJSON, new TypeToken<ArrayList<InsulinInjection>>() {
}.getType());
StringBuilder x = new StringBuilder();
for (InsulinInjection y : insulinInjections) {
x.append(y.getProfile().getName() + " " + y.getUnits()+" ");
}
} catch (Exception e) {
if (JoH.ratelimit("ij-json-error", 60)) {
UserError.Log.wtf(TAG, "Error converting insulinJson: " + e + " " + insulinJSON);
}
notes = "CORRUPT DATA";
// state of insulinInjections is basically undefined here as we cannot recover from corrupt data
// we could neutralise the treatment data in other ways perhaps.
}
} else {
// return empty if not set // TODO do we want to cache this or not to avoid memory creation?
return new ArrayList<>();
}
}
return insulinInjections;
}
// take a simple insulin value and produce a list assuming it is bolus insulin - for legacy conversion
static private List<InsulinInjection> convertLegacyDoseToBolusInjectionList(final double insulinSum) {
final ArrayList<InsulinInjection> injections = new ArrayList<>();
injections.add(new InsulinInjection(InsulinManager.getBolusProfile(), insulinSum));
return injections;
}
// take a simple insulin value and produce a list by name of insulin - for general quick conversion
public static List<InsulinInjection> convertLegacyDoseToInjectionListByName(final String insulinName, final double insulinSum) {
Log.d(TAG,"convertingLegacyDoseByName: "+insulinName+" "+insulinSum);
final Insulin insulin = InsulinManager.getProfile(insulinName);
if (insulin == null) return null; // TODO should we actually throw an exception here as this should never happen and the result would be invalid
final ArrayList<InsulinInjection> injections = new ArrayList<>();
injections.add(new InsulinInjection(insulin, insulinSum));
return injections;
}
public void setInsulinJSON(String json) {
if ((json == null) || json.isEmpty())
json = "[]";
try {
insulinInjections = new Gson().fromJson(json, new TypeToken<ArrayList<InsulinInjection>>() {
}.getType());
insulinJSON = json; // set json only if we didn't get exception processing it
} catch (Exception e) {
UserError.Log.e(TAG, "Got exception in setInsulinJson: " + e + " for " + json);
}
}
public Treatments()
{
eventType = DEFAULT_EVENT_TYPE;
carbs = 0;
insulin = 0;
//setInsulinInjections(null);
}
public static synchronized Treatments create(final double carbs, final double insulin, long timestamp) {
return create(carbs, insulin, timestamp, null);
}
public static synchronized Treatments create(final double carbs, final double insulinSum, final long timestamp, final String suggested_uuid) {
if (MultipleInsulins.isEnabled()) {
return create(carbs, insulinSum, convertLegacyDoseToBolusInjectionList(insulinSum), timestamp, suggested_uuid);
} else {
return create(carbs, insulinSum, null, timestamp, suggested_uuid);
}
}
public static synchronized Treatments create(final double carbs, final double insulinSum, final List<InsulinInjection> insulin, long timestamp) {
return create(carbs, insulinSum, insulin, timestamp, null);
}
public static synchronized Treatments create(final double carbs, final double insulinSum, final List<InsulinInjection> insulin, long timestamp, String suggested_uuid) {
// if treatment more than 1 minutes in the future
final long future_seconds = (timestamp - JoH.tsl()) / 1000;
if (future_seconds > (60 * 60)) {
JoH.static_toast_long("Refusing to create a treatement more than 1 hours in the future!");
return null;
}
if ((future_seconds > 60) && (future_seconds < 86400) && ((carbs > 0) || (insulinSum > 0))) {
final Context context = xdrip.getAppContext();
JoH.scheduleNotification(context, "Treatment Reminder", "@" + JoH.hourMinuteString(timestamp) + " : "
+ carbs + " " + context.getString(R.string.carbs) + " / "
+ insulinSum + " " + context.getString(R.string.units), (int) future_seconds, 34026);
}
return create(carbs, insulinSum, insulin, timestamp, -1, suggested_uuid);
}
public static synchronized Treatments create(final double carbs, final double insulinSum, final List<InsulinInjection> insulin, long timestamp, double position, String suggested_uuid) {
// TODO sanity check values
Log.d(TAG, "Creating treatment: " +
"Insulin: " + insulinSum + " / " +
"Carbs: " + carbs +
(suggested_uuid != null && !suggested_uuid.isEmpty()
? " " + "uuid: " + suggested_uuid
: ""));
if ((carbs == 0) && (insulinSum == 0)) return null;
if (timestamp == 0) {
timestamp = new Date().getTime();
}
final Treatments treatment = new Treatments();
if (position > 0) {
treatment.enteredBy = XDRIP_TAG + " pos:" + JoH.qs(position, 2);
} else {
treatment.enteredBy = XDRIP_TAG;
}
treatment.carbs = carbs;
treatment.insulin = insulinSum;
treatment.setInsulinInjections(insulin);
treatment.timestamp = timestamp;
treatment.created_at = DateUtil.toISOString(timestamp);
treatment.uuid = suggested_uuid != null ? suggested_uuid : UUID.randomUUID().toString();
treatment.save();
// GcmActivity.pushTreatmentAsync(Treatment);
// NSClientChat.pushTreatmentAsync(Treatment);
pushTreatmentSync(treatment);
UndoRedo.addUndoTreatment(treatment.uuid);
return treatment;
}
// Note
public static synchronized Treatments create_note(String note, long timestamp) {
return create_note(note, timestamp, -1, null);
}
public static synchronized Treatments create_note(String note, long timestamp, double position) {
return create_note(note, timestamp, position, null);
}
public static synchronized Treatments create_note(String note, long timestamp, double position, String suggested_uuid) {
// TODO sanity check values
Log.d(TAG, "Creating treatment note: " + note);
if (timestamp == 0) {
timestamp = new Date().getTime();
}
if ((note == null || (note.length() == 0))) {
Log.i(TAG, "Empty treatment note - not saving");
return null;
}
boolean is_new = false;
// find treatment
Treatments treatment = byTimestamp(timestamp, MINUTE_IN_MS * 5);
// if unknown create
if (treatment == null) {
treatment = new Treatments();
Log.d(TAG, "Creating new treatment entry for note");
is_new = true;
treatment.notes = note;
treatment.timestamp = timestamp;
treatment.created_at = DateUtil.toISOString(timestamp);
treatment.uuid = suggested_uuid != null ? suggested_uuid : UUID.randomUUID().toString();
} else {
if (treatment.notes == null) treatment.notes = "";
Log.d(TAG, "Found existing treatment for note: " + treatment.uuid + ((suggested_uuid != null) ? " vs suggested: " + suggested_uuid : "") + " distance:" + Long.toString(timestamp - treatment.timestamp) + " " + treatment.notes);
if (treatment.notes.contains(note)) {
Log.d(TAG, "Suggested note update already present - skipping");
return null;
}
// append existing note or treatment
if (treatment.notes.length() > 0) treatment.notes += " \u2192 ";
treatment.notes += note;
Log.d(TAG, "Final notes: " + treatment.notes);
}
// if ((treatment.enteredBy == null) || (!treatment.enteredBy.contains(NightscoutUploader.VIA_NIGHTSCOUT_TAG))) {
// tag it as from xdrip if it isn't being synced from nightscout right now to allow local updates to nightscout sourced notes
if (suggested_uuid == null) {
if (position > 0) {
treatment.enteredBy = XDRIP_TAG + " pos:" + JoH.qs(position, 2);
} else {
treatment.enteredBy = XDRIP_TAG;
}
}
treatment.save();
pushTreatmentSync(treatment, is_new, suggested_uuid);
if (is_new) UndoRedo.addUndoTreatment(treatment.uuid);
return treatment;
}
public static synchronized Treatments SensorStart(long timestamp) {
if (timestamp == 0) {
timestamp = new Date().getTime();
}
final Treatments Treatment = new Treatments();
Treatment.enteredBy = XDRIP_TAG;
Treatment.eventType = "Sensor Start";
Treatment.created_at = DateUtil.toISOString(timestamp);
Treatment.timestamp = timestamp;
Treatment.uuid = UUID.randomUUID().toString();
Treatment.save();
pushTreatmentSync(Treatment);
return Treatment;
}
private static void pushTreatmentSync(Treatments treatment) {
pushTreatmentSync(treatment, true, null); // new entry by default
}
private static void pushTreatmentSync(Treatments treatment, boolean is_new, String suggested_uuid) {
if (Home.get_master_or_follower()) GcmActivity.pushTreatmentAsync(treatment);
if (!(Pref.getBoolean("cloud_storage_api_enable", false) || Pref.getBoolean("cloud_storage_mongodb_enable", false))) {
NSClientChat.pushTreatmentAsync(treatment);
} else {
Log.d(TAG, "Skipping NSClient treatment broadcast as nightscout direct sync is enabled");
}
if (suggested_uuid == null) {
// only sync to nightscout if source of change was not from nightscout
if (UploaderQueue.newEntry(is_new ? "insert" : "update", treatment) != null) {
SyncService.startSyncService(3000); // sync in 3 seconds
}
}
}
public static void pushTreatmentSyncToWatch(Treatments treatment, boolean is_new) {
Log.d(TAG, "pushTreatmentSyncToWatch Add treatment to UploaderQueue.");
if (Pref.getBooleanDefaultFalse("wear_sync")) {
if (UploaderQueue.newEntryForWatch(is_new ? "insert" : "update", treatment) != null) {
SyncService.startSyncService(3000); // sync in 3 seconds
}
}
}
// This shouldn't be needed but it seems it is
private static void fixUpTable() {
if (patched) return;
String[] patchup = {
"CREATE TABLE Treatments (_id INTEGER PRIMARY KEY AUTOINCREMENT);",
"ALTER TABLE Treatments ADD COLUMN timestamp INTEGER;",
"ALTER TABLE Treatments ADD COLUMN uuid TEXT;",
"ALTER TABLE Treatments ADD COLUMN eventType TEXT;",
"ALTER TABLE Treatments ADD COLUMN enteredBy TEXT;",
"ALTER TABLE Treatments ADD COLUMN notes TEXT;",
"ALTER TABLE Treatments ADD COLUMN created_at TEXT;",
"ALTER TABLE Treatments ADD COLUMN insulin REAL;",
"ALTER TABLE Treatments ADD COLUMN insulinJSON TEXT;",
"ALTER TABLE Treatments ADD COLUMN carbs REAL;",
"CREATE INDEX index_Treatments_timestamp on Treatments(timestamp);",
"CREATE UNIQUE INDEX index_Treatments_uuid on Treatments(uuid);"};
for (String patch : patchup) {
try {
SQLiteUtils.execSql(patch);
//Log.e(TAG, "Processed patch should not have succeeded!!: " + patch);
} catch (Exception e) {
// Log.d(TAG, "Patch: " + patch + " generated exception as it should: " + e.toString());
}
}
patched = true;
}
public static Treatments last() {
fixUpTable();
return new Select()
.from(Treatments.class)
.orderBy("_ID desc")
.executeSingle();
}
public static Treatments lastNotFromXdrip() {
fixUpTable();
return new Select()
.from(Treatments.class)
.where("enteredBy NOT LIKE '" + XDRIP_TAG + "%'")
.orderBy("_ID DESC")
.executeSingle();
}
public static List<Treatments> latest(int num) {
try {
return new Select()
.from(Treatments.class)
.orderBy("timestamp desc")
.limit(num)
.execute();
} catch (android.database.sqlite.SQLiteException e) {
fixUpTable();
return null;
}
}
public static Treatments byuuid(String uuid) {
if (uuid == null) return null;
return new Select()
.from(Treatments.class)
.where("uuid = ?", uuid)
.orderBy("_ID desc")
.executeSingle();
}
public static Treatments byid(long id) {
return new Select()
.from(Treatments.class)
.where("_ID = ?", id)
.executeSingle();
}
public static Treatments byTimestamp(long timestamp) {
return byTimestamp(timestamp, 1500);
}
public static Treatments byTimestamp(long timestamp, long plus_minus_millis) {
if (plus_minus_millis > Integer.MAX_VALUE) {
throw new RuntimeException("Treatment by TimeStamp out of range value: " + plus_minus_millis);
}
return byTimestamp(timestamp, (int) plus_minus_millis);
}
public static Treatments byTimestamp(long timestamp, int plus_minus_millis) {
return new Select()
.from(Treatments.class)
.where("timestamp <= ? and timestamp >= ?", (timestamp + plus_minus_millis), (timestamp - plus_minus_millis)) // window
.orderBy("abs(timestamp-" + Long.toString(timestamp) + ") asc")
.executeSingle();
}
public static void delete_all() {
delete_all(false);
}
public static void delete_all(boolean from_interactive) {
if (from_interactive) {
GcmActivity.push_delete_all_treatments();
}
new Delete()
.from(Treatments.class)
.execute();
// not synced with uploader queue - should we?
}
public static Treatments delete_last() {
return delete_last(false);
}
public static void delete_by_timestamp(long timestamp) {
delete_by_timestamp(timestamp, 1500, false);
}
public static void delete_by_timestamp(long timestamp, int accuracy, boolean from_interactive) {
final Treatments t = byTimestamp(timestamp, accuracy); // do we need to alter default accuracy?
if (t != null) {
Log.d(TAG, "Deleting treatment closest to: " + JoH.dateTimeText(timestamp) + " matches uuid: " + t.uuid);
delete_by_uuid(t.uuid, from_interactive);
} else {
Log.e(TAG, "Couldn't find a treatment near enough to " + JoH.dateTimeText(timestamp) + " to delete!");
}
}
public static void delete_by_uuid(String uuid) {
delete_by_uuid(uuid, false);
}
public static void delete_by_uuid(String uuid, boolean from_interactive) {
Treatments thistreat = byuuid(uuid);
if (thistreat != null) {
UploaderQueue.newEntry("delete", thistreat);
if (from_interactive) {
GcmActivity.push_delete_treatment(thistreat);
SyncService.startSyncService(3000); // sync in 3 seconds
}
thistreat.delete();
Home.staticRefreshBGCharts();
}
}
public static Treatments delete_last(boolean from_interactive) {
Treatments thistreat = last();
if (thistreat != null) {
if (from_interactive) {
GcmActivity.push_delete_treatment(thistreat);
//GoogleDriveInterface gdrive = new GoogleDriveInterface();
//gdrive.deleteTreatmentAtRemote(thistreat.uuid);
}
UploaderQueue.newEntry("delete", thistreat);
thistreat.delete();
}
return null;
}
public static Treatments fromJSON(String json) {
try {
return new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json, Treatments.class);
} catch (Exception e) {
Log.d(TAG, "Got exception parsing treatment json: " + e.toString());
Home.toaststatic("Error on treatment, probably decryption key mismatch");
return null;
}
}
public static synchronized boolean pushTreatmentFromJson(String json) {
return pushTreatmentFromJson(json, false);
}
public static synchronized boolean pushTreatmentFromJson(String json, boolean from_interactive) {
Log.d(TAG, "converting treatment from json: " + json);
final Treatments mytreatment = fromJSON(json);
if (mytreatment != null) {
if ((mytreatment.carbs == 0) && (mytreatment.insulin == 0)
&& (mytreatment.notes != null) && (mytreatment.notes.startsWith("AndroidAPS started"))) {
Log.d(TAG, "Skipping AndroidAPS started message");
return false;
}
if ((mytreatment.eventType != null) && (mytreatment.eventType.equals("Temp Basal"))) {
// we don't yet parse or process these
Log.d(TAG, "Skipping Temp Basal msg");
return false;
}
if (mytreatment.uuid == null) {
try {
final JSONObject jsonobj = new JSONObject(json);
if (jsonobj.has("_id")) mytreatment.uuid = jsonobj.getString("_id");
} catch (JSONException e) {
//
}
if (mytreatment.uuid == null) mytreatment.uuid = UUID.randomUUID().toString();
}
// anything received +- 1500 ms is going to be treated as a duplicate
final Treatments dupe_treatment = byTimestamp(mytreatment.timestamp);
if (dupe_treatment != null) {
Log.i(TAG, "Duplicate treatment for: " + mytreatment.timestamp);
if ((dupe_treatment.insulin == 0) && (mytreatment.insulin > 0)) {
dupe_treatment.setInsulinJSON(mytreatment.insulinJSON);
dupe_treatment.insulin = mytreatment.insulin;
dupe_treatment.save();
Home.staticRefreshBGChartsOnIdle();
}
if ((dupe_treatment.carbs == 0) && (mytreatment.carbs > 0)) {
dupe_treatment.carbs = mytreatment.carbs;
dupe_treatment.save();
Home.staticRefreshBGChartsOnIdle();
}
if ((dupe_treatment.uuid != null) && (mytreatment.uuid != null) && (dupe_treatment.uuid.equals(mytreatment.uuid)) && (mytreatment.notes != null)) {
if ((dupe_treatment.notes == null) || (dupe_treatment.notes.length() < mytreatment.notes.length())) {
dupe_treatment.notes = mytreatment.notes;
fixUpTable();
dupe_treatment.save();
Log.d(TAG, "Saved updated treatement notes");
// should not end up needing to append notes and be from_interactive via undo as these
// would be mutually exclusive operations so we don't need to handle that here.
Home.staticRefreshBGChartsOnIdle();
// TODO review if this is correct place for new notes only
evaluateNotesForNotification(mytreatment);
}
}
return false;
}
Log.d(TAG, "Saving pushed treatment: " + mytreatment.uuid);
if ((mytreatment.enteredBy == null) || (mytreatment.enteredBy.equals(""))) {
mytreatment.enteredBy = "sync";
}
if ((mytreatment.eventType == null) || (mytreatment.eventType.equals(""))) {
mytreatment.eventType = DEFAULT_EVENT_TYPE; // should have a default
}
if ((mytreatment.created_at == null) || (mytreatment.created_at.equals(""))) {
try {
mytreatment.created_at = DateUtil.toISOString(mytreatment.timestamp); // should have a default
} catch (Exception e) {
Log.e(TAG, "Could not convert timestamp to isostring");
}
}
fixUpTable();
long x = mytreatment.save();
Log.d(TAG, "Saving treatment result: " + x);
if (from_interactive) {
pushTreatmentSync(mytreatment);
}
// TODO review if this is correct place for new notes only
evaluateNotesForNotification(mytreatment);
Home.staticRefreshBGChartsOnIdle();
return true;
} else {
return false;
}
}
private static void evaluateNotesForNotification(final Treatments mytreatment) {
if (!emptyString(mytreatment.notes) && mytreatment.notes.startsWith("-")) {
BlueJayEntry.sendNotifyIfEnabled(mytreatment.notes);
}
}
public static List<Treatments> latestForGraph(int number, double startTime) {
return latestForGraph(number, startTime, JoH.ts());
}
public static List<Treatments> latestForGraph(int number, double startTime, double endTime) {
fixUpTable();
DecimalFormat df = new DecimalFormat("#");
df.setMaximumFractionDigits(1); // are there decimal points in the database??
return new Select()
.from(Treatments.class)
.where("timestamp >= ? and timestamp <= ?", df.format(startTime), df.format(endTime))
.orderBy("timestamp asc")
.limit(number)
.execute();
}
public static List<Treatments> latestForGraph(final int number, final long startTime, final long endTime) {
fixUpTable();
return new Select()
.from(Treatments.class)
.where("timestamp >= ? and timestamp <= ?", startTime, endTime)
.orderBy("timestamp asc")
.limit(number)
.execute();
}
public static long getTimeStampWithOffset(double offset) {
// optimisation instead of creating a new date each time?
return (long) (new Date().getTime() - offset);
}
/// this is no longer used
/* public static CobCalc cobCalc(Treatments treatment, double lastDecayedBy, double time) {
double delay = 20; // minutes till carbs start decaying
double delayms = delay * Constants.MINUTE_IN_MS;
if (treatment.carbs > 0) {
CobCalc thisCobCalc = new CobCalc();
thisCobCalc.carbTime = treatment.timestamp;
// no previous carb treatment? Set to our start time
if (lastDecayedBy == 0) {
lastDecayedBy = thisCobCalc.carbTime;
}
double carbs_hr = Profile.getCarbAbsorptionRate(time);
double carbs_min = carbs_hr / 60;
double carbs_ms = carbs_min / Constants.MINUTE_IN_MS;
thisCobCalc.decayedBy = thisCobCalc.carbTime; // initially set to start time for this treatment
double minutesleft = (lastDecayedBy - thisCobCalc.carbTime) / Constants.MINUTE_IN_MS;
double how_long_till_carbs_start_ms = (lastDecayedBy - thisCobCalc.carbTime);
thisCobCalc.decayedBy += (Math.max(delay, minutesleft) + treatment.carbs / carbs_min) * Constants.MINUTE_IN_MS;
if (delay > minutesleft) {
thisCobCalc.initialCarbs = treatment.carbs;
} else {
thisCobCalc.initialCarbs = treatment.carbs + minutesleft * carbs_min;
}
double startDecay = thisCobCalc.carbTime + (delay * Constants.MINUTE_IN_MS);
if (time < lastDecayedBy || time > startDecay) {
thisCobCalc.isDecaying = 1;
} else {
thisCobCalc.isDecaying = 0;
}
return thisCobCalc;
} else {
return null;
}
}
*/
// when using multiple insulins
private static Pair<Double, Double> calculateIobActivityFromTreatmentAtTime(final Treatments treatment, final double time, final boolean useBasal) {
double iobContrib = 0, activityContrib = 0;
if (treatment.insulin > 0) {
// Log.d(TAG,"NEW TYPE insulin: "+treatment.insulin+ " "+treatment.insulinJSON);
// translate a legacy entry to be bolus insulin
List<InsulinInjection> injectionsList = treatment.getInsulinInjections();
if (injectionsList == null || injectionsList.size() == 0) {
Log.d(TAG,"CONVERTING LEGACY: "+treatment.insulinJSON+ " "+injectionsList);
injectionsList = convertLegacyDoseToBolusInjectionList(treatment.insulin);
treatment.insulinInjections = injectionsList; // cache but best not to save it
}
for (final InsulinInjection injection : injectionsList)
if (injection.getUnits() > 0 && (useBasal || !injection.isBasal())) {
iobContrib += injection.getUnits() * abs(injection.getProfile().calculateIOB((time - treatment.timestamp) / MINUTE_IN_MS));
activityContrib += injection.getUnits() * abs(injection.getProfile().calculateActivity((time - treatment.timestamp) / MINUTE_IN_MS));
}
if (iobContrib < 0) iobContrib = 0;
if (activityContrib < 0) activityContrib = 0;
}
return new Pair<>(iobContrib, activityContrib);
}
// using the original calculation
private static Pair<Double, Double> calculateLegacyIobActivityFromTreatmentAtTime(final Treatments treatment, final double time) {
final double dia = Profile.insulinActionTime(time); // duration insulin action in hours
final double peak = 75; // minutes in based on a 3 hour DIA - scaled proportionally (orig 75)
double insulin_delay_minutes = 0;
double insulin_timestamp = treatment.timestamp + (insulin_delay_minutes * 60 * 1000);
//Iob response = new Iob();
final double scaleFactor = 3.0 / dia;
double iobContrib = 0;
//double activityContrib = 0;
// only use treatments with insulin component which have already happened
if ((treatment.insulin > 0) && (insulin_timestamp < time)) {
// double bolusTime = insulin_timestamp; // bit of a dupe
double minAgo = scaleFactor * (((time - insulin_timestamp) / 1000) / 60);
if (minAgo < peak) {
double x1 = minAgo / 5 + 1;
iobContrib = treatment.insulin * (1 - 0.001852 * x1 * x1 + 0.001852 * x1);
// units: BG (mg/dL) = (BG/U) * U insulin * scalar
// activityContrib = sens * activityMultipler * treatment.insulin * (2 / dia / 60 / peak) * minAgo;
} else if (minAgo < 180) {
double x2 = (minAgo - peak) / 5;
iobContrib = treatment.insulin * (0.001323 * x2 * x2 - .054233 * x2 + .55556);
// activityContrib = sens * activityMultipler * treatment.insulin * (2 / dia / 60 - (minAgo - peak) * 2 / dia / 60 / (60 * dia - peak));
}
}
if (iobContrib < 0) iobContrib = 0;
//if (activityContrib < 0) activityContrib = 0;
return new Pair<>(iobContrib, 0d);
}
private static Iob calcTreatment(final Treatments treatment, final double time, final boolean useBasal) {
final Iob response = new Iob();
if (MultipleInsulins.isEnabled()) {
Pair<Double,Double> result = calculateIobActivityFromTreatmentAtTime(treatment, time, useBasal);
response.iob = result.first;
response.jActivity = result.second;
} else {
Pair<Double,Double> result = calculateLegacyIobActivityFromTreatmentAtTime(treatment, time);
response.iob = result.first;
// response.jActivity = result.second;
}
return response;
}
// requires stepms granularity which we should already have
private static double timesliceIactivityAtTime(Map<Double, Iob> timeslices, double thistime) {
if (timeslices.containsKey(thistime)) {
return timeslices.get(thistime).jActivity;
} else {
return 0;
}
}
private static void timesliceCarbWriter(Map<Double, Iob> timeslices, double thistime, double carbs) {
// offset for carb action time??
Iob tempiob;
if (timeslices.containsKey(thistime)) {
tempiob = timeslices.get(thistime);
tempiob.cob = tempiob.cob + carbs;
} else {
tempiob = new Iob();
tempiob.timestamp = (long) thistime;
// tempiob.date = new Date((long)thistime);
tempiob.cob = carbs;
}
timeslices.put(thistime, tempiob);
}
private static void timesliceInsulinWriter(Map<Double, Iob> timeslices, Iob thisiob, double thistime) {
if (thisiob.iob > 0) {
if (timeslices.containsKey(thistime)) {
Iob tempiob = timeslices.get(thistime);
tempiob.iob += thisiob.iob;
tempiob.jActivity+= thisiob.jActivity;
timeslices.put(thistime, tempiob);
} else {
thisiob.timestamp = (long) thistime;
// thisiob.date = new Date((long)thistime);
timeslices.put(thistime, thisiob); // first entry at timeslice so put the record in as is
}
}
}
// NEW NEW NEW
public static List<Iob> ioBForGraph_new(int number, double startTime) {
// Log.d(TAG, "Processing iobforgraph2: main ");
JoH.benchmark_method_start();
final boolean multipleInsulins = MultipleInsulins.isEnabled();
final boolean useBasal = MultipleInsulins.useBasalActivity();
// number param currently ignored
// 10 hours max look or from insulin manager if enabled
final double dontLookThisFar = MultipleInsulins.isEnabled() ? MINUTE_IN_MS * InsulinManager.getMaxEffect(true) : 10 * HOUR_IN_MS;
// look back the longest effect period of all enabled insulin profiles (startTime is always 24h behind NOW)
List<Treatments> theTreatments = latestForGraph(2000, startTime - dontLookThisFar);
Log.d(TAG,"TREATMENT LIST: "+theTreatments.size()+" "+JoH.dateTimeText((long)(startTime - dontLookThisFar)));
if (theTreatments.size() == 0) return null;
int counter = 0; // iteration counter
final double step_minutes = 5;
final double stepms = step_minutes * MINUTE_IN_MS; // 300s = 5 mins
double mytime = startTime;
double tendtime = startTime;
final double carb_delay_minutes = Profile.carbDelayMinutes(mytime); // not likely a time dependent parameter
final double carb_delay_ms_stepped = ((long) (carb_delay_minutes / step_minutes)) * step_minutes * MINUTE_IN_MS;
Log.d(TAG, "Carb delay ms: " + carb_delay_ms_stepped);
Map<String, Boolean> carbsEaten = new HashMap<String, Boolean>();
// linear array populated as needed and layered by each treatment etc
SortedMap<Double, Iob> timeslices = new TreeMap<Double, Iob>();
Iob calcreply;
// First process all IoB calculations
for (Treatments thisTreatment : theTreatments) {
// early optimisation exclusion
mytime = ((long) (thisTreatment.timestamp / stepms)) * stepms; // effects of treatment occur only after it is given / fit to slot time
tendtime = mytime + 36 * HOUR_IN_MS; // 36 hours max look (24h history plus 12h forecast)
if (tendtime > startTime + 30 * HOUR_IN_MS)
tendtime = startTime + 30 * HOUR_IN_MS; // dont look more than 6h in future // TODO review time limit
if (thisTreatment.insulin > 0) {
// lay down insulin on board
do {
calcreply = calcTreatment(thisTreatment, mytime, useBasal);
calcreply.jActivity *= step_minutes; // has to be multiplied because derivation function of IOB calculates a step_minutes lower activity as the "old" logic
calcreply.jActivity *= Profile.getSensitivity(mytime);
if (mytime >= startTime) {
timesliceInsulinWriter(timeslices, calcreply, mytime);
}
mytime = mytime + stepms; // advance time counter
} while ((mytime < tendtime) &&
((calcreply.iob == 0) || (calcreply.iob > 0.01)));
}
} // per insulin treatment
// legacy jActivity calculation
if (!multipleInsulins) {
Log.d(TAG, "Single insulin type iteration counter: " + counter);
// evaluate insulin impact
Iob lastiob = null;
for (Map.Entry<Double, Iob> entry : timeslices.entrySet()) {
Iob thisiob = entry.getValue();
if (lastiob != null) {
if ((thisiob.iob != 0) || (lastiob.iob != 0)) {
if (thisiob.iob < lastiob.iob) {
// decaying iob
thisiob.jActivity = (lastiob.iob - thisiob.iob) * Profile.getSensitivity(thisiob.timestamp);
} else {
// more insulin added
thisiob.jActivity = 0; // TODO THIS IS NOT RIGHT IT MISSES ONE DECAY STEP
}
}
}
//Log.d(TAG,"iobinfo2 iob debug: "+JoH.qs(thisiob.timestamp)+" C:"+JoH.qs(thisiob.cob,4)+" I:"+JoH.qs(thisiob.iob,4)+" CA:"+JoH.qs(thisiob.jCarbImpact)+" IA:"+JoH.qs(thisiob.jActivity));
counter++;
lastiob = thisiob;
}
//
}
// calculate carb treatments
for (Treatments thisTreatment : theTreatments) {
if (thisTreatment.carbs > 0) {
mytime = ((long) (thisTreatment.timestamp / stepms)) * stepms; // effects of treatment occur only after it is given / fit to slot time
tendtime = mytime + 6 * HOUR_IN_MS; // 6 hours max look
double cob_time = mytime + carb_delay_ms_stepped;
double stomachDiff = ((Profile.getCarbAbsorptionRate(cob_time) * stepms) / HOUR_IN_MS); // initial value
double newdelayedCarbs = 0;
double cob_remain = thisTreatment.carbs;
while ((cob_remain > 0) && (stomachDiff > 0) && (cob_time < tendtime)) {
if (cob_time >= startTime) {
timesliceCarbWriter(timeslices, cob_time, cob_remain);
}
cob_time += stepms;
stomachDiff = ((Profile.getCarbAbsorptionRate(cob_time) * stepms) / HOUR_IN_MS);
cob_remain -= stomachDiff;
newdelayedCarbs = (timesliceIactivityAtTime(timeslices, cob_time) * Profile.getLiverSensRatio(cob_time) / Profile.getSensitivity(cob_time)) * Profile.getCarbRatio(cob_time);
if (newdelayedCarbs > 0) {
final double maximpact = stomachDiff * Profile.maxLiverImpactRatio(cob_time);
if (newdelayedCarbs > maximpact) newdelayedCarbs = maximpact;
cob_remain += newdelayedCarbs; // add back on liverfactor adjustment
}
counter++;
}
// end record if not present
if (cob_time >= startTime) {
timesliceCarbWriter(timeslices, cob_time, 0);
}
}
}
// evaluate carb impact
Iob lastiob = null;
for (Map.Entry<Double, Iob> entry : timeslices.entrySet()) {
Iob thisiob = entry.getValue();
if (lastiob != null) {
if ((thisiob.cob != 0 || (lastiob.cob != 0))) {
if (thisiob.cob < lastiob.cob) {
// decaying cob
thisiob.jCarbImpact = (lastiob.cob - thisiob.cob) / Profile.getCarbRatio(thisiob.timestamp) * Profile.getSensitivity(thisiob.timestamp);
} else {
// more carbs added
thisiob.jCarbImpact = 0; // TODO THIS IS NOT RIGHT IT MISSES ONE DECAY STEP
}
}
}
// Log.d(TAG,"iobinfo2carb debug: "+JoH.qs(thisiob.timestamp)+" C:"+JoH.qs(thisiob.cob,4)+" I:"+JoH.qs(thisiob.iob,4)+" CA:"+JoH.qs(thisiob.jCarbImpact)+" IA:"+JoH.qs(thisiob.jActivity));
counter++;
lastiob = thisiob;
}
Log.d(TAG, "second iteration counter: " + counter);
Log.d(TAG, "Timeslices size: " + timeslices.size());
JoH.benchmark_method_end();
return new ArrayList<Iob>(timeslices.values());
}
/* /// OLD ONE BELOW
public static List<Iob> ioBForGraph_old(int number, double startTime) {
JoH.benchmark_method_start();
//JoH.benchmark_method_end();
Log.d(TAG, "Processing iobforgraph: main ");
// get all treatments from 24 hours earlier than our current time
List<Treatments> theTreatments = latestForGraph(2000, startTime - 86400000);
Map<String, Boolean> carbsEaten = new HashMap<String, Boolean>();
// this could be much more optimized with linear array instead of loops
final double dontLookThisFar = 10 * 60 * 60 * 1000; // 10 hours max look
double stomachCarbs = 0;
final double step_minutes = 10;
final double stepms = step_minutes * 60 * 1000; // 600s = 10 mins
if (theTreatments.size() == 0) return null;
Map ioblookup = new HashMap<Double, Double>(); // store for iob total vs time
List<Iob> responses = new ArrayList<Iob>();
Iob calcreply;
double mytime = startTime;
double lastmytime = mytime;
double max_look_time = startTime + (30 * 60 * 60 * 1000);
int counter = 0;
// 30 hours max look at
while ((responses.size() < number) && (mytime < max_look_time)) {
double lastDecayedBy = 0, isDecaying = 0, delayMinutes = 0; // reset per time slot
double totalIOB = 0, totalCOB = 0, totalActivity = 0;
// per treatment per timeblock
for (Treatments thisTreatment : theTreatments) {
// early optimisation exclusion
if ((thisTreatment.timestamp <= mytime) && (mytime - thisTreatment.timestamp) < dontLookThisFar) {
calcreply = calcTreatment(thisTreatment, mytime, lastDecayedBy); // was last decayed by but that offset wrongly??
totalIOB += calcreply.iob;
//totalCOB += calcreply.cob;
totalActivity += calcreply.activity;
} // endif excluding a treatment
} // per treatment
//
ioblookup.put(mytime, totalIOB);
if (ioblookup.containsKey(lastmytime)) {
double iobdiff = (double) ioblookup.get(lastmytime) - totalIOB;
if (iobdiff < 0) iobdiff = 0;
if ((iobdiff != 0) || (totalActivity != 0)) {
Log.d(TAG, "New IOB diffi @: " + JoH.qs(mytime) + " = " + JoH.qs(iobdiff) + " old activity: " + JoH.qs(totalActivity));
}
totalActivity = iobdiff; // WARNING OVERRIDE
}
double stomachDiff = ((Profile.getCarbAbsorptionRate(mytime) * stepms) / (60 * 60 * 1000));
double newdelayedCarbs = (totalActivity * Profile.getLiverSensRatio(mytime) / Profile.getSensitivity(mytime)) * Profile.getCarbRatio(mytime);
// calculate carbs
for (Treatments thisTreatment : theTreatments) {
// early optimisation exclusion
if ((thisTreatment.timestamp <= mytime) && (mytime - thisTreatment.timestamp) < dontLookThisFar) {
if ((thisTreatment.carbs > 0) && (thisTreatment.timestamp < mytime)) {
// factor carbs delay in above when complete
if (!carbsEaten.containsKey(thisTreatment.uuid)) {
carbsEaten.put(thisTreatment.uuid, true);
stomachCarbs = stomachCarbs + thisTreatment.carbs;
stomachCarbs = stomachCarbs + stomachDiff; // offset first subtraction
// pre-subtract for granularity or just reduce granularity
Log.d(TAG, "newcarbs: " + thisTreatment.carbs + " " + thisTreatment.uuid + " @ " + thisTreatment.timestamp + " mytime: " + JoH.qs(mytime) + " diff: " + JoH.qs((thisTreatment.timestamp - mytime) / 1000) + " stomach: " + JoH.qs(stomachCarbs));
}
lastCarbs = thisTreatment;
CobCalc cCalc = cobCalc(thisTreatment, lastDecayedBy, mytime); // need to handle last decayedby shunting
double decaysin_hr = (cCalc.decayedBy - mytime) / 1000 / 60 / 60;
if (decaysin_hr > -10) {
// units: BG
double avgActivity = totalActivity;
// units: g = BG * scalar / BG / U * g / U
double delayedCarbs = (avgActivity * Profile.getLiverSensRatio(mytime) / Profile.getSensitivity(mytime)) * Profile.getCarbRatio(mytime);
delayMinutes = Math.round(delayedCarbs / (Profile.getCarbAbsorptionRate(mytime) / 60));
Log.d(TAG, "Avg activity: " + JoH.qs(avgActivity) + " Decaysin_hr: " + JoH.qs(decaysin_hr) + " delay minutes: " + JoH.qs(delayMinutes) + " delayed carbs: " + JoH.qs(delayedCarbs));
if (delayMinutes > 0) {
Log.d(TAG, "Delayed Carbs: " + JoH.qs(delayedCarbs) + " Delay minutes: " + JoH.qs(delayMinutes) + " Average activity: " + JoH.qs(avgActivity));
cCalc.decayedBy += delayMinutes * 60 * 1000;
decaysin_hr = (cCalc.decayedBy - mytime) / 1000 / 60 / 60;
}
}
lastDecayedBy = cCalc.decayedBy;
if (decaysin_hr > 0) {
Log.d(TAG, "cob: Adding " + JoH.qs(delayMinutes) + " minutes to decay of " + JoH.qs(thisTreatment.carbs) + "g bolus at " + JoH.qs(thisTreatment.timestamp));
totalCOB += Math.min(thisTreatment.carbs, decaysin_hr * Profile.getCarbAbsorptionRate(thisTreatment.timestamp));
Log.d(TAG, "cob: " + JoH.qs(Math.min(cCalc.initialCarbs, decaysin_hr * Profile.getCarbAbsorptionRate(thisTreatment.timestamp)))
+ " inital carbs:" + JoH.qs(cCalc.initialCarbs) + " decaysin_hr:" + JoH.qs(decaysin_hr) + " absorbrate:" + JoH.qs(Profile.getCarbAbsorptionRate(thisTreatment.timestamp)));
isDecaying = cCalc.isDecaying;
} else {
// totalCOB = 0; //nix this?
}
} // if this treatment has carbs
} // end if processing this treatment
} // per carb treatment
if (stomachCarbs > 0) {
Log.d(TAG, "newcarbs Stomach Diff: " + JoH.qs(stomachDiff) + " Old total: " + JoH.qs(stomachCarbs) + " Delayed carbs: " + JoH.qs(newdelayedCarbs));
stomachCarbs = stomachCarbs - stomachDiff;
if (newdelayedCarbs > 0) {
double maximpact = stomachDiff * Profile.maxLiverImpactRatio(mytime);
if (newdelayedCarbs > maximpact) newdelayedCarbs = maximpact;
stomachCarbs = stomachCarbs + newdelayedCarbs; // add back on liverfactor ones
}
if (stomachCarbs < 0) stomachCarbs = 0;
}
if ((totalIOB > Profile.minimum_shown_iob) || (totalCOB > Profile.minimum_shown_cob) || (stomachCarbs > Profile.minimum_shown_cob)) {
Iob thisrecord = new Iob();
thisrecord.timestamp = (long) mytime;
thisrecord.iob = totalIOB;
thisrecord.activity = totalActivity; // hacky cruft
thisrecord.cob = stomachCarbs;
thisrecord.jCarbImpact = 0; // calculated below
thisrecord.rawCarbImpact = (isDecaying * Profile.getSensitivity(mytime)) / Profile.getCarbRatio(mytime) * Profile.getCarbAbsorptionRate(mytime) / 60;
// don't get confused with cob totals from previous treatments
if ((responses.size() > 0) && (Math.abs(responses.get(responses.size() - 1).timestamp - thisrecord.timestamp) <= stepms)) {
double cobdiff = responses.get(responses.size() - 1).cob - thisrecord.cob;
if (cobdiff > 0) {
thisrecord.jCarbImpact = (cobdiff / Profile.getCarbRatio(mytime)) * Profile.getSensitivity(mytime);
}
double iobdiff = responses.get(responses.size() - 1).iob - totalIOB;
if (iobdiff > 0) {
thisrecord.jActivity = (iobdiff * Profile.getSensitivity(mytime));
}
}
Log.d(TAG, "added record: cob raw impact: " + Double.toString(thisrecord.rawCarbImpact) + " Isdecaying: "
+ JoH.qs(isDecaying) + " jCarbImpact: " + JoH.qs(thisrecord.jCarbImpact) +
" jActivity: " + JoH.qs(thisrecord.jActivity) + " old activity: " + JoH.qs(thisrecord.activity));
responses.add(thisrecord);
}
lastmytime = mytime;
mytime = mytime + stepms;
counter++;
} // while time period in range
Log.d(TAG, "Finished Processing iobforgraph: main - processed: " + Integer.toString(counter) + " Timeslot records");
JoH.benchmark_method_end();
return responses;
}*/
public String getBestShortText() {
if (!eventType.equals(DEFAULT_EVENT_TYPE)) {
return eventType;
} else {
if (hasInsulinInjections()) {
return getInsulinInjectionsShortString()
+ (noteHasContent() ? (" " + notes) : "");
} else {
return noteHasContent() ? notes : "Treatment";
}
}
}
public String toJSON() {
JSONObject jsonObject = new JSONObject();
try {
jsonObject.put("uuid", uuid);
jsonObject.put("insulin", insulin);
jsonObject.put("insulinJSON", insulinJSON);
jsonObject.put("carbs", carbs);
jsonObject.put("timestamp", timestamp);
jsonObject.put("notes", notes);
jsonObject.put("enteredBy", enteredBy);
return jsonObject.toString();
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return "";
}
}
private static final double MAX_SMB_UNITS = 0.3;
private static final double MAX_OPENAPS_SMB_UNITS = 0.4;
public boolean likelySMB() {
return (carbs == 0 && insulin > 0
&& ((insulin <= MAX_SMB_UNITS && (notes == null || notes.length() == 0)) || (enteredBy != null && enteredBy.startsWith("openaps:") && insulin <= MAX_OPENAPS_SMB_UNITS)));
}
public boolean noteOnly() {
return carbs == 0 && insulin == 0 && noteHasContent();
}
public boolean hasContent() {
return insulin != 0 || carbs != 0 || noteHasContent() || !isEventTypeDefault();
}
public boolean noteHasContent() {
return notes != null && notes.length() > 0;
}
public boolean isEventTypeDefault() {
return eventType == null || eventType.equalsIgnoreCase(DEFAULT_EVENT_TYPE);
}
public static boolean matchUUID(final List<Treatments> treatments, final String uuid) {
for (final Treatments treatment : treatments) {
if (treatment.uuid.equalsIgnoreCase(uuid)) return true;
}
return false;
}
public String toS() {
Gson gson = new GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()
.registerTypeAdapter(Date.class, new DateTypeAdapter())
.serializeSpecialFloatingPointValues()
.create();
return gson.toJson(this);
}
}