1253 lines
54 KiB
Java
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);
|
|
}
|
|
}
|
|
|
|
|
|
|