package com.eveningoutpost.dexdrip.Models; import android.content.Context; import android.content.SharedPreferences; import android.os.AsyncTask; import android.os.PowerManager; import android.preference.PreferenceManager; import android.provider.BaseColumns; 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.BestGlucose; import com.eveningoutpost.dexdrip.GcmActivity; import com.eveningoutpost.dexdrip.Home; import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.EGVRecord; import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.SensorRecord; import com.eveningoutpost.dexdrip.Models.UserError.Log; import com.eveningoutpost.dexdrip.R; import com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService; import com.eveningoutpost.dexdrip.Services.SyncService; import com.eveningoutpost.dexdrip.ShareModels.ShareUploadableBg; import com.eveningoutpost.dexdrip.UtilityModels.BgGraphBuilder; import com.eveningoutpost.dexdrip.UtilityModels.BgSendQueue; import com.eveningoutpost.dexdrip.UtilityModels.Constants; import com.eveningoutpost.dexdrip.UtilityModels.Inevitable; import com.eveningoutpost.dexdrip.UtilityModels.Notifications; import com.eveningoutpost.dexdrip.UtilityModels.Pref; import com.eveningoutpost.dexdrip.UtilityModels.UploaderQueue; import com.eveningoutpost.dexdrip.UtilityModels.WholeHouse; import com.eveningoutpost.dexdrip.calibrations.CalibrationAbstract; import com.eveningoutpost.dexdrip.messages.BgReadingMessage; import com.eveningoutpost.dexdrip.messages.BgReadingMultiMessage; import com.eveningoutpost.dexdrip.utils.DexCollectionType; import com.eveningoutpost.dexdrip.utils.SqliteRejigger; import com.eveningoutpost.dexdrip.wearintegration.WatchUpdaterService; 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.squareup.wire.Wire; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Random; import java.util.UUID; import static com.eveningoutpost.dexdrip.calibrations.PluggableCalibration.getCalibrationPluginFromPreferences; import static com.eveningoutpost.dexdrip.calibrations.PluggableCalibration.newCloseSensorData; @Table(name = "BgReadings", id = BaseColumns._ID) public class BgReading extends Model implements ShareUploadableBg { private final static String TAG = BgReading.class.getSimpleName(); private final static String TAG_ALERT = TAG + " AlertBg"; private final static String PERSISTENT_HIGH_SINCE = "persistent_high_since"; public static final double AGE_ADJUSTMENT_TIME = 86400000 * 1.9; public static final double AGE_ADJUSTMENT_FACTOR = .45; //TODO: Have these as adjustable settings!! public final static double BESTOFFSET = (60000 * 0); // Assume readings are about x minutes off from actual! public static final int BG_READING_ERROR_VALUE = 38; // error marker public static final int BG_READING_MINIMUM_VALUE = 39; public static final int BG_READING_MAXIMUM_VALUE = 400; private static volatile long earliest_backfill = 0; @Column(name = "sensor", index = true) public Sensor sensor; @Column(name = "calibration", index = true, onDelete = Column.ForeignKeyAction.CASCADE) public Calibration calibration; @Expose @Column(name = "timestamp", index = true) public long timestamp; @Expose @Column(name = "time_since_sensor_started") public double time_since_sensor_started; @Expose @Column(name = "raw_data") public volatile double raw_data; @Expose @Column(name = "filtered_data") public double filtered_data; @Expose @Column(name = "age_adjusted_raw_value") public double age_adjusted_raw_value; @Expose @Column(name = "calibration_flag") public boolean calibration_flag; @Expose @Column(name = "calculated_value") public double calculated_value; @Expose @Column(name = "filtered_calculated_value") public double filtered_calculated_value; @Expose @Column(name = "calculated_value_slope") public double calculated_value_slope; @Expose @Column(name = "a") public double a; @Expose @Column(name = "b") public double b; @Expose @Column(name = "c") public double c; @Expose @Column(name = "ra") public double ra; @Expose @Column(name = "rb") public double rb; @Expose @Column(name = "rc") public double rc; @Expose // TODO unification with wear support ConflictAction.REPLACE for wear, done with rejig below @Column(name = "uuid", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE) public String uuid; @Expose @Column(name = "calibration_uuid") public String calibration_uuid; @Expose @Column(name = "sensor_uuid", index = true) public String sensor_uuid; // mapped to the no longer used "synced" to keep DB Scheme compatible @Expose @Column(name = "snyced") public boolean ignoreForStats; @Expose @Column(name = "raw_calculated") public double raw_calculated; @Expose @Column(name = "hide_slope") public boolean hide_slope; @Expose @Column(name = "noise") public String noise; @Expose @Column(name = "dg_mgdl") public double dg_mgdl = 0d; @Expose @Column(name = "dg_slope") public double dg_slope = 0d; @Expose @Column(name = "dg_delta_name") public String dg_delta_name; @Expose @Column(name = "source_info") public volatile String source_info; public synchronized static void updateDB() { final String[] updates = new String[]{"ALTER TABLE BgReadings ADD COLUMN dg_mgdl REAL;", "ALTER TABLE BgReadings ADD COLUMN dg_slope REAL;", "ALTER TABLE BgReadings ADD COLUMN dg_delta_name TEXT;", "ALTER TABLE BgReadings ADD COLUMN source_info TEXT;"}; for (String patch : updates) { try { SQLiteUtils.execSql(patch); } catch (Exception e) { } } // needs different handling on wear if (JoH.areWeRunningOnAndroidWear()) { BgSendQueue.emptyQueue(); SqliteRejigger.rejigSchema("BgReadings", "uuid TEXT UNIQUE ON CONFLICT FAIL", "uuid TEXT UNIQUE ON CONFLICT REPLACE"); SqliteRejigger.rejigSchema("BgReadings", "uuid TEXT UNIQUE ON CONFLICT IGNORE", "uuid TEXT UNIQUE ON CONFLICT REPLACE"); SqliteRejigger.rejigSchema("BgSendQueue", "BgReadings_temp", "BgReadings"); } } public double getDg_mgdl(){ if(dg_mgdl != 0) return dg_mgdl; return calculated_value; } public double getDg_slope(){ if(dg_mgdl != 0) return dg_slope; if(calculated_value_slope !=0) return calculated_value_slope; return currentSlope(); } public String getDg_deltaName(){ if(dg_mgdl != 0 && dg_delta_name != null) return dg_delta_name; return slopeName(); } public double calculated_value_mmol() { return mmolConvert(calculated_value); } public void injectDisplayGlucose(BestGlucose.DisplayGlucose displayGlucose) { //displayGlucose can be null. E.g. when out of order values come in if (displayGlucose != null) { if (Math.abs(displayGlucose.timestamp - timestamp) < Constants.MINUTE_IN_MS * 10) { dg_mgdl = displayGlucose.mgdl; dg_slope = displayGlucose.slope; dg_delta_name = displayGlucose.delta_name; // TODO we probably should reflect the display glucose delta here as well for completeness this.save(); } else { if (JoH.ratelimit("cannotinjectdg", 30)) { UserError.Log.e(TAG, "Cannot inject display glucose value as time difference too great: " + JoH.dateTimeText(displayGlucose.timestamp) + " vs " + JoH.dateTimeText(timestamp)); } } } } public double mmolConvert(double mgdl) { return mgdl * Constants.MGDL_TO_MMOLL; } public String displayValue(Context context) { final String unit = Pref.getString("units", "mgdl"); final DecimalFormat df = new DecimalFormat("#"); final double this_value = getDg_mgdl(); if (this_value >= 400) { return "HIGH"; } else if (this_value >= 40) { if (unit.equals("mgdl")) { df.setMaximumFractionDigits(0); return df.format(this_value); } else { df.setMaximumFractionDigits(1); return df.format(mmolConvert(this_value)); } } else { return "LOW"; // TODO doesn't understand special low values } } public static double activeSlope() { BgReading bgReading = BgReading.lastNoSenssor(); if (bgReading != null) { double slope = (2 * bgReading.a * (new Date().getTime() + BESTOFFSET)) + bgReading.b; Log.i(TAG, "ESTIMATE SLOPE" + slope); return slope; } return 0; } public static double activePrediction() { BgReading bgReading = BgReading.lastNoSenssor(); if (bgReading != null) { double currentTime = new Date().getTime(); if (currentTime >= bgReading.timestamp + (60000 * 7)) { currentTime = bgReading.timestamp + (60000 * 7); } double time = currentTime + BESTOFFSET; return ((bgReading.a * time * time) + (bgReading.b * time) + bgReading.c); } return 0; } public static double calculateSlope(BgReading current, BgReading last) { if (current.timestamp == last.timestamp || current.calculated_value == last.calculated_value) { return 0; } else { return (last.calculated_value - current.calculated_value) / (last.timestamp - current.timestamp); } } public static double currentSlope() { return currentSlope(Home.get_follower()); } public static double currentSlope(boolean is_follower) { List last_2 = BgReading.latest(2, is_follower); if ((last_2 != null) && (last_2.size() == 2)) { double slope = calculateSlope(last_2.get(0), last_2.get(1)); return slope; } else { return 0d; } } //*******CLASS METHODS***********// // Dexcom Bluetooth Share public static void create(EGVRecord[] egvRecords, long addativeOffset, Context context) { for (EGVRecord egvRecord : egvRecords) { BgReading.create(egvRecord, addativeOffset, context); } } public static void create(SensorRecord[] sensorRecords, long addativeOffset, Context context) { for (SensorRecord sensorRecord : sensorRecords) { BgReading.create(sensorRecord, addativeOffset, context); } } public static void create(SensorRecord sensorRecord, long addativeOffset, Context context) { Log.i(TAG, "create: gonna make some sensor records: " + sensorRecord.getUnfiltered()); if (BgReading.is_new(sensorRecord, addativeOffset)) { BgReading bgReading = new BgReading(); Sensor sensor = Sensor.currentSensor(); Calibration calibration = Calibration.getForTimestamp(sensorRecord.getSystemTime().getTime() + addativeOffset); if (sensor != null && calibration != null) { bgReading.sensor = sensor; bgReading.sensor_uuid = sensor.uuid; bgReading.calibration = calibration; bgReading.calibration_uuid = calibration.uuid; bgReading.raw_data = (sensorRecord.getUnfiltered() / 1000); bgReading.filtered_data = (sensorRecord.getFiltered() / 1000); bgReading.timestamp = sensorRecord.getSystemTime().getTime() + addativeOffset; if (bgReading.timestamp > new Date().getTime()) { return; } bgReading.uuid = UUID.randomUUID().toString(); bgReading.time_since_sensor_started = bgReading.timestamp - sensor.started_at; bgReading.calculateAgeAdjustedRawValue(); bgReading.save(); } } } // Dexcom Bluetooth Share public static void create(EGVRecord egvRecord, long addativeOffset, Context context) { BgReading bgReading = BgReading.getForTimestamp(egvRecord.getSystemTime().getTime() + addativeOffset); Log.i(TAG, "create: Looking for BG reading to tag this thing to: " + egvRecord.getBGValue()); if (bgReading != null) { bgReading.calculated_value = egvRecord.getBGValue(); if (egvRecord.getBGValue() <= 13) { Calibration calibration = bgReading.calibration; double firstAdjSlope = calibration.first_slope + (calibration.first_decay * (Math.ceil(new Date().getTime() - calibration.timestamp) / (1000 * 60 * 10))); double calSlope = (calibration.first_scale / firstAdjSlope) * 1000; double calIntercept = ((calibration.first_scale * calibration.first_intercept) / firstAdjSlope) * -1; bgReading.raw_calculated = (((calSlope * bgReading.raw_data) + calIntercept) - 5); } Log.i(TAG, "create: NEW VALUE CALCULATED AT: " + bgReading.calculated_value); bgReading.calculated_value_slope = bgReading.slopefromName(egvRecord.getTrend().friendlyTrendName()); bgReading.noise = egvRecord.noiseValue(); String friendlyName = egvRecord.getTrend().friendlyTrendName(); if (friendlyName.compareTo("NONE") == 0 || friendlyName.compareTo("NOT_COMPUTABLE") == 0 || friendlyName.compareTo("NOT COMPUTABLE") == 0 || friendlyName.compareTo("OUT OF RANGE") == 0 || friendlyName.compareTo("OUT_OF_RANGE") == 0) { bgReading.hide_slope = true; } bgReading.save(); bgReading.find_new_curve(); bgReading.find_new_raw_curve(); //context.startService(new Intent(context, Notifications.class)); Notifications.start(); // this may not be needed as it is duplicated in handleNewBgReading BgSendQueue.handleNewBgReading(bgReading, "create", context); } } public static BgReading getForTimestamp(double timestamp) { Sensor sensor = Sensor.currentSensor(); if (sensor != null) { BgReading bgReading = new Select() .from(BgReading.class) .where("Sensor = ? ", sensor.getId()) .where("timestamp <= ?", (timestamp + (60 * 1000))) // 1 minute padding (should never be that far off, but why not) .where("calculated_value = 0") .where("raw_calculated = 0") .orderBy("timestamp desc") .executeSingle(); if (bgReading != null && Math.abs(bgReading.timestamp - timestamp) < (3 * 60 * 1000)) { //cool, so was it actually within 4 minutes of that bg reading? Log.i(TAG, "getForTimestamp: Found a BG timestamp match"); return bgReading; } } Log.d(TAG, "getForTimestamp: No luck finding a BG timestamp match"); return null; } // used in wear public static BgReading getForTimestampExists(double timestamp) { Sensor sensor = Sensor.currentSensor(); if (sensor != null) { BgReading bgReading = new Select() .from(BgReading.class) .where("Sensor = ? ", sensor.getId()) .where("timestamp <= ?", (timestamp + (60 * 1000))) // 1 minute padding (should never be that far off, but why not) .orderBy("timestamp desc") .executeSingle(); if (bgReading != null && Math.abs(bgReading.timestamp - timestamp) < (3 * 60 * 1000)) { //cool, so was it actually within 4 minutes of that bg reading? Log.i(TAG, "getForTimestamp: Found a BG timestamp match"); return bgReading; } } Log.d(TAG, "getForTimestamp: No luck finding a BG timestamp match"); return null; } public static BgReading getForPreciseTimestamp(long timestamp, long precision) { return getForPreciseTimestamp(timestamp, precision, true); } public static BgReading getForPreciseTimestamp(long timestamp, long precision, boolean lock_to_sensor) { final Sensor sensor = Sensor.currentSensor(); if ((sensor != null) || !lock_to_sensor) { final BgReading bgReading = new Select() .from(BgReading.class) .where(lock_to_sensor ? "Sensor = ?" : "timestamp > ?", (lock_to_sensor ? sensor.getId() : 0)) .where("timestamp <= ?", (timestamp + precision)) .where("timestamp >= ?", (timestamp - precision)) .orderBy("abs(timestamp - " + timestamp + ") asc") .executeSingle(); if (bgReading != null && Math.abs(bgReading.timestamp - timestamp) < precision) { //cool, so was it actually within precision of that bg reading? //Log.d(TAG, "getForPreciseTimestamp: Found a BG timestamp match"); return bgReading; } } Log.d(TAG, "getForPreciseTimestamp: No luck finding a BG timestamp match: " + JoH.dateTimeText((long) timestamp) + " precision:" + precision + " Sensor: " + ((sensor == null) ? "null" : sensor.getId())); return null; } public static boolean is_new(SensorRecord sensorRecord, long addativeOffset) { double timestamp = sensorRecord.getSystemTime().getTime() + addativeOffset; Sensor sensor = Sensor.currentSensor(); if (sensor != null) { BgReading bgReading = new Select() .from(BgReading.class) .where("Sensor = ? ", sensor.getId()) .where("timestamp <= ?", (timestamp + (60 * 1000))) // 1 minute padding (should never be that far off, but why not) .orderBy("timestamp desc") .executeSingle(); if (bgReading != null && Math.abs(bgReading.timestamp - timestamp) < (3 * 60 * 1000)) { //cool, so was it actually within 4 minutes of that bg reading? Log.i(TAG, "isNew; Old Reading"); return false; } } Log.i(TAG, "isNew: New Reading"); return true; } public static BgReading create(double raw_data, double filtered_data, Context context, Long timestamp) { return create(raw_data, filtered_data, context, timestamp, false); } public static BgReading create(double raw_data, double filtered_data, Context context, Long timestamp, boolean quick) { if (context == null) context = xdrip.getAppContext(); BgReading bgReading = new BgReading(); final Sensor sensor = Sensor.currentSensor(); if (sensor == null) { Log.i("BG GSON: ", bgReading.toS()); return bgReading; } if (raw_data == 0) { Log.e(TAG,"Warning: raw_data is 0 in BgReading.create()"); } Calibration calibration = Calibration.lastValid(); if (calibration == null) { Log.d(TAG, "create: No calibration yet"); bgReading.sensor = sensor; bgReading.sensor_uuid = sensor.uuid; bgReading.raw_data = (raw_data / 1000); bgReading.filtered_data = (filtered_data / 1000); bgReading.timestamp = timestamp; bgReading.uuid = UUID.randomUUID().toString(); bgReading.time_since_sensor_started = bgReading.timestamp - sensor.started_at; bgReading.calibration_flag = false; bgReading.calculateAgeAdjustedRawValue(); bgReading.save(); bgReading.perform_calculations(); BgSendQueue.sendToPhone(context); } else { Log.d(TAG, "Calibrations, so doing everything: " + calibration.uuid); bgReading = createFromRawNoSave(sensor, calibration, raw_data, filtered_data, timestamp); bgReading.save(); // used when we are not fast inserting data if (!quick) { bgReading.perform_calculations(); if (JoH.ratelimit("opportunistic-calibration", 60)) { BloodTest.opportunisticCalibration(); } //context.startService(new Intent(context, Notifications.class)); // allow this instead to be fired inside handleNewBgReading when noise will have been injected already } bgReading.postProcess(quick); } Log.i("BG GSON: ", bgReading.toS()); return bgReading; } public void postProcess(final boolean quick) { injectNoise(true); // Add noise parameter for nightscout injectDisplayGlucose(BestGlucose.getDisplayGlucose()); // Add display glucose for nightscout BgSendQueue.handleNewBgReading(this, "create", xdrip.getAppContext(), Home.get_follower(), quick); } public static BgReading createFromRawNoSave(Sensor sensor, Calibration calibration, double raw_data, double filtered_data, long timestamp) { final BgReading bgReading = new BgReading(); if (sensor == null) { sensor = Sensor.currentSensor(); if (sensor == null) { return bgReading; } } if (calibration == null) { calibration = Calibration.lastValid(); if (calibration == null) { return bgReading; } } bgReading.sensor = sensor; bgReading.sensor_uuid = sensor.uuid; bgReading.calibration = calibration; bgReading.calibration_uuid = calibration.uuid; bgReading.raw_data = (raw_data / 1000); bgReading.filtered_data = (filtered_data / 1000); bgReading.timestamp = timestamp; bgReading.uuid = UUID.randomUUID().toString(); bgReading.time_since_sensor_started = bgReading.timestamp - sensor.started_at; bgReading.calculateAgeAdjustedRawValue(); if (calibration.check_in) { double firstAdjSlope = calibration.first_slope + (calibration.first_decay * (Math.ceil(new Date().getTime() - calibration.timestamp) / (1000 * 60 * 10))); double calSlope = (calibration.first_scale / firstAdjSlope) * 1000; double calIntercept = ((calibration.first_scale * calibration.first_intercept) / firstAdjSlope) * -1; bgReading.calculated_value = (((calSlope * bgReading.raw_data) + calIntercept) - 5); bgReading.filtered_calculated_value = (((calSlope * bgReading.ageAdjustedFiltered()) + calIntercept) - 5); } else { BgReading lastBgReading = BgReading.last(); if (lastBgReading != null && lastBgReading.calibration != null) { Log.d(TAG, "Create calibration.uuid=" + calibration.uuid + " bgReading.uuid: " + bgReading.uuid + " lastBgReading.calibration_uuid: " + lastBgReading.calibration_uuid + " lastBgReading.calibration.uuid: " + lastBgReading.calibration.uuid); Log.d(TAG, "Create lastBgReading.calibration_flag=" + lastBgReading.calibration_flag + " bgReading.timestamp: " + bgReading.timestamp + " lastBgReading.timestamp: " + lastBgReading.timestamp + " lastBgReading.calibration.timestamp: " + lastBgReading.calibration.timestamp); Log.d(TAG, "Create lastBgReading.calibration_flag=" + lastBgReading.calibration_flag + " bgReading.timestamp: " + JoH.dateTimeText(bgReading.timestamp) + " lastBgReading.timestamp: " + JoH.dateTimeText(lastBgReading.timestamp) + " lastBgReading.calibration.timestamp: " + JoH.dateTimeText(lastBgReading.calibration.timestamp)); if (lastBgReading.calibration_flag == true && ((lastBgReading.timestamp + (60000 * 20)) > bgReading.timestamp) && ((lastBgReading.calibration.timestamp + (60000 * 20)) > bgReading.timestamp)) { lastBgReading.calibration.rawValueOverride(BgReading.weightedAverageRaw(lastBgReading.timestamp, bgReading.timestamp, lastBgReading.calibration.timestamp, lastBgReading.age_adjusted_raw_value, bgReading.age_adjusted_raw_value), xdrip.getAppContext()); newCloseSensorData(); } } if ((bgReading.raw_data != 0) && (bgReading.raw_data * 2 == bgReading.filtered_data)) { Log.wtf(TAG, "Filtered data is exactly double raw - this is completely wrong - dead transmitter? - blocking glucose calculation"); bgReading.calculated_value = 0; bgReading.filtered_calculated_value = 0; bgReading.hide_slope = true; } else if (!SensorSanity.isRawValueSane(bgReading.raw_data)) { Log.wtf(TAG, "Raw data fails sanity check! " + bgReading.raw_data); bgReading.calculated_value = 0; bgReading.filtered_calculated_value = 0; bgReading.hide_slope = true; } else { // calculate glucose number from raw final CalibrationAbstract.CalibrationData pcalibration; final CalibrationAbstract plugin = getCalibrationPluginFromPreferences(); // make sure do this only once if ((plugin != null) && ((pcalibration = plugin.getCalibrationData()) != null) && (Pref.getBoolean("use_pluggable_alg_as_primary", false))) { Log.d(TAG, "USING CALIBRATION PLUGIN AS PRIMARY!!!"); if (plugin.isCalibrationSane(pcalibration)) { bgReading.calculated_value = (pcalibration.slope * bgReading.age_adjusted_raw_value) + pcalibration.intercept; bgReading.filtered_calculated_value = (pcalibration.slope * bgReading.ageAdjustedFiltered()) + calibration.intercept; } else { UserError.Log.wtf(TAG, "Calibration plugin failed intercept sanity check: " + pcalibration.toS()); Home.toaststaticnext("Calibration plugin failed intercept sanity check"); } } else { bgReading.calculated_value = ((calibration.slope * bgReading.age_adjusted_raw_value) + calibration.intercept); bgReading.filtered_calculated_value = ((calibration.slope * bgReading.ageAdjustedFiltered()) + calibration.intercept); } updateCalculatedValueToWithinMinMax(bgReading); } } // LimiTTer can send 12 to indicate problem with NFC reading. if ((!calibration.check_in) && (raw_data == 12) && (filtered_data == 12)) { // store the raw value for sending special codes, note updateCalculatedValue would try to nix it bgReading.calculated_value = raw_data; bgReading.filtered_calculated_value = filtered_data; } return bgReading; } public static boolean isRawMarkerValue(final double raw_data) { return raw_data == BgReading.SPECIAL_G5_PLACEHOLDER || raw_data == BgReading.SPECIAL_RAW_NOT_AVAILABLE; } static void updateCalculatedValueToWithinMinMax(BgReading bgReading) { // TODO should this really be <10 other values also special?? if (bgReading.calculated_value < 10) { bgReading.calculated_value = BG_READING_ERROR_VALUE; bgReading.hide_slope = true; } else { bgReading.calculated_value = Math.min(BG_READING_MAXIMUM_VALUE, Math.max(BG_READING_MINIMUM_VALUE, bgReading.calculated_value)); } Log.i(TAG, "NEW VALUE CALCULATED AT: " + bgReading.calculated_value); } // Used by xDripViewer public static void create(Context context, double raw_data, double age_adjusted_raw_value, double filtered_data, Long timestamp, double calculated_bg, double calculated_current_slope, boolean hide_slope) { BgReading bgReading = new BgReading(); Sensor sensor = Sensor.currentSensor(); if (sensor == null) { Log.w(TAG, "No sensor, ignoring this bg reading"); return; } Calibration calibration = Calibration.lastValid(); if (calibration == null) { Log.d(TAG, "create: No calibration yet"); bgReading.sensor = sensor; bgReading.sensor_uuid = sensor.uuid; bgReading.raw_data = (raw_data / 1000); bgReading.age_adjusted_raw_value = age_adjusted_raw_value; bgReading.filtered_data = (filtered_data / 1000); bgReading.timestamp = timestamp; bgReading.uuid = UUID.randomUUID().toString(); bgReading.calculated_value = calculated_bg; bgReading.calculated_value_slope = calculated_current_slope; bgReading.hide_slope = hide_slope; bgReading.save(); bgReading.perform_calculations(); } else { Log.d(TAG, "Calibrations, so doing everything bgReading = " + bgReading); bgReading.sensor = sensor; bgReading.sensor_uuid = sensor.uuid; bgReading.calibration = calibration; bgReading.calibration_uuid = calibration.uuid; bgReading.raw_data = (raw_data / 1000); bgReading.age_adjusted_raw_value = age_adjusted_raw_value; bgReading.filtered_data = (filtered_data / 1000); bgReading.timestamp = timestamp; bgReading.uuid = UUID.randomUUID().toString(); bgReading.calculated_value = calculated_bg; bgReading.calculated_value_slope = calculated_current_slope; bgReading.hide_slope = hide_slope; bgReading.save(); } BgSendQueue.handleNewBgReading(bgReading, "create", context); Log.i("BG GSON: ", bgReading.toS()); } public static void pushBgReadingSyncToWatch(BgReading bgReading, boolean is_new) { Log.d(TAG, "pushTreatmentSyncToWatch Add treatment to UploaderQueue."); if (Pref.getBooleanDefaultFalse("wear_sync")) { if (UploaderQueue.newEntryForWatch(is_new ? "insert" : "update", bgReading) != null) { SyncService.startSyncService(3000); // sync in 3 seconds } } } public String displaySlopeArrow() { return slopeToArrowSymbol(this.dg_mgdl > 0 ? this.dg_slope * 60000 : this.calculated_value_slope * 60000); } public static String activeSlopeArrow() { double slope = (float) (BgReading.activeSlope() * 60000); return slopeToArrowSymbol(slope); } public static String slopeToArrowSymbol(double slope) { if (slope <= (-3.5)) { return "\u21ca";// ⇊ } else if (slope <= (-2)) { return "\u2193"; // ↓ } else if (slope <= (-1)) { return "\u2198"; // ↘ } else if (slope <= (1)) { return "\u2192"; // → } else if (slope <= (2)) { return "\u2197"; // ↗ } else if (slope <= (3.5)) { return "\u2191"; // ↑ } else { return "\u21c8"; // ⇈ } } public String slopeArrow() { return slopeToArrowSymbol(this.calculated_value_slope * 60000); } public String slopeName() { double slope_by_minute = calculated_value_slope * 60000; String arrow = "NONE"; if (slope_by_minute <= (-3.5)) { arrow = "DoubleDown"; } else if (slope_by_minute <= (-2)) { arrow = "SingleDown"; } else if (slope_by_minute <= (-1)) { arrow = "FortyFiveDown"; } else if (slope_by_minute <= (1)) { arrow = "Flat"; } else if (slope_by_minute <= (2)) { arrow = "FortyFiveUp"; } else if (slope_by_minute <= (3.5)) { arrow = "SingleUp"; } else if (slope_by_minute <= (40)) { arrow = "DoubleUp"; } if (hide_slope) { arrow = "NOT COMPUTABLE"; } return arrow; } public static String slopeName(double slope_by_minute) { String arrow = "NONE"; if (slope_by_minute <= (-3.5)) { arrow = "DoubleDown"; } else if (slope_by_minute <= (-2)) { arrow = "SingleDown"; } else if (slope_by_minute <= (-1)) { arrow = "FortyFiveDown"; } else if (slope_by_minute <= (1)) { arrow = "Flat"; } else if (slope_by_minute <= (2)) { arrow = "FortyFiveUp"; } else if (slope_by_minute <= (3.5)) { arrow = "SingleUp"; } else if (slope_by_minute <= (40)) { arrow = "DoubleUp"; } return arrow; } public static double slopefromName(String slope_name) { if (slope_name == null) return 0; double slope_by_minute = 0; if (slope_name.compareTo("DoubleDown") == 0) { slope_by_minute = -3.5; } else if (slope_name.compareTo("SingleDown") == 0) { slope_by_minute = -2; } else if (slope_name.compareTo("FortyFiveDown") == 0) { slope_by_minute = -1; } else if (slope_name.compareTo("Flat") == 0) { slope_by_minute = 0; } else if (slope_name.compareTo("FortyFiveUp") == 0) { slope_by_minute = 2; } else if (slope_name.compareTo("SingleUp") == 0) { slope_by_minute = 3.5; } else if (slope_name.compareTo("DoubleUp") == 0) { slope_by_minute = 4; } else if (isSlopeNameInvalid(slope_name)) { slope_by_minute = 0; } return slope_by_minute / 60000; } public static boolean isSlopeNameInvalid(String slope_name) { if (slope_name.compareTo("NOT_COMPUTABLE") == 0 || slope_name.compareTo("NOT COMPUTABLE") == 0 || slope_name.compareTo("OUT_OF_RANGE") == 0 || slope_name.compareTo("OUT OF RANGE") == 0 || slope_name.compareTo("NONE") == 0) { return true; } else { return false; } } // Get a slope arrow based on pure guessed defaults so we can show it prior to calibration public static String getSlopeArrowSymbolBeforeCalibration() { final List last = BgReading.latestUnCalculated(2); if ((last!=null) && (last.size()==2)) { final double guess_slope = 1; // This is the "Default" slope for Dex and LimiTTer final double time_delta = (last.get(0).timestamp-last.get(1).timestamp); if (time_delta<=(BgGraphBuilder.DEXCOM_PERIOD * 2)) { final double estimated_delta = (last.get(0).age_adjusted_raw_value * guess_slope) - (last.get(1).age_adjusted_raw_value * guess_slope); final double estimated_delta2 = (last.get(0).raw_data * guess_slope) - (last.get(1).raw_data * guess_slope); Log.d(TAG, "SlopeArrowBeforeCalibration: guess delta: " + estimated_delta + " delta2: " + estimated_delta2 + " timedelta: " + time_delta); return slopeToArrowSymbol(estimated_delta / (time_delta / 60000)); } else { return ""; } } else { return ""; } } public static boolean last_within_minutes(final int mins) { return last_within_millis(mins * 60000); } public static boolean last_within_millis(final long millis) { final BgReading reading = last(); return reading != null && ((JoH.tsl() - reading.timestamp) < millis); } public boolean within_millis(final long millis) { return ((JoH.tsl() - this.timestamp) < millis); } public boolean isStale() { return !within_millis(Home.stale_data_millis()); } public static BgReading last() { return BgReading.last(Home.get_follower()); } public static BgReading last(boolean is_follower) { if (is_follower) { return new Select() .from(BgReading.class) .where("calculated_value != 0") .where("raw_data != 0") // .where("timestamp <= ?", JoH.tsl()) .orderBy("timestamp desc") .executeSingle(); } else { Sensor sensor = Sensor.currentSensor(); if (sensor != null) { return new Select() .from(BgReading.class) .where("Sensor = ? ", sensor.getId()) .where("calculated_value != 0") .where("raw_data != 0") // .where("timestamp <= ?", JoH.tsl()) .orderBy("timestamp desc") .executeSingle(); } } return null; } public static List latest_by_size(int number) { final Sensor sensor = Sensor.currentSensor(); if (sensor == null) return null; return new Select() .from(BgReading.class) .where("Sensor = ? ", sensor.getId()) .where("raw_data != 0") .orderBy("timestamp desc") .limit(number) .execute(); } public static BgReading lastNoSenssor() { return new Select() .from(BgReading.class) .where("calculated_value != 0") .where("raw_data != 0") // .where("timestamp <= ?", JoH.tsl()) .orderBy("timestamp desc") .executeSingle(); } public static List latest(int number) { return latest(number, Home.get_follower()); } public static List latest(int number, boolean is_follower) { if (is_follower) { // exclude sensor information when working as a follower return new Select() .from(BgReading.class) .where("calculated_value != 0") .where("raw_data != 0") // .where("timestamp <= ?", JoH.tsl()) .orderBy("timestamp desc") .limit(number) .execute(); } else { Sensor sensor = Sensor.currentSensor(); if (sensor == null) { return null; } return new Select() .from(BgReading.class) .where("Sensor = ? ", sensor.getId()) .where("calculated_value != 0") .where("raw_data != 0") // .where("timestamp <= ?", JoH.tsl()) .orderBy("timestamp desc") .limit(number) .execute(); } } public static boolean isDataStale() { final BgReading last = lastNoSenssor(); if (last == null) return true; return JoH.msSince(last.timestamp) > Home.stale_data_millis(); } public static List latestUnCalculated(int number) { Sensor sensor = Sensor.currentSensor(); if (sensor == null) { return null; } return new Select() .from(BgReading.class) .where("Sensor = ? ", sensor.getId()) .where("raw_data != 0") .orderBy("timestamp desc") .limit(number) .execute(); } public static List latestForGraph(int number, double startTime) { return latestForGraph(number, (long) startTime, Long.MAX_VALUE); } public static List latestForGraph(int number, long startTime) { return latestForGraph(number, startTime, Long.MAX_VALUE); } public static List latestForGraph(int number, long startTime, long endTime) { return new Select() .from(BgReading.class) .where("timestamp >= " + Math.max(startTime, 0)) .where("timestamp <= " + endTime) .where("calculated_value != 0") .where("raw_data != 0") .orderBy("timestamp desc") .limit(number) .execute(); } public static List latestForGraphSensor(int number, long startTime, long endTime) { Sensor sensor = Sensor.currentSensor(); if (sensor == null) { return null; } return new Select() .from(BgReading.class) .where("Sensor = ? ", sensor.getId()) .where("timestamp >= " + Math.max(startTime, 0)) .where("timestamp <= " + endTime) .where("calculated_value != 0") .where("raw_data != 0") .where("calibration_uuid != \"\"") .orderBy("timestamp desc") .limit(number) .execute(); } public static List latestForSensorAsc(int number, long startTime, long endTime, boolean follower) { if (follower) { return new Select() .from(BgReading.class) .where("timestamp >= ?", Math.max(startTime, 0)) .where("timestamp <= ?", endTime) .where("calculated_value != 0") .where("raw_data != 0") .orderBy("timestamp asc") .limit(number) .execute(); } else { final Sensor sensor = Sensor.currentSensor(); if (sensor == null) { return null; } return new Select() .from(BgReading.class) .where("Sensor = ? ", sensor.getId()) .where("timestamp >= ?", Math.max(startTime, 0)) .where("timestamp <= ?", endTime) .where("calculated_value != 0") .where("raw_data != 0") .orderBy("timestamp asc") .limit(number) .execute(); } } public static List latestForSensorAsc(int number, long startTime, long endTime) { return latestForSensorAsc(number, startTime, endTime, false); } public static List latestForGraphAsc(int number, long startTime) {//KS return latestForGraphAsc(number, startTime, Long.MAX_VALUE); } public static List latestForGraphAsc(int number, long startTime, long endTime) {//KS return new Select() .from(BgReading.class) .where("timestamp >= " + Math.max(startTime, 0)) .where("timestamp <= " + endTime) .where("calculated_value != 0") .where("raw_data != 0") .orderBy("timestamp asc") .limit(number) .execute(); } public static BgReading readingNearTimeStamp(double startTime) { final double margin = (4 * 60 * 1000); final DecimalFormat df = new DecimalFormat("#"); df.setMaximumFractionDigits(1); return new Select() .from(BgReading.class) .where("timestamp >= " + df.format(startTime - margin)) .where("timestamp <= " + df.format(startTime + margin)) .where("calculated_value != 0") .where("raw_data != 0") .executeSingle(); } public static List last30Minutes() { double timestamp = (new Date().getTime()) - (60000 * 30); return new Select() .from(BgReading.class) .where("timestamp >= " + timestamp) .where("calculated_value != 0") .where("raw_data != 0") .orderBy("timestamp desc") .execute(); } public static boolean isDataSuitableForDoubleCalibration() { final List uncalculated = BgReading.latestUnCalculated(3); if (uncalculated.size() < 3) return false; final ProcessInitialDataQuality.InitialDataQuality idq = ProcessInitialDataQuality.getInitialDataQuality(uncalculated); if (!idq.pass) { UserError.Log.d(TAG, "Data quality failure for double calibration: " + idq.advice); } return idq.pass || Pref.getBooleanDefaultFalse("bypass_calibration_quality_check"); } public static List futureReadings() { double timestamp = new Date().getTime(); return new Select() .from(BgReading.class) .where("timestamp > " + timestamp) .orderBy("timestamp desc") .execute(); } // used in wear public static BgReading findByUuid(String uuid) { return new Select() .from(BgReading.class) .where("uuid = ?", uuid) .executeSingle(); } public static double estimated_bg(double timestamp) { timestamp = timestamp + BESTOFFSET; BgReading latest = BgReading.last(); if (latest == null) { return 0; } else { return (latest.a * timestamp * timestamp) + (latest.b * timestamp) + latest.c; } } public static double estimated_raw_bg(double timestamp) { timestamp = timestamp + BESTOFFSET; double estimate; BgReading latest = BgReading.last(); if (latest == null) { Log.i(TAG, "No data yet, assume perfect!"); estimate = 160; } else { estimate = (latest.ra * timestamp * timestamp) + (latest.rb * timestamp) + latest.rc; } Log.i(TAG, "ESTIMATE RAW BG" + estimate); return estimate; } public static void bgReadingInsertFromJson(String json) { bgReadingInsertFromJson(json, true); } private static void FixCalibration(BgReading bgr) { if (bgr.calibration_uuid == null || "".equals(bgr.calibration_uuid)) { Log.d(TAG, "Bgr with no calibration, doing nothing"); return; } Calibration calibration = Calibration.byuuid(bgr.calibration_uuid); if (calibration == null) { Log.i(TAG, "received Unknown calibration: " + bgr.calibration_uuid + " asking for sensor upate..."); GcmActivity.requestSensorCalibrationsUpdate(); } else { bgr.calibration = calibration; } } public BgReading noRawWillBeAvailable() { raw_data = SPECIAL_RAW_NOT_AVAILABLE; save(); return this; } public BgReading appendSourceInfo(String info) { if ((source_info == null) || (source_info.length() == 0)) { source_info = info; } else { if (!source_info.startsWith(info) && (!source_info.contains("::" + info))) { source_info += "::" + info; } else { UserError.Log.e(TAG, "Ignoring duplicate source info " + source_info + " -> " + info); } } return this; } public boolean isBackfilled() { return raw_data == SPECIAL_G5_PLACEHOLDER; } public boolean isRemote() { return filtered_data == SPECIAL_REMOTE_PLACEHOLDER; } public static final double SPECIAL_RAW_NOT_AVAILABLE = -0.1279; public static final double SPECIAL_G5_PLACEHOLDER = -0.1597; public static final double SPECIAL_FOLLOWER_PLACEHOLDER = -0.1486; public static final double SPECIAL_REMOTE_PLACEHOLDER = -0.1375; public static BgReading bgReadingInsertFromG5(double calculated_value, long timestamp) { return bgReadingInsertFromG5(calculated_value, timestamp, null); } // TODO can these methods be unified to reduce duplication // TODO remember to sync this with wear code base public static synchronized BgReading bgReadingInsertFromG5(double calculated_value, long timestamp, String sourceInfoAppend) { final Sensor sensor = Sensor.currentSensor(); if (sensor == null) { Log.w(TAG, "No sensor, ignoring this bg reading"); return null; } // TODO slope!! final BgReading existing = getForPreciseTimestamp(timestamp, Constants.MINUTE_IN_MS); if (existing == null) { final BgReading bgr = new BgReading(); bgr.sensor = sensor; bgr.sensor_uuid = sensor.uuid; bgr.time_since_sensor_started = JoH.msSince(sensor.started_at); // is there a helper for this? bgr.timestamp = timestamp; bgr.uuid = UUID.randomUUID().toString(); bgr.calculated_value = calculated_value; bgr.raw_data = SPECIAL_G5_PLACEHOLDER; // placeholder bgr.appendSourceInfo("G5 Native"); if (sourceInfoAppend != null && sourceInfoAppend.length() > 0) { bgr.appendSourceInfo(sourceInfoAppend); } bgr.save(); if (JoH.ratelimit("sync wakelock", 15)) { final PowerManager.WakeLock linger = JoH.getWakeLock("G5 Insert", 4000); } Inevitable.stackableTask("NotifySyncBgr", 3000, () -> notifyAndSync(bgr)); return bgr; } else { return existing; } } public static synchronized BgReading bgReadingInsertMedtrum(double calculated_value, long timestamp, String sourceInfoAppend, double raw_data) { final Sensor sensor = Sensor.currentSensor(); if (sensor == null) { Log.w(TAG, "No sensor, ignoring this bg reading"); return null; } // TODO slope!! final BgReading existing = getForPreciseTimestamp(timestamp, Constants.MINUTE_IN_MS); if (existing == null) { final BgReading bgr = new BgReading(); bgr.sensor = sensor; bgr.sensor_uuid = sensor.uuid; bgr.time_since_sensor_started = JoH.msSince(sensor.started_at); // is there a helper for this? bgr.timestamp = timestamp; bgr.uuid = UUID.randomUUID().toString(); bgr.calculated_value = calculated_value; bgr.raw_data = raw_data / 1000d; bgr.filtered_data = bgr.raw_data; if (sourceInfoAppend != null && sourceInfoAppend.equals("Backfill")) { bgr.raw_data = BgReading.SPECIAL_G5_PLACEHOLDER; } else { bgr.calculateAgeAdjustedRawValue(); } bgr.appendSourceInfo("Medtrum Native"); if (sourceInfoAppend != null && sourceInfoAppend.length() > 0) { bgr.appendSourceInfo(sourceInfoAppend); } bgr.save(); if (JoH.ratelimit("sync wakelock", 15)) { final PowerManager.WakeLock linger = JoH.getWakeLock("Medtrum Insert", 4000); } Inevitable.task("NotifySyncBgr" + bgr.timestamp, 3000, () -> notifyAndSync(bgr)); if (bgr.isBackfilled()) { handleResyncWearAfterBackfill(bgr.timestamp); } return bgr; } else { return existing; } } public static synchronized BgReading bgReadingInsertLibre2(double calculated_value, long timestamp, double raw_data) { final Sensor sensor = Sensor.currentSensor(); if (sensor == null) { Log.w(TAG, "No sensor, ignoring this bg reading"); return null; } // TODO slope!! final BgReading existing = getForPreciseTimestamp(timestamp, Constants.MINUTE_IN_MS); if (existing == null) { Calibration calibration = Calibration.lastValid(); final BgReading bgReading = new BgReading(); if (calibration == null) { Log.d(TAG, "create: No calibration yet"); bgReading.sensor = sensor; bgReading.sensor_uuid = sensor.uuid; bgReading.raw_data = raw_data; bgReading.age_adjusted_raw_value = raw_data; bgReading.filtered_data = raw_data; bgReading.timestamp = timestamp; bgReading.uuid = UUID.randomUUID().toString(); bgReading.calculated_value = calculated_value; bgReading.calculated_value_slope = 0; bgReading.hide_slope = false; bgReading.appendSourceInfo("Libre2 Native"); bgReading.find_slope(); bgReading.save(); bgReading.perform_calculations(); bgReading.postProcess(false); } else { Log.d(TAG, "Calibrations, so doing everything bgReading = " + bgReading); bgReading.sensor = sensor; bgReading.sensor_uuid = sensor.uuid; bgReading.calibration = calibration; bgReading.calibration_uuid = calibration.uuid; bgReading.raw_data = raw_data ; bgReading.age_adjusted_raw_value = raw_data; bgReading.filtered_data = raw_data; bgReading.timestamp = timestamp; bgReading.uuid = UUID.randomUUID().toString(); bgReading.calculated_value = ((calibration.slope * calculated_value) + calibration.intercept); bgReading.filtered_calculated_value = ((calibration.slope * bgReading.ageAdjustedFiltered()) + calibration.intercept); bgReading.calculated_value_slope = 0; bgReading.hide_slope = false; bgReading.appendSourceInfo("Libre2 Native"); BgReading.updateCalculatedValueToWithinMinMax(bgReading); bgReading.find_slope(); bgReading.save(); bgReading.postProcess(false); } return bgReading; } else { return existing; } } public static void handleResyncWearAfterBackfill(final long earliest) { if (earliest_backfill == 0 || earliest < earliest_backfill) earliest_backfill = earliest; if (WatchUpdaterService.isEnabled()) { Inevitable.task("wear-backfill-sync", 10000, () -> { WatchUpdaterService.startServiceAndResendDataIfNeeded(earliest_backfill); earliest_backfill = 0; }); } } public void setRemoteMarker() { filtered_data = SPECIAL_REMOTE_PLACEHOLDER; } public static void notifyAndSync(final BgReading bgr) { final boolean recent = bgr.isCurrent(); if (recent) { Notifications.start(); // may not be needed as this is duplicated in handleNewBgReading // probably not wanted for G5 internal values? //bgr.injectNoise(true); // Add noise parameter for nightscout //bgr.injectDisplayGlucose(BestGlucose.getDisplayGlucose()); // Add display glucose for nightscout } BgSendQueue.handleNewBgReading(bgr, "create", xdrip.getAppContext(), Home.get_follower(), !recent); // pebble and widget and follower } public static BgReading bgReadingInsertFromJson(String json, boolean do_notification) { return bgReadingInsertFromJson(json, do_notification, WholeHouse.isEnabled()); } public static BgReading bgReadingInsertFromJson(String json, boolean do_notification, boolean force_sensor) { if ((json == null) || (json.length() == 0)) { Log.e(TAG, "bgreadinginsertfromjson passed a null or zero length json"); return null; } final BgReading bgr = fromJSON(json); if (bgr != null) { try { if (readingNearTimeStamp(bgr.timestamp) == null) { FixCalibration(bgr); if (force_sensor) { final Sensor forced_sensor = Sensor.currentSensor(); if (forced_sensor != null) { bgr.sensor = forced_sensor; bgr.sensor_uuid = forced_sensor.uuid; } if (Pref.getBooleanDefaultFalse("illustrate_remote_data")) { bgr.setRemoteMarker(); } } final long now = JoH.tsl(); if (bgr.timestamp > now) { UserError.Log.wtf(TAG, "Received a bg reading that appears to be in the future: " + JoH.dateTimeText(bgr.timestamp) + " vs " + JoH.dateTimeText(now)); } bgr.save(); if (do_notification) { Notifications.start(); // this may not be needed as it fires in handleNewBgReading //xdrip.getAppContext().startService(new Intent(xdrip.getAppContext(), Notifications.class)); // alerts et al BgSendQueue.handleNewBgReading(bgr, "create", xdrip.getAppContext(), Home.get_follower()); // pebble and widget and follower } } else { Log.d(TAG, "Ignoring duplicate bgr record due to timestamp: " + json); } } catch (Exception e) { Log.d(TAG, "Could not save BGR: " + e.toString()); } } else { Log.e(TAG,"Got null bgr from json"); } return bgr; } // TODO this method shares some code with above.. merge public static void bgReadingInsertFromInt(int value, long timestamp, boolean do_notification) { // TODO sanity check data! if ((value <= 0) || (timestamp <= 0)) { Log.e(TAG, "Invalid data fed to InsertFromInt"); return; } BgReading bgr = new BgReading(); if (bgr != null) { bgr.uuid = UUID.randomUUID().toString(); bgr.timestamp = timestamp; bgr.calculated_value = value; // rough code for testing! bgr.filtered_calculated_value = value; bgr.raw_data = value; bgr.age_adjusted_raw_value = value; bgr.filtered_data = value; final Sensor forced_sensor = Sensor.currentSensor(); if (forced_sensor != null) { bgr.sensor = forced_sensor; bgr.sensor_uuid = forced_sensor.uuid; } try { if (readingNearTimeStamp(bgr.timestamp) == null) { bgr.save(); bgr.find_slope(); if (do_notification) { // xdrip.getAppContext().startService(new Intent(xdrip.getAppContext(), Notifications.class)); // alerts et al Notifications.start(); // this may not be needed as it is duplicated in handleNewBgReading } BgSendQueue.handleNewBgReading(bgr, "create", xdrip.getAppContext(), false, !do_notification); // pebble and widget } else { Log.d(TAG, "Ignoring duplicate bgr record due to timestamp: " + timestamp); } } catch (Exception e) { Log.d(TAG, "Could not save BGR: " + e.toString()); } } else { Log.e(TAG,"Got null bgr from create"); } } public static BgReading byUUID(String uuid) { if (uuid == null) return null; return new Select() .from(BgReading.class) .where("uuid = ?", uuid) .executeSingle(); } public static BgReading byid(long id) { return new Select() .from(BgReading.class) .where("_ID = ?", id) .executeSingle(); } public static BgReading fromJSON(String json) { if (json.length()==0) { Log.d(TAG,"Empty json received in bgreading fromJson"); return null; } try { Log.d(TAG, "Processing incoming json: " + json); return new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json,BgReading.class); } catch (Exception e) { Log.d(TAG, "Got exception parsing BgReading json: " + e.toString()); Home.toaststaticnext("Error on BGReading sync, probably decryption key mismatch"); return null; } } private BgReadingMessage toMessageNative() { return new BgReadingMessage.Builder() .timestamp(timestamp) //.a(a) //.b(b) //.c(c) .age_adjusted_raw_value(age_adjusted_raw_value) .calculated_value(calculated_value) .filtered_calculated_value(filtered_calculated_value) .calibration_flag(calibration_flag) .raw_calculated(raw_calculated) .raw_data(raw_data) .calculated_value_slope(calculated_value_slope) //.calibration_uuid(calibration_uuid) .uuid(uuid) .build(); } public byte[] toMessage() { final List btl = new ArrayList<>(); btl.add(this); return toMultiMessage(btl); } public static byte[] toMultiMessage(List bgl) { if (bgl == null) return null; final List BgReadingMessageList = new ArrayList<>(); for (BgReading bg : bgl) { BgReadingMessageList.add(bg.toMessageNative()); } return BgReadingMultiMessage.ADAPTER.encode(new BgReadingMultiMessage(BgReadingMessageList)); } private static final long CLOSEST_READING_MS = 290000; private static void processFromMessage(BgReadingMessage btm) { if ((btm != null) && (btm.uuid != null) && (btm.uuid.length() == 36)) { BgReading bg = byUUID(btm.uuid); if (bg != null) { // we already have this uuid and we don't have a circumstance to update the record, so quick return here return; } if (bg == null) { bg = getForPreciseTimestamp(Wire.get(btm.timestamp, BgReadingMessage.DEFAULT_TIMESTAMP), CLOSEST_READING_MS, false); if (bg != null) { UserError.Log.wtf(TAG, "Error matches a different uuid with the same timestamp: " + bg.uuid + " vs " + btm.uuid + " skipping!"); return; } bg = new BgReading(); } bg.timestamp = Wire.get(btm.timestamp, BgReadingMessage.DEFAULT_TIMESTAMP); bg.calculated_value = Wire.get(btm.calculated_value, BgReadingMessage.DEFAULT_CALCULATED_VALUE); bg.filtered_calculated_value = Wire.get(btm.filtered_calculated_value, BgReadingMessage.DEFAULT_FILTERED_CALCULATED_VALUE); bg.calibration_flag = Wire.get(btm.calibration_flag, BgReadingMessage.DEFAULT_CALIBRATION_FLAG); bg.raw_calculated = Wire.get(btm.raw_calculated, BgReadingMessage.DEFAULT_RAW_CALCULATED); bg.raw_data = Wire.get(btm.raw_data, BgReadingMessage.DEFAULT_RAW_DATA); bg.calculated_value_slope = Wire.get(btm.calculated_value_slope, BgReadingMessage.DEFAULT_CALCULATED_VALUE_SLOPE); bg.calibration_uuid = btm.calibration_uuid; bg.uuid = btm.uuid; bg.save(); } else { UserError.Log.wtf(TAG, "processFromMessage uuid is null or invalid"); } } public synchronized static void processFromMultiMessage(byte[] payload) { try { final BgReadingMultiMessage bgmm = BgReadingMultiMessage.ADAPTER.decode(payload); if ((bgmm != null) && (bgmm.bgreading_message != null)) { for (BgReadingMessage btm : bgmm.bgreading_message) { processFromMessage(btm); } Home.staticRefreshBGCharts(); } } catch (IOException | NullPointerException | IllegalStateException e) { UserError.Log.e(TAG, "exception processFromMessage: " + e); } } public String toJSON(boolean sendCalibration) { final JSONObject jsonObject = new JSONObject(); try { jsonObject.put("uuid", uuid); jsonObject.put("a", a); // how much of this do we actually need? jsonObject.put("b", b); jsonObject.put("c", c); jsonObject.put("timestamp", timestamp); jsonObject.put("age_adjusted_raw_value", age_adjusted_raw_value); jsonObject.put("calculated_value", calculated_value); jsonObject.put("filtered_calculated_value", filtered_calculated_value); jsonObject.put("calibration_flag", calibration_flag); jsonObject.put("filtered_data", filtered_data); jsonObject.put("raw_calculated", raw_calculated); jsonObject.put("raw_data", raw_data); try { jsonObject.put("calculated_value_slope", calculated_value_slope); } catch (JSONException e) { jsonObject.put("hide_slope", true); // calculated value slope is NaN - hide slope should already be true locally too } if (sendCalibration) { jsonObject.put("calibration_uuid", calibration_uuid); } // jsonObject.put("sensor", sensor); return jsonObject.toString(); } catch (JSONException e) { UserError.Log.wtf(TAG, "Error producing in toJSON: " + e); if (Double.isNaN(a)) Log.e(TAG, "a is NaN"); if (Double.isNaN(b)) Log.e(TAG, "b is NaN"); if (Double.isNaN(c)) Log.e(TAG, "c is NaN"); if (Double.isNaN(age_adjusted_raw_value)) Log.e(TAG, "age_adjusted_raw_value is NaN"); if (Double.isNaN(calculated_value)) Log.e(TAG, "calculated_value is NaN"); if (Double.isNaN(filtered_calculated_value)) Log.e(TAG, "filtered_calculated_value is NaN"); if (Double.isNaN(filtered_data)) Log.e(TAG, "filtered_data is NaN"); if (Double.isNaN(raw_calculated)) Log.e(TAG, "raw_calculated is NaN"); if (Double.isNaN(raw_data)) Log.e(TAG, "raw_data is NaN"); if (Double.isNaN(calculated_value_slope)) Log.e(TAG, "calculated_value_slope is NaN"); return ""; } } public static void deleteALL() { try { SQLiteUtils.execSql("delete from BgSendQueue"); SQLiteUtils.execSql("delete from BgReadings"); Log.d(TAG, "Deleting all BGReadings"); } catch (Exception e) { Log.e(TAG, "Got exception running deleteALL " + e.toString()); } } public static void deleteRandomData() { Random rand = new Random(); int minutes_ago_end = rand.nextInt(120); int minutes_ago_start = minutes_ago_end + rand.nextInt(35)+5; long ts_start = JoH.tsl() - minutes_ago_start * Constants.MINUTE_IN_MS; long ts_end = JoH.tsl() - minutes_ago_end * Constants.MINUTE_IN_MS; UserError.Log.d(TAG,"Deleting random bgreadings: "+JoH.dateTimeText(ts_start)+" -> "+JoH.dateTimeText(ts_end)); testDeleteRange(ts_start, ts_end); } public static void testDeleteRange(long start_time, long end_time) { List bgrs = new Delete() .from(BgReading.class) .where("timestamp < ?", end_time) .where("timestamp > ?",start_time) .execute(); // UserError.Log.d("OB1TEST","Deleted: "+bgrs.size()+" records"); } public static List cleanup(int retention_days) { return new Delete() .from(BgReading.class) .where("timestamp < ?", JoH.tsl() - (retention_days * Constants.DAY_IN_MS)) .execute(); } public static void cleanupOutOfRangeValues() { new Delete() .from(BgReading.class) .where("timestamp > ?", JoH.tsl() - (3 * Constants.DAY_IN_MS)) .where("calculated_value > ?", 324) .execute(); } // used in wear public static void cleanup(long timestamp) { try { SQLiteUtils.execSql("delete from BgSendQueue"); List data = new Select() .from(BgReading.class) .where("timestamp < ?", timestamp) .orderBy("timestamp desc") .execute(); if (data != null) Log.d(TAG, "cleanup BgReading size=" + data.size()); new Cleanup().execute(data); } catch (Exception e) { Log.e(TAG, "Got exception running cleanup " + e.toString()); } } // used in wear private static class Cleanup extends AsyncTask, Integer, Boolean> { @Override protected Boolean doInBackground(List... errors) { try { for(BgReading data : errors[0]) { data.delete(); } return true; } catch(Exception e) { return false; } } } //*******INSTANCE METHODS***********// public void perform_calculations() { find_new_curve(); find_new_raw_curve(); find_slope(); } public void find_slope() { List last_2 = BgReading.latest(2); // FYI: By default, assertions are disabled at runtime. Add "-ea" to commandline to enable. // https://docs.oracle.com/javase/7/docs/technotes/guides/language/assert.html assert last_2.get(0).uuid.equals(this.uuid) : "Invariant condition not fulfilled: calculating slope and current reading wasn't saved before"; if ((last_2 != null) && (last_2.size() == 2)) { calculated_value_slope = calculateSlope(this, last_2.get(1)); save(); } else if ((last_2 != null) && (last_2.size() == 1)) { calculated_value_slope = 0; save(); } else { if (JoH.ratelimit("no-bg-couldnt-find-slope", 15)) { Log.w(TAG, "NO BG? COULDNT FIND SLOPE!"); } } } public void find_new_curve() { JoH.clearCache(); List last_3 = BgReading.latest(3); if ((last_3 != null) && (last_3.size() == 3)) { BgReading latest = last_3.get(0); BgReading second_latest = last_3.get(1); BgReading third_latest = last_3.get(2); double y3 = latest.calculated_value; double x3 = latest.timestamp; double y2 = second_latest.calculated_value; double x2 = second_latest.timestamp; double y1 = third_latest.calculated_value; double x1 = third_latest.timestamp; a = y1/((x1-x2)*(x1-x3))+y2/((x2-x1)*(x2-x3))+y3/((x3-x1)*(x3-x2)); b = (-y1*(x2+x3)/((x1-x2)*(x1-x3))-y2*(x1+x3)/((x2-x1)*(x2-x3))-y3*(x1+x2)/((x3-x1)*(x3-x2))); c = (y1*x2*x3/((x1-x2)*(x1-x3))+y2*x1*x3/((x2-x1)*(x2-x3))+y3*x1*x2/((x3-x1)*(x3-x2))); Log.i(TAG, "find_new_curve: BG PARABOLIC RATES: "+a+"x^2 + "+b+"x + "+c); save(); } else if ((last_3 != null) && (last_3.size() == 2)) { Log.i(TAG, "find_new_curve: Not enough data to calculate parabolic rates - assume Linear"); BgReading latest = last_3.get(0); BgReading second_latest = last_3.get(1); double y2 = latest.calculated_value; double x2 = latest.timestamp; double y1 = second_latest.calculated_value; double x1 = second_latest.timestamp; if(y1 == y2) { b = 0; } else { b = (y2 - y1)/(x2 - x1); } a = 0; c = -1 * ((latest.b * x1) - y1); Log.i(TAG, ""+latest.a+"x^2 + "+latest.b+"x + "+latest.c); save(); } else { Log.i(TAG, "find_new_curve: Not enough data to calculate parabolic rates - assume static data"); a = 0; b = 0; c = calculated_value; Log.i(TAG, ""+a+"x^2 + "+b+"x + "+c); save(); } } public void calculateAgeAdjustedRawValue(){ final double adjust_for = AGE_ADJUSTMENT_TIME - time_since_sensor_started; if ((adjust_for > 0) && (!DexCollectionType.hasLibre())) { age_adjusted_raw_value = ((AGE_ADJUSTMENT_FACTOR * (adjust_for / AGE_ADJUSTMENT_TIME)) * raw_data) + raw_data; Log.i(TAG, "calculateAgeAdjustedRawValue: RAW VALUE ADJUSTMENT FROM:" + raw_data + " TO: " + age_adjusted_raw_value); } else { age_adjusted_raw_value = raw_data; } } void find_new_raw_curve() { JoH.clearCache(); final List last_3 = BgReading.latest(3); if ((last_3 != null) && (last_3.size() == 3)) { final BgReading latest = last_3.get(0); final BgReading second_latest = last_3.get(1); final BgReading third_latest = last_3.get(2); double y3 = latest.age_adjusted_raw_value; double x3 = latest.timestamp; double y2 = second_latest.age_adjusted_raw_value; double x2 = second_latest.timestamp; double y1 = third_latest.age_adjusted_raw_value; double x1 = third_latest.timestamp; ra = y1/((x1-x2)*(x1-x3))+y2/((x2-x1)*(x2-x3))+y3/((x3-x1)*(x3-x2)); rb = (-y1*(x2+x3)/((x1-x2)*(x1-x3))-y2*(x1+x3)/((x2-x1)*(x2-x3))-y3*(x1+x2)/((x3-x1)*(x3-x2))); rc = (y1*x2*x3/((x1-x2)*(x1-x3))+y2*x1*x3/((x2-x1)*(x2-x3))+y3*x1*x2/((x3-x1)*(x3-x2))); Log.i(TAG, "find_new_raw_curve: RAW PARABOLIC RATES: "+ra+"x^2 + "+rb+"x + "+rc); save(); } else if ((last_3 != null) && (last_3.size()) == 2) { BgReading latest = last_3.get(0); BgReading second_latest = last_3.get(1); double y2 = latest.age_adjusted_raw_value; double x2 = latest.timestamp; double y1 = second_latest.age_adjusted_raw_value; double x1 = second_latest.timestamp; if(y1 == y2) { rb = 0; } else { rb = (y2 - y1)/(x2 - x1); } ra = 0; rc = -1 * ((latest.rb * x1) - y1); Log.i(TAG, "find_new_raw_curve: Not enough data to calculate parabolic rates - assume Linear data"); Log.i(TAG, "RAW PARABOLIC RATES: "+ra+"x^2 + "+rb+"x + "+rc); save(); } else { Log.i(TAG, "find_new_raw_curve: Not enough data to calculate parabolic rates - assume static data"); BgReading latest_entry = BgReading.lastNoSenssor(); ra = 0; rb = 0; if (latest_entry != null) { rc = latest_entry.age_adjusted_raw_value; } else { rc = 105; } save(); } } private static double weightedAverageRaw(double timeA, double timeB, double calibrationTime, double rawA, double rawB) { final double relativeSlope = (rawB - rawA)/(timeB - timeA); final double relativeIntercept = rawA - (relativeSlope * timeA); return ((relativeSlope * calibrationTime) + relativeIntercept); } public String toS() { Gson gson = new GsonBuilder() .excludeFieldsWithoutExposeAnnotation() .registerTypeAdapter(Date.class, new DateTypeAdapter()) .serializeSpecialFloatingPointValues() .create(); return gson.toJson(this); } public String timeStamp() { return JoH.dateTimeText(timestamp); } public int noiseValue() { if(noise == null || noise.compareTo("") == 0) { return 1; } else { return Integer.valueOf(noise); } } public BgReading injectNoise(boolean save) { final BgReading bgReading = this; if (JoH.msSince(bgReading.timestamp) > Constants.MINUTE_IN_MS * 20) { bgReading.noise = "0"; } else { BgGraphBuilder.refreshNoiseIfOlderThan(bgReading.timestamp); if (BgGraphBuilder.last_noise > BgGraphBuilder.NOISE_HIGH) { bgReading.noise = "4"; } else if (BgGraphBuilder.last_noise > BgGraphBuilder.NOISE_TOO_HIGH_FOR_PREDICT) { bgReading.noise = "3"; } else if (BgGraphBuilder.last_noise > BgGraphBuilder.NOISE_TRIGGER) { bgReading.noise = "2"; } } if (save) bgReading.save(); return bgReading; } // list(0) is the most recent reading. public static List getXRecentPoints(int NumReadings) { List latest = BgReading.latest(NumReadings); if (latest == null || latest.size() != NumReadings) { // for less than NumReadings readings, we can't tell what the situation // Log.d(TAG_ALERT, "getXRecentPoints we don't have enough readings, returning null"); return null; } // So, we have at least three values... for(BgReading bgReading : latest) { Log.d(TAG_ALERT, "getXRecentPoints - reading: time = " + bgReading.timestamp + " calculated_value " + bgReading.calculated_value); } // now let's check that they are relevant. the last reading should be from the last 5 minutes, // x-1 more readings should be from the last (x-1)*5 minutes. we will allow 5 minutes for the last // x to allow one packet to be missed. if (new Date().getTime() - latest.get(NumReadings - 1).timestamp > (NumReadings * 5 + 6) * 60 * 1000) { Log.d(TAG_ALERT, "getXRecentPoints we don't have enough points from the last " + (NumReadings * 5 + 6) + " minutes, returning null"); return null; } return latest; } public static boolean checkForPersistentHigh() { // skip if not enabled if (!Pref.getBooleanDefaultFalse("persistent_high_alert_enabled")) return false; List last = BgReading.latest(1); if ((last != null) && (last.size()>0)) { final long now = JoH.tsl(); final long since = now - last.get(0).timestamp; // only process if last reading <10 mins if (since < 600000) { // check if exceeding high if (last.get(0).calculated_value > Home.convertToMgDlIfMmol( JoH.tolerantParseDouble(Pref.getString("highValue", "170")))) { final double this_slope = last.get(0).calculated_value_slope * 60000; //Log.d(TAG, "CheckForPersistentHigh: Slope: " + JoH.qs(this_slope)); // if not falling if (this_slope > 0) { final long high_since = Pref.getLong(PERSISTENT_HIGH_SINCE, 0); if (high_since == 0) { // no previous persistent high so set start as now Pref.setLong(PERSISTENT_HIGH_SINCE, now); Log.d(TAG, "Registering start of persistent high at time now"); } else { final long high_for_mins = (now - high_since) / (1000 * 60); long threshold_mins; try { threshold_mins = Long.parseLong(Pref.getString("persistent_high_threshold_mins", "60")); } catch (NumberFormatException e) { threshold_mins = 60; Home.toaststaticnext("Invalid persistent high for longer than minutes setting: using 60 mins instead"); } if (high_for_mins > threshold_mins) { // we have been high for longer than the threshold - raise alert // except if alerts are disabled if (Pref.getLong("alerts_disabled_until", 0) > new Date().getTime()) { Log.i(TAG, "checkforPersistentHigh: Notifications are currently disabled cannot alert!!"); return false; } Log.i(TAG, "Persistent high for: " + high_for_mins + " mins -> alerting"); Notifications.persistentHighAlert(xdrip.getAppContext(), true, xdrip.getAppContext().getString(R.string.persistent_high_for_greater_than) + (int) high_for_mins + xdrip.getAppContext().getString(R.string.space_mins)); } else { Log.d(TAG, "Persistent high below time threshold at: " + high_for_mins); } } } } else { // not high - cancel any existing if (Pref.getLong(PERSISTENT_HIGH_SINCE,0)!=0) { Log.i(TAG,"Cancelling previous persistent high as we are no longer high"); Pref.setLong(PERSISTENT_HIGH_SINCE, 0); // clear it Notifications.persistentHighAlert(xdrip.getAppContext(), false, ""); // cancel it } } } } return false; // actually we should probably return void as we do everything inside this method } public static void checkForRisingAllert(Context context) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); Boolean rising_alert = prefs.getBoolean("rising_alert", false); if(!rising_alert) { return; } if(prefs.getLong("alerts_disabled_until", 0) > new Date().getTime()){ Log.i("NOTIFICATIONS", "checkForRisingAllert: Notifications are currently disabled!!"); return; } String riseRate = prefs.getString("rising_bg_val", "2"); float friseRate = 2; try { friseRate = Float.parseFloat(riseRate); } catch (NumberFormatException nfe) { Log.e(TAG_ALERT, "checkForRisingAllert reading falling_bg_val failed, continuing with 2", nfe); } Log.d(TAG_ALERT, "checkForRisingAllert will check for rate of " + friseRate); boolean riseAlert = checkForDropRiseAllert(friseRate, false); Notifications.RisingAlert(context, riseAlert); } public static void checkForDropAllert(Context context) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); Boolean falling_alert = prefs.getBoolean("falling_alert", false); if(!falling_alert) { return; } if(prefs.getLong("alerts_disabled_until", 0) > new Date().getTime()){ Log.d("NOTIFICATIONS", "checkForDropAllert: Notifications are currently disabled!!"); return; } String dropRate = prefs.getString("falling_bg_val", "2"); float fdropRate = 2; try { fdropRate = Float.parseFloat(dropRate); } catch (NumberFormatException nfe) { Log.e(TAG_ALERT, "reading falling_bg_val failed, continuing with 2", nfe); } Log.i(TAG_ALERT, "checkForDropAllert will check for rate of " + fdropRate); boolean dropAlert = checkForDropRiseAllert(fdropRate, true); Notifications.DropAlert(context, dropAlert); } // true say, alert is on. private static boolean checkForDropRiseAllert(float MaxSpeed, boolean drop) { Log.d(TAG_ALERT, "checkForDropRiseAllert called drop=" + drop); List latest = getXRecentPoints(4); if(latest == null) { Log.d(TAG_ALERT, "checkForDropRiseAllert we don't have enough points from the last 15 minutes, returning false"); return false; } float time3 = (latest.get(0).timestamp - latest.get(3).timestamp) / 60000; double bg_diff3 = latest.get(3).calculated_value - latest.get(0).calculated_value; if (!drop) { bg_diff3 *= (-1); } Log.i(TAG_ALERT, "bg_diff3=" + bg_diff3 + " time3 = " + time3); if(bg_diff3 < time3 * MaxSpeed) { Log.d(TAG_ALERT, "checkForDropRiseAllert for latest 4 points not fast enough, returning false"); return false; } // we should alert here, but if the last measurement was less than MaxSpeed / 2, I won't. float time1 = (latest.get(0).timestamp - latest.get(1).timestamp) / 60000; double bg_diff1 = latest.get(1).calculated_value - latest.get(0).calculated_value; if (!drop) { bg_diff1 *= (-1); } if(time1 > 7.0) { Log.d(TAG_ALERT, "checkForDropRiseAllert the two points are not close enough, returning true"); return true; } if(bg_diff1 < time1 * MaxSpeed /2) { Log.d(TAG_ALERT, "checkForDropRiseAllert for latest 2 points not fast enough, returning false"); return false; } Log.d(TAG_ALERT, "checkForDropRiseAllert returning true speed is " + (bg_diff3 / time3)); return true; } // Make sure that this function either sets the alert or removes it. public static boolean getAndRaiseUnclearReading(Context context) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); if(prefs.getLong("alerts_disabled_until", 0) > new Date().getTime()){ Log.d("NOTIFICATIONS", "getAndRaiseUnclearReading Notifications are currently disabled!!"); UserNotification.DeleteNotificationByType("bg_unclear_readings_alert"); return false; } Boolean bg_unclear_readings_alerts = prefs.getBoolean("bg_unclear_readings_alerts", false); if (!bg_unclear_readings_alerts || !DexCollectionType.hasFiltered() || Ob1G5CollectionService.usingG6() || Ob1G5CollectionService.usingNativeMode()) { Log.d(TAG_ALERT, "getUnclearReading returned false since feature is disabled"); UserNotification.DeleteNotificationByType("bg_unclear_readings_alert"); return false; } Long UnclearTimeSetting = Long.parseLong(prefs.getString("bg_unclear_readings_minutes", "90")) * 60000; Long UnclearTime = BgReading.getUnclearTime(UnclearTimeSetting); if (UnclearTime >= UnclearTimeSetting ) { Log.d("NOTIFICATIONS", "Readings have been unclear for too long!!"); Notifications.bgUnclearAlert(context); return true; } UserNotification.DeleteNotificationByType("bg_unclear_readings_alert"); if (UnclearTime > 0 ) { Log.d(TAG_ALERT, "We are in an clear state, but not for too long. Alerts are disabled"); return true; } return false; } /* * This function comes to check weather we are in a case that we have an allert but since things are * getting better we should not do anything. (This is only in the case that the alert was snoozed before.) * This means that if this is a low alert, and we have two readings in the last 15 minutes, and * either we have gone in 10 in the last two readings, or we have gone in 3 in the last reading, we * don't play the alert again, but rather wait for the alert to finish. * I'll start with having the same values for the high alerts. */ public static boolean trendingToAlertEnd(Context context, boolean above) { // TODO: check if we are not in an UnclerTime. Log.d(TAG_ALERT, "trendingToAlertEnd called"); List latest = getXRecentPoints(3); if(latest == null) { Log.d(TAG_ALERT, "trendingToAlertEnd we don't have enough points from the last 15 minutes, returning false"); return false; } if(above == false) { // This is a low alert, we should be going up if((latest.get(0).calculated_value - latest.get(1).calculated_value > 4) || (latest.get(0).calculated_value - latest.get(2).calculated_value > 10)) { Log.d(TAG_ALERT, "trendingToAlertEnd returning true for low alert"); return true; } } else { // This is a high alert we should be heading down if((latest.get(1).calculated_value - latest.get(0).calculated_value > 4) || (latest.get(2).calculated_value - latest.get(0).calculated_value > 10)) { Log.d(TAG_ALERT, "trendingToAlertEnd returning true for high alert"); return true; } } Log.d(TAG_ALERT, "trendingToAlertEnd returning false, not in the right direction (or not fast enough)"); return false; } // Should that be combined with noiseValue? private Boolean Unclear() { Log.d(TAG_ALERT, "Unclear filtered_data=" + filtered_data + " raw_data=" + raw_data); return raw_data > filtered_data * 1.3 || raw_data < filtered_data * 0.7; } /* * returns the time (in ms) that the state is not clear and no alerts should work * The base of the algorithm is that any period can be bad or not. bgReading.Unclear() tells that. * a non clear bgReading means MAX_INFLUANCE time after it we are in a bad position * Since this code is based on heuristics, and since times are not accurate, boundary issues can be ignored. * * interstingTime is the period to check. That is if the last period is bad, we want to know how long does it go bad... * */ // The extra 120,000 is to allow the packet to be delayed for some time and still be counted in that group // Please don't use for MAX_INFLUANCE a number that is complete multiply of 5 minutes (300,000) static final int MAX_INFLUANCE = 30 * 60000 - 120000; // A bad point means data is untrusted for 30 minutes. private static Long getUnclearTimeHelper(List latest, Long interstingTime, final Long now) { // The code ignores missing points (that is they some times are treated as good and some times as bad. // If this bothers someone, I believe that the list should be filled with the missing points as good and continue to run. Long LastGoodTime = 0l; // 0 represents that we are not in a good part Long UnclearTime = 0l; for(BgReading bgReading : latest) { // going over the readings from latest to first if(bgReading.timestamp < now - (interstingTime + MAX_INFLUANCE)) { // Some readings are missing, we can stop checking break; } if(bgReading.timestamp <= now - MAX_INFLUANCE && UnclearTime == 0) { Log.d(TAG_ALERT, "We did not have a problematic reading for MAX_INFLUANCE time, so now all is well"); return 0l; } if (bgReading.Unclear()) { // here we assume that there are no missing points. Missing points might join the good and bad values as well... // we should have checked if we have a period, but it is hard to say how to react to them. Log.d(TAG_ALERT, "We have a bad reading, so setting UnclearTime to " + bgReading.timestamp); UnclearTime = bgReading.timestamp; LastGoodTime = 0l; } else { if (LastGoodTime == 0l) { Log.d(TAG_ALERT, "We are starting a good period at "+ bgReading.timestamp); LastGoodTime = bgReading.timestamp; } else { // we have some good period, is it good enough? if(LastGoodTime - bgReading.timestamp >= MAX_INFLUANCE) { // Here UnclearTime should be already set, otherwise we will return a toob big value if (UnclearTime ==0) { Log.wtf(TAG_ALERT, "ERROR - UnclearTime must not be 0 here !!!"); } Log.d(TAG_ALERT, "We have a good period from " + bgReading.timestamp + " to " + LastGoodTime + "returning " + (now - UnclearTime +5 *60000)); return now - UnclearTime + 5 *60000; } } } } // if we are here, we have a problem... or not. if(UnclearTime == 0l) { Log.d(TAG_ALERT, "Since we did not find a good period, but we also did not find a single bad value, we assume things are good"); return 0l; } Log.d(TAG_ALERT, "We scanned all over, but could not find a good period. we have a bad value, so assuming that the whole period is bad" + " returning " + interstingTime); // Note that we might now have all the points, and in this case, since we don't have a good period I return a bad period. return interstingTime; } // This is to enable testing of the function, by passing different values public static Long getUnclearTime(Long interstingTime) { List latest = BgReading.latest((interstingTime.intValue() + MAX_INFLUANCE)/ 60000 /5 ); if (latest == null) { return 0L; } final Long now = new Date().getTime(); return getUnclearTimeHelper(latest, interstingTime, now); } public static Long getTimeSinceLastReading() { BgReading bgReading = BgReading.last(); if (bgReading != null) { return (new Date().getTime() - bgReading.timestamp); } return (long) 0; } public double usedRaw() { Calibration calibration = Calibration.lastValid(); if (calibration != null && calibration.check_in) { return raw_data; } return age_adjusted_raw_value; } public boolean isCurrent() { return JoH.msSince(timestamp) < Constants.MINUTE_IN_MS * 2; } public double ageAdjustedFiltered(){ double usedRaw = usedRaw(); if(usedRaw == raw_data || raw_data == 0d){ return filtered_data; } else { // adjust the filtered_data with the same factor as the age adjusted raw value return filtered_data * (usedRaw/raw_data); } } // ignores calibration checkins for speed public double ageAdjustedFiltered_fast() { // adjust the filtered_data with the same factor as the age adjusted raw value return filtered_data * (age_adjusted_raw_value / raw_data); } // the input of this function is a string. each char can be g(=good) or b(=bad) or s(=skip, point unmissed). static List createlatestTest(String input, Long now) { Random randomGenerator = new Random(); List out = new LinkedList (); char[] chars= input.toCharArray(); for(int i=0; i < chars.length; i++) { BgReading bg = new BgReading(); int rand = randomGenerator.nextInt(20000) - 10000; bg.timestamp = now - i * 5 * 60000 + rand; bg.raw_data = 150; if(chars[i] == 'g') { bg.filtered_data = 151; } else if (chars[i] == 'b') { bg.filtered_data = 110; } else { continue; } out.add(bg); } return out; } static void TestgetUnclearTime(String input, Long interstingTime, Long expectedResult) { final Long now = new Date().getTime(); List readings = createlatestTest(input, now); Long result = getUnclearTimeHelper(readings, interstingTime * 60000, now); if (result >= expectedResult * 60000 - 20000 && result <= expectedResult * 60000+20000) { Log.d(TAG_ALERT, "Test passed"); } else { Log.d(TAG_ALERT, "Test failed expectedResult = " + expectedResult + " result = "+ result / 60000.0); } } public static void TestgetUnclearTimes() { TestgetUnclearTime("gggggggggggggggggggggggg", 90l, 0l * 5); TestgetUnclearTime("bggggggggggggggggggggggg", 90l, 1l * 5); TestgetUnclearTime("bbgggggggggggggggggggggg", 90l, 2l *5 ); TestgetUnclearTime("gbgggggggggggggggggggggg", 90l, 2l * 5); TestgetUnclearTime("gbgggbggbggbggbggbggbgbg", 90l, 18l * 5); TestgetUnclearTime("bbbgggggggbbgggggggggggg", 90l, 3l * 5); TestgetUnclearTime("ggggggbbbbbbgggggggggggg", 90l, 0l * 5); TestgetUnclearTime("ggssgggggggggggggggggggg", 90l, 0l * 5); TestgetUnclearTime("ggssbggssggggggggggggggg", 90l, 5l * 5); TestgetUnclearTime("bb", 90l, 18l * 5); // intersting time is 2 minutes, we should always get 0 (in 5 minutes units TestgetUnclearTime("gggggggggggggggggggggggg", 2l, 0l * 5); TestgetUnclearTime("bggggggggggggggggggggggg", 2l, 2l); TestgetUnclearTime("bbgggggggggggggggggggggg", 2l, 2l); TestgetUnclearTime("gbgggggggggggggggggggggg", 2l, 2l); TestgetUnclearTime("gbgggbggbggbggbggbggbgbg", 2l, 2l); // intersting time is 10 minutes, we should always get 0 (in 5 minutes units TestgetUnclearTime("gggggggggggggggggggggggg", 10l, 0l * 5); TestgetUnclearTime("bggggggggggggggggggggggg", 10l, 1l * 5); TestgetUnclearTime("bbgggggggggggggggggggggg", 10l, 2l * 5); TestgetUnclearTime("gbgggggggggggggggggggggg", 10l, 2l * 5); TestgetUnclearTime("gbgggbggbggbggbggbggbgbg", 10l, 2l * 5); TestgetUnclearTime("bbbgggggggbbgggggggggggg", 10l, 2l * 5); TestgetUnclearTime("ggggggbbbbbbgggggggggggg", 10l, 0l * 5); TestgetUnclearTime("ggssgggggggggggggggggggg", 10l, 0l * 5); TestgetUnclearTime("ggssbggssggggggggggggggg", 10l, 2l * 5); TestgetUnclearTime("bb", 10l, 2l * 5); } public int getSlopeOrdinal() { double slope_by_minute = calculated_value_slope * 60000; int ordinal = 0; if(!hide_slope) { if (slope_by_minute <= (-3.5)) { ordinal = 7; } else if (slope_by_minute <= (-2)) { ordinal = 6; } else if (slope_by_minute <= (-1)) { ordinal = 5; } else if (slope_by_minute <= (1)) { ordinal = 4; } else if (slope_by_minute <= (2)) { ordinal = 3; } else if (slope_by_minute <= (3.5)) { ordinal = 2; } else { ordinal = 1; } } return ordinal; } public int getMgdlValue() { return (int) calculated_value; } public long getEpochTimestamp() { return timestamp; } }