Initial project commit
This commit is contained in:
151
lib/nightscout/com/eveningoutpost/dexdrip/Models/APStatus.java
Normal file
151
lib/nightscout/com/eveningoutpost/dexdrip/Models/APStatus.java
Normal file
@@ -0,0 +1,151 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Delete;
|
||||
import com.activeandroid.query.Select;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
|
||||
import com.eveningoutpost.dexdrip.wearintegration.ExternalStatusService;
|
||||
import com.google.gson.annotations.Expose;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Created by jamorham on 11/06/2018.
|
||||
*/
|
||||
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Table(name = "APStatus", id = BaseColumns._ID)
|
||||
public class APStatus extends PlusModel {
|
||||
|
||||
private static boolean patched = false;
|
||||
private final static String TAG = APStatus.class.getSimpleName();
|
||||
private final static boolean d = false;
|
||||
|
||||
private static final String[] schema = {
|
||||
"CREATE TABLE APStatus (_id INTEGER PRIMARY KEY AUTOINCREMENT);",
|
||||
"ALTER TABLE APStatus ADD COLUMN timestamp INTEGER;",
|
||||
"ALTER TABLE APStatus ADD COLUMN basal_percent INTEGER;",
|
||||
"CREATE UNIQUE INDEX index_APStatus_timestamp on APStatus(timestamp);"};
|
||||
|
||||
|
||||
@Expose
|
||||
@Column(name = "timestamp", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE)
|
||||
public long timestamp;
|
||||
|
||||
@Expose
|
||||
@Column(name = "basal_percent")
|
||||
public int basal_percent;
|
||||
|
||||
|
||||
public String toS() {
|
||||
return JoH.defaultGsonInstance().toJson(this);
|
||||
}
|
||||
|
||||
// static methods
|
||||
|
||||
public static APStatus createEfficientRecord(long timestamp_ms, int basal_percent) {
|
||||
final APStatus existing = last();
|
||||
if (existing == null || (existing.basal_percent != basal_percent)) {
|
||||
|
||||
if (existing != null && existing.timestamp > timestamp_ms) {
|
||||
UserError.Log.e(TAG, "Refusing to create record older than current: " + JoH.dateTimeText(timestamp_ms) + " vs " + JoH.dateTimeText(existing.timestamp));
|
||||
return null;
|
||||
}
|
||||
|
||||
final APStatus fresh = APStatus.builder()
|
||||
.timestamp(timestamp_ms)
|
||||
.basal_percent(basal_percent)
|
||||
.build();
|
||||
|
||||
UserError.Log.d(TAG, "New record created: " + fresh.toS());
|
||||
|
||||
fresh.save();
|
||||
return fresh;
|
||||
} else {
|
||||
return existing;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TODO use persistent store?
|
||||
public static APStatus last() {
|
||||
try {
|
||||
return new Select()
|
||||
.from(APStatus.class)
|
||||
.orderBy("timestamp desc")
|
||||
.executeSingle();
|
||||
} catch (android.database.sqlite.SQLiteException e) {
|
||||
updateDB();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<APStatus> latestForGraph(int number, double startTime) {
|
||||
return latestForGraph(number, (long) startTime, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static List<APStatus> latestForGraph(int number, long startTime) {
|
||||
return latestForGraph(number, startTime, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static List<APStatus> latestForGraph(int number, long startTime, long endTime) {
|
||||
try {
|
||||
final List<APStatus> results = new Select()
|
||||
.from(APStatus.class)
|
||||
.where("timestamp >= " + Math.max(startTime, 0))
|
||||
.where("timestamp <= " + endTime)
|
||||
.orderBy("timestamp asc") // warn asc!
|
||||
.limit(number)
|
||||
.execute();
|
||||
// extend line to now if we have current data but it is continuation of last record
|
||||
// so not generating a new efficient record.
|
||||
if (results != null && (results.size() > 0)) {
|
||||
final APStatus last = results.get(results.size() - 1);
|
||||
final long last_raw_record_timestamp = ExternalStatusService.getLastStatusLineTime();
|
||||
// check are not already using the latest.
|
||||
if (last_raw_record_timestamp > last.timestamp) {
|
||||
final Integer last_recorded_tbr = ExternalStatusService.getTBRInt();
|
||||
if (last_recorded_tbr != null) {
|
||||
if ((last.basal_percent == last_recorded_tbr)
|
||||
&& (JoH.msSince(last.timestamp) < Constants.HOUR_IN_MS * 3)
|
||||
&& (JoH.msSince(ExternalStatusService.getLastStatusLineTime()) < Constants.MINUTE_IN_MS * 20)) {
|
||||
results.add(new APStatus(JoH.tsl(), last_recorded_tbr));
|
||||
UserError.Log.d(TAG, "Adding extension record");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
} catch (android.database.sqlite.SQLiteException e) {
|
||||
updateDB();
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static List<APStatus> cleanup(int retention_days) {
|
||||
return new Delete()
|
||||
.from(APStatus.class)
|
||||
.where("timestamp < ?", JoH.tsl() - (retention_days * 86400000L))
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
||||
// create the table ourselves without worrying about model versioning and downgrading
|
||||
public static void updateDB() {
|
||||
patched = fixUpTable(schema, patched);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
193
lib/nightscout/com/eveningoutpost/dexdrip/Models/Accuracy.java
Normal file
193
lib/nightscout/com/eveningoutpost/dexdrip/Models/Accuracy.java
Normal file
@@ -0,0 +1,193 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
/**
|
||||
* Created by jamorham on 01/02/2017.
|
||||
*/
|
||||
|
||||
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Select;
|
||||
import com.eveningoutpost.dexdrip.BestGlucose;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
|
||||
import com.google.gson.annotations.Expose;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Table(name = "Accuracy", id = BaseColumns._ID)
|
||||
public class Accuracy extends PlusModel {
|
||||
private static final String TAG = "Accuracy";
|
||||
private static boolean patched = false;
|
||||
static final String[] schema = {
|
||||
"CREATE TABLE Accuracy (_id INTEGER PRIMARY KEY AUTOINCREMENT);",
|
||||
"ALTER TABLE Accuracy ADD COLUMN timestamp INTEGER;",
|
||||
"ALTER TABLE Accuracy ADD COLUMN bg REAL;",
|
||||
"ALTER TABLE Accuracy ADD COLUMN bgtimestamp INTEGER;",
|
||||
"ALTER TABLE Accuracy ADD COLUMN bgsource TEXT;",
|
||||
"ALTER TABLE Accuracy ADD COLUMN plugin TEXT;",
|
||||
"ALTER TABLE Accuracy ADD COLUMN calculated REAL;",
|
||||
"ALTER TABLE Accuracy ADD COLUMN lag INTEGER;",
|
||||
"ALTER TABLE Accuracy ADD COLUMN difference REAL;",
|
||||
"CREATE INDEX index_Accuracy_timestamp on Accuracy(timestamp);",
|
||||
"CREATE INDEX index_Accuracy_bgtimestamp on Accuracy(bgtimestamp);"
|
||||
};
|
||||
|
||||
@Expose
|
||||
@Column(name = "timestamp", index = true)
|
||||
public long timestamp;
|
||||
|
||||
@Expose
|
||||
@Column(name = "bg")
|
||||
public double bg;
|
||||
|
||||
@Expose
|
||||
@Column(name = "bgtimestamp", index = true)
|
||||
public long bgtimestamp;
|
||||
|
||||
@Expose
|
||||
@Column(name = "bgsource")
|
||||
public String bgsource;
|
||||
|
||||
@Expose
|
||||
@Column(name = "plugin")
|
||||
public String plugin;
|
||||
|
||||
@Expose
|
||||
@Column(name = "calculated")
|
||||
public double calculated;
|
||||
|
||||
@Expose
|
||||
@Column(name = "lag")
|
||||
public boolean lag;
|
||||
|
||||
@Expose
|
||||
@Column(name = "difference")
|
||||
public double difference;
|
||||
|
||||
private static final boolean d = false;
|
||||
|
||||
public static Accuracy create(BloodTest bloodTest, BestGlucose.DisplayGlucose dg) {
|
||||
if (dg == null) return null;
|
||||
final BgReading from_dg = new BgReading();
|
||||
from_dg.timestamp = dg.timestamp;
|
||||
from_dg.calculated_value = dg.mgdl;
|
||||
return create(bloodTest, from_dg, dg.plugin_name);
|
||||
}
|
||||
|
||||
|
||||
public static Accuracy create(BloodTest bloodTest, BgReading bgReading, String plugin) {
|
||||
if ((bloodTest == null) || (bgReading == null)) return null;
|
||||
patched = fixUpTable(schema, patched);
|
||||
if (getForPreciseTimestamp(bgReading.timestamp, Constants.MINUTE_IN_MS, plugin) != null) {
|
||||
UserError.Log.d(TAG, "Duplicate accuracy timestamp for: " + JoH.dateTimeText(bgReading.timestamp));
|
||||
return null;
|
||||
}
|
||||
final Accuracy ac = new Accuracy();
|
||||
ac.timestamp = bgReading.timestamp;
|
||||
ac.bg = bloodTest.mgdl;
|
||||
ac.bgtimestamp = bloodTest.timestamp;
|
||||
ac.bgsource = bloodTest.source;
|
||||
ac.plugin = plugin;
|
||||
ac.calculated = bgReading.calculated_value;
|
||||
//ac.lag = bgReading.timestamp-bloodTest.timestamp;
|
||||
ac.difference = bgReading.calculated_value - bloodTest.mgdl;
|
||||
ac.save();
|
||||
return ac;
|
||||
}
|
||||
|
||||
static Accuracy getForPreciseTimestamp(double timestamp, double precision, String plugin) {
|
||||
patched = fixUpTable(schema, patched);
|
||||
final Accuracy accuracy = new Select()
|
||||
.from(Accuracy.class)
|
||||
.where("timestamp <= ?", (timestamp + precision))
|
||||
.where("timestamp >= ?", (timestamp - precision))
|
||||
.where("plugin = ?", plugin)
|
||||
.orderBy("abs(timestamp - " + timestamp + ") asc")
|
||||
.executeSingle();
|
||||
if (accuracy != null && Math.abs(accuracy.timestamp - timestamp) < precision) {
|
||||
return accuracy;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<Accuracy> latestForGraph(int number, long startTime, long endTime) {
|
||||
try {
|
||||
return new Select()
|
||||
.from(Accuracy.class)
|
||||
.where("timestamp >= " + Math.max(startTime, 0))
|
||||
.where("timestamp <= " + endTime)
|
||||
.orderBy("timestamp desc, _id asc")
|
||||
.limit(number)
|
||||
.execute();
|
||||
} catch (android.database.sqlite.SQLiteException e) {
|
||||
patched = fixUpTable(schema, patched);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
public static String evaluateAccuracy(long period) {
|
||||
// TODO CACHE ?
|
||||
final boolean domgdl = Pref.getString("units", "mgdl").equals("mgdl");
|
||||
final Map<String, Double> totals = new HashMap<>();
|
||||
final Map<String, Double> signed_totals = new HashMap<>();
|
||||
final Map<String, Integer> count = new HashMap<>();
|
||||
final List<Accuracy> alist = latestForGraph(500, JoH.tsl() - period, JoH.tsl());
|
||||
|
||||
// total up differences
|
||||
for (Accuracy entry : alist) {
|
||||
if (totals.containsKey(entry.plugin)) {
|
||||
totals.put(entry.plugin, totals.get(entry.plugin) + Math.abs(entry.difference));
|
||||
signed_totals.put(entry.plugin, signed_totals.get(entry.plugin) + entry.difference);
|
||||
count.put(entry.plugin, count.get(entry.plugin) + 1);
|
||||
} else {
|
||||
totals.put(entry.plugin, Math.abs(entry.difference));
|
||||
signed_totals.put(entry.plugin, entry.difference);
|
||||
count.put(entry.plugin, 1);
|
||||
}
|
||||
}
|
||||
String result = "";
|
||||
int plugin_count = 0;
|
||||
for (Map.Entry<String, Double> total : totals.entrySet()) {
|
||||
plugin_count++;
|
||||
final String plugin = total.getKey();
|
||||
final int this_count = count.get(plugin);
|
||||
final double this_total = total.getValue();
|
||||
// calculate the abs mean, 0 = perfect
|
||||
final double this_mean = this_total / this_count;
|
||||
final double signed_total = signed_totals.get(plugin);
|
||||
final double signed_mean = signed_total / this_count;
|
||||
// calculate the bias ratio. 0% means totally unbiased, 100% means all data skewed towards signed mean
|
||||
final double signed_ratio = (Math.abs(signed_mean) / this_mean) * 100;
|
||||
|
||||
if (d) UserError.Log.d(TAG, plugin + ": total: " + JoH.qs(this_total) + " count: " + this_count + " avg: " + JoH.qs(this_mean) + " mmol: " + JoH.qs((this_mean) * Constants.MGDL_TO_MMOLL) + " bias: " + JoH.qs(signed_mean) + " " + JoH.qs(signed_ratio, 0) + "%");
|
||||
String plugin_result = plugin.substring(0, 1).toLowerCase() + ": " + asString(this_mean, signed_mean, signed_ratio, domgdl);
|
||||
UserError.Log.d(TAG, plugin_result);
|
||||
if (result.length() > 0) result += " ";
|
||||
result += plugin_result;
|
||||
}
|
||||
|
||||
return plugin_count == 1 ? result : result.replaceFirst(" mmol", "").replaceFirst(" mgdl", " ");
|
||||
}
|
||||
|
||||
private static String asString(double mean, double signed_mean, double signed_ratio, boolean domgdl) {
|
||||
|
||||
String symbol = "err";
|
||||
if (signed_ratio < 90) {
|
||||
symbol = "\u00B1"; // +- symbol
|
||||
} else {
|
||||
if (signed_mean < 0) {
|
||||
symbol = "\u207B"; // superscript minus
|
||||
} else {
|
||||
symbol = "\u207A"; // superscript plus
|
||||
}
|
||||
}
|
||||
return symbol + (!domgdl ? JoH.qs(mean * Constants.MGDL_TO_MMOLL, 2) + " mmol" : JoH.qs(mean, 1) + " mgdl");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.activeandroid.util.SQLiteUtils;
|
||||
import com.eveningoutpost.dexdrip.Models.UserError.Log;
|
||||
|
||||
import com.activeandroid.Model;
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Select;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.AlertPlayer;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Created by Emma Black on 1/14/15.
|
||||
*/
|
||||
@Table(name = "ActiveBgAlert", id = BaseColumns._ID)
|
||||
public class ActiveBgAlert extends Model {
|
||||
|
||||
private final static String TAG = AlertPlayer.class.getSimpleName();
|
||||
private static boolean patched = false;
|
||||
|
||||
@Column(name = "alert_uuid")
|
||||
public String alert_uuid;
|
||||
|
||||
@Column(name = "is_snoozed")
|
||||
public volatile boolean is_snoozed;
|
||||
|
||||
@Column(name = "last_alerted_at") // Do we need this
|
||||
public volatile Long last_alerted_at;
|
||||
|
||||
@Column(name = "next_alert_at")
|
||||
public volatile Long next_alert_at;
|
||||
|
||||
// This is needed in order to have ascending alerts
|
||||
// we set the real value of it when is_snoozed is being turned to false
|
||||
@Column(name = "alert_started_at")
|
||||
public volatile Long alert_started_at;
|
||||
|
||||
|
||||
public boolean ready_to_alarm() {
|
||||
if(new Date().getTime() > next_alert_at) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean currentlyAlerting() {
|
||||
final ActiveBgAlert activeBgAlert = getOnly();
|
||||
return activeBgAlert != null && !activeBgAlert.is_snoozed;
|
||||
}
|
||||
|
||||
public static boolean alertSnoozeOver() {
|
||||
ActiveBgAlert activeBgAlert = getOnly();
|
||||
if (activeBgAlert == null) {
|
||||
// no alert exists, so snoozing is over... (this should not happen)
|
||||
Log.wtf(TAG, "ActiveBgAlert getOnly returning null (we have just checked it)");
|
||||
return true;
|
||||
}
|
||||
return activeBgAlert.ready_to_alarm();
|
||||
}
|
||||
|
||||
public void snooze(int minutes) {
|
||||
next_alert_at = new Date().getTime() + minutes * 60000;
|
||||
is_snoozed = true;
|
||||
Log.ueh("Snoozed Alert","Snoozed until: "+JoH.dateTimeText(next_alert_at));
|
||||
save();
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
|
||||
try {
|
||||
String alert_uuid = "alert_uuid: " + this.alert_uuid;
|
||||
String is_snoozed = "is_snoozed: " + this.is_snoozed;
|
||||
String last_alerted_at = "last_alerted_at: " + DateFormat.getDateTimeInstance(
|
||||
DateFormat.LONG, DateFormat.LONG).format(new Date(this.last_alerted_at));
|
||||
String next_alert_at = "next_alert_at: " + DateFormat.getDateTimeInstance(
|
||||
DateFormat.LONG, DateFormat.LONG).format(new Date(this.next_alert_at));
|
||||
|
||||
String alert_started_at = "alert_started_at: " + DateFormat.getDateTimeInstance(
|
||||
DateFormat.LONG, DateFormat.LONG).format(new Date(this.alert_started_at));
|
||||
|
||||
return alert_uuid + " " + is_snoozed + " " + last_alerted_at + " " + next_alert_at + " " + alert_started_at;
|
||||
|
||||
} catch (NullPointerException e) {
|
||||
Log.e(TAG, "Got Nullpointer exception in toString()! " + e);
|
||||
return "Nullpointer exception in toString!";
|
||||
}
|
||||
}
|
||||
|
||||
// We should only have at most one active alert at any given time.
|
||||
// This means that we will only have one of this objects at the database at any given time.
|
||||
// so we have the following static functions: getOnly, saveData, ClearData
|
||||
|
||||
public static ActiveBgAlert getOnly() {
|
||||
ActiveBgAlert aba = new Select()
|
||||
.from(ActiveBgAlert.class)
|
||||
.orderBy("_ID asc")
|
||||
.executeSingle();
|
||||
|
||||
if (aba != null) {
|
||||
Log.v(TAG, "ActiveBgAlert getOnly aba = " + aba.toString());
|
||||
} else {
|
||||
Log.v(TAG, "ActiveBgAlert getOnly returning null");
|
||||
}
|
||||
|
||||
return aba;
|
||||
}
|
||||
|
||||
public static AlertType alertTypegetOnly() {
|
||||
return alertTypegetOnly(getOnly());
|
||||
}
|
||||
|
||||
public static AlertType alertTypegetOnly(final ActiveBgAlert aba) {
|
||||
|
||||
if (aba == null) {
|
||||
Log.v(TAG, "ActiveBgAlert: alertTypegetOnly returning null");
|
||||
return null;
|
||||
}
|
||||
|
||||
AlertType alert = AlertType.get_alert(aba.alert_uuid);
|
||||
if(alert == null) {
|
||||
Log.d(TAG, "alertTypegetOnly did not find the active alert as part of existing alerts. returning null");
|
||||
// removing the alert to be in a better state.
|
||||
ClearData();
|
||||
return null;
|
||||
}
|
||||
if(!alert.uuid.equals(aba.alert_uuid)) {
|
||||
Log.wtf(TAG, "AlertType.get_alert did not return the correct alert");
|
||||
}
|
||||
return alert;
|
||||
}
|
||||
|
||||
public static void Create(String alert_uuid, boolean is_snoozed, Long next_alert_at) {
|
||||
Log.d(TAG, "ActiveBgAlert Create called");
|
||||
fixUpTable();
|
||||
ActiveBgAlert aba = getOnly();
|
||||
if (aba == null) {
|
||||
aba = new ActiveBgAlert();
|
||||
}
|
||||
aba.alert_uuid = alert_uuid;
|
||||
aba.is_snoozed = is_snoozed;
|
||||
aba.last_alerted_at = 0L;
|
||||
aba.next_alert_at = next_alert_at;
|
||||
aba.alert_started_at = new Date().getTime();
|
||||
aba.save();
|
||||
}
|
||||
|
||||
public static void ClearData() {
|
||||
Log.d(TAG, "ActiveBgAlert ClearData called");
|
||||
ActiveBgAlert aba = getOnly();
|
||||
if (aba != null) {
|
||||
aba.delete();
|
||||
}
|
||||
}
|
||||
|
||||
public static void ClearIfSnoozeFinished() {
|
||||
Log.d(TAG, "ActiveBgAlert ClearIfSnoozeFinished called");
|
||||
ActiveBgAlert aba = getOnly();
|
||||
if (aba != null) {
|
||||
if(new Date().getTime() > aba.next_alert_at) {
|
||||
Log.d(TAG, "ActiveBgAlert ClearIfSnoozeFinished deleting allert");
|
||||
aba.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function is called from ClockTick, when we play
|
||||
// If we were snoozed, we update the snooze to false, and update the start time.
|
||||
// return the time in minutes from the time playing the alert has started
|
||||
public int getAndUpdateAlertingMinutes() {
|
||||
if(is_snoozed) {
|
||||
is_snoozed = false;
|
||||
alert_started_at = new Date().getTime();
|
||||
save();
|
||||
}
|
||||
Long timeSeconds = (new Date().getTime() - alert_started_at) / 1000;
|
||||
return (int)Math.round(timeSeconds / 60.0);
|
||||
}
|
||||
|
||||
public void updateNextAlertAt(long nextAlertTime){
|
||||
next_alert_at = nextAlertTime;
|
||||
save();
|
||||
}
|
||||
|
||||
private static void fixUpTable() {
|
||||
if (patched) return;
|
||||
String[] patchup = {
|
||||
"ALTER TABLE ActiveBgAlert ADD COLUMN alert_started_at INTEGER;"
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.activeandroid.Model;
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Select;
|
||||
|
||||
/**
|
||||
* Created by Emma Black on 11/3/14.
|
||||
*/
|
||||
@Table(name = "ActiveBluetoothDevice", id = BaseColumns._ID)
|
||||
public class ActiveBluetoothDevice extends Model {
|
||||
@Column(name = "name")
|
||||
public String name;
|
||||
|
||||
@Column(name = "address")
|
||||
public String address;
|
||||
|
||||
@Column(name = "connected")
|
||||
public boolean connected;
|
||||
|
||||
|
||||
public static final Object table_lock = new Object();
|
||||
|
||||
public static synchronized ActiveBluetoothDevice first() {
|
||||
return new Select()
|
||||
.from(ActiveBluetoothDevice.class)
|
||||
.orderBy("_ID asc")
|
||||
.executeSingle();
|
||||
}
|
||||
|
||||
public static synchronized void forget() {
|
||||
ActiveBluetoothDevice activeBluetoothDevice = ActiveBluetoothDevice.first();
|
||||
if (activeBluetoothDevice != null) {
|
||||
activeBluetoothDevice.delete();
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void connected() {
|
||||
ActiveBluetoothDevice activeBluetoothDevice = ActiveBluetoothDevice.first();
|
||||
if(activeBluetoothDevice != null) {
|
||||
activeBluetoothDevice.connected = true;
|
||||
activeBluetoothDevice.save();
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void disconnected() {
|
||||
ActiveBluetoothDevice activeBluetoothDevice = ActiveBluetoothDevice.first();
|
||||
if(activeBluetoothDevice != null) {
|
||||
activeBluetoothDevice.connected = false;
|
||||
activeBluetoothDevice.save();
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized boolean is_connected() {
|
||||
ActiveBluetoothDevice activeBluetoothDevice = ActiveBluetoothDevice.first();
|
||||
return (activeBluetoothDevice != null && activeBluetoothDevice.connected);
|
||||
}
|
||||
|
||||
}
|
||||
677
lib/nightscout/com/eveningoutpost/dexdrip/Models/AlertType.java
Normal file
677
lib/nightscout/com/eveningoutpost/dexdrip/Models/AlertType.java
Normal file
@@ -0,0 +1,677 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.activeandroid.util.SQLiteUtils;
|
||||
import com.eveningoutpost.dexdrip.Models.UserError.Log;
|
||||
import com.activeandroid.Model;
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Select;
|
||||
import com.eveningoutpost.dexdrip.Services.ActivityRecognizedService;
|
||||
import com.eveningoutpost.dexdrip.Services.MissedReadingService;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.AlertPlayer;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Notifications;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.internal.bind.DateTypeAdapter;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
|
||||
/**
|
||||
* Created by Emma Black on 1/14/15.
|
||||
*/
|
||||
@Table(name = "AlertType", id = BaseColumns._ID)
|
||||
public class AlertType extends Model {
|
||||
|
||||
@Expose
|
||||
@Column(name = "name")
|
||||
public String name;
|
||||
|
||||
@Expose
|
||||
@Column(name = "active")
|
||||
public boolean active;
|
||||
|
||||
@Expose
|
||||
@Column(name = "volume")
|
||||
public int volume;
|
||||
|
||||
@Expose
|
||||
@Column(name = "vibrate")
|
||||
public boolean vibrate;
|
||||
|
||||
@Expose
|
||||
@Column(name = "light")
|
||||
public boolean light;
|
||||
|
||||
@Expose
|
||||
@Column(name = "override_silent_mode")
|
||||
public boolean override_silent_mode;
|
||||
|
||||
@Expose
|
||||
@Column(name = "force_speaker")
|
||||
public boolean force_speaker;
|
||||
|
||||
@Expose
|
||||
@Column(name = "predictive")
|
||||
public boolean predictive;
|
||||
|
||||
@Expose
|
||||
@Column(name = "time_until_threshold_crossed")
|
||||
public double time_until_threshold_crossed;
|
||||
|
||||
// If it is not above, then it must be below.
|
||||
@Expose
|
||||
@Column(name = "above")
|
||||
public boolean above;
|
||||
|
||||
@Expose
|
||||
@Column(name = "threshold")
|
||||
public double threshold;
|
||||
|
||||
@Expose
|
||||
@Column(name = "all_day")
|
||||
public boolean all_day;
|
||||
|
||||
@Expose
|
||||
@Column(name = "start_time_minutes")
|
||||
public int start_time_minutes; // This have probable be in minutes from start of day. this is not time...
|
||||
|
||||
@Expose
|
||||
@Column(name = "end_time_minutes")
|
||||
public int end_time_minutes;
|
||||
|
||||
@Expose
|
||||
@Column(name = "minutes_between") //??? what is the difference between minutes_between and default_snooze ???
|
||||
public int minutes_between; // The idea here was if ignored it will go off again each x minutes, snooze would be if it was aknowledged and dismissed it will go off again in y minutes
|
||||
// that said, Im okay with doing away with the minutes between and just doing it at a set 5 mins like dex
|
||||
|
||||
@Expose
|
||||
@Column(name = "default_snooze")
|
||||
public int default_snooze;
|
||||
|
||||
@Expose
|
||||
@Column(name = "text") // ??? what's that? is it different from name?
|
||||
public String text; // I figured if we wanted some special text, Its
|
||||
|
||||
@Expose
|
||||
@Column(name = "mp3_file")
|
||||
public String mp3_file;
|
||||
|
||||
@Expose
|
||||
@Column(name = "uuid", index = true)
|
||||
public String uuid;
|
||||
|
||||
public final static String LOW_ALERT_55 = "c5f1999c-4ec5-449e-adad-3980b172b920";
|
||||
private final static String TAG = Notifications.class.getSimpleName();
|
||||
private final static String TAG_ALERT = "AlertBg";
|
||||
private static boolean patched = false;
|
||||
|
||||
// This shouldn't be needed but it seems it is
|
||||
public static void fixUpTable() {
|
||||
if (patched) return;
|
||||
String[] patchup = {
|
||||
"ALTER TABLE AlertType ADD COLUMN volume INTEGER;",
|
||||
"ALTER TABLE AlertType ADD COLUMN light INTEGER;",
|
||||
"ALTER TABLE AlertType ADD COLUMN predictive INTEGER;",
|
||||
"ALTER TABLE AlertType ADD COLUMN text TEXT;",
|
||||
"ALTER TABLE AlertType ADD COLUMN force_speaker INTEGER;",
|
||||
"ALTER TABLE AlertType ADD COLUMN time_until_threshold_crossed REAL;"
|
||||
};
|
||||
|
||||
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 AlertType get_alert(String uuid) {
|
||||
|
||||
return new Select()
|
||||
.from(AlertType.class)
|
||||
.where("uuid = ? ", uuid)
|
||||
.executeSingle();
|
||||
}
|
||||
|
||||
/*
|
||||
* This function has 3 needs. In the case of "unclear state" return null
|
||||
* In the case of "unclear state" for more than predefined time, return the "55" alert
|
||||
* In case that alerts are turned off, only return the 55.
|
||||
*/
|
||||
public static AlertType get_highest_active_alert(Context context, double bg) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
if(prefs.getLong("alerts_disabled_until", 0) > new Date().getTime()){
|
||||
Log.d("NOTIFICATIONS", "Notifications are currently disabled!!");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (bg <= 14) { // Special dexcom codes should not set off low alarms
|
||||
return null;
|
||||
}
|
||||
|
||||
AlertType at;
|
||||
at = get_highest_active_alert_helper(bg, prefs);
|
||||
if (at != null) {
|
||||
Log.d(TAG_ALERT, "get_highest_active_alert_helper returned alert uuid = " + at.uuid + " alert name = " + at.name);
|
||||
} else {
|
||||
Log.d(TAG_ALERT, "get_highest_active_alert_helper returned NULL");
|
||||
}
|
||||
return at;
|
||||
}
|
||||
|
||||
private static AlertType filter_alert_on_stale(AlertType alert, SharedPreferences prefs)
|
||||
{
|
||||
// this should already be happening in notifications.java but it doesn't seem to work so adding here as well
|
||||
if (prefs.getBoolean("disable_alerts_stale_data", false)) {
|
||||
final int stale_minutes = Math.max(6, Integer.parseInt(prefs.getString("disable_alerts_stale_data_minutes", "15")) + 2);
|
||||
if (!BgReading.last_within_minutes(stale_minutes)) {
|
||||
Log.w(TAG, "Blocking alarm raise as data older than: " + stale_minutes);
|
||||
return null; // block
|
||||
}
|
||||
}
|
||||
return alert; // allow
|
||||
}
|
||||
|
||||
// bg_minute is the estimatin of the bg change rate
|
||||
private static AlertType get_highest_active_alert_helper(double bg, SharedPreferences prefs) {
|
||||
// Chcek the low alerts
|
||||
|
||||
final double offset = ActivityRecognizedService.raise_limit_due_to_vehicle_mode() ? ActivityRecognizedService.getVehicle_mode_adjust_mgdl() : 0;
|
||||
|
||||
if(prefs.getLong("low_alerts_disabled_until", 0) > new Date().getTime()){
|
||||
Log.i("NOTIFICATIONS", "get_highest_active_alert_helper: Low alerts are currently disabled!! Skipping low alerts");
|
||||
|
||||
} else {
|
||||
List<AlertType> lowAlerts = new Select()
|
||||
.from(AlertType.class)
|
||||
.where("threshold >= ?", bg-offset)
|
||||
.where("above = ?", false)
|
||||
.orderBy("threshold asc")
|
||||
.execute();
|
||||
|
||||
for (AlertType lowAlert : lowAlerts) {
|
||||
if(lowAlert.should_alarm(bg-offset)) {
|
||||
return filter_alert_on_stale(lowAlert,prefs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If no low alert found or low alerts disabled, check higher alert.
|
||||
if(prefs.getLong("high_alerts_disabled_until", 0) > new Date().getTime()){
|
||||
Log.i("NOTIFICATIONS", "get_highest_active_alert_helper: High alerts are currently disabled!! Skipping high alerts");
|
||||
;
|
||||
} else {
|
||||
List<AlertType> HighAlerts = new Select()
|
||||
.from(AlertType.class)
|
||||
.where("threshold <= ?", bg)
|
||||
.where("above = ?", true)
|
||||
.orderBy("threshold desc")
|
||||
.execute();
|
||||
|
||||
for (AlertType HighAlert : HighAlerts) {
|
||||
//Log.e(TAG, "Testing high alert " + HighAlert.toString());
|
||||
if(HighAlert.should_alarm(bg)) {
|
||||
return filter_alert_on_stale(HighAlert,prefs);
|
||||
}
|
||||
}
|
||||
}
|
||||
// no alert found
|
||||
return null;
|
||||
}
|
||||
|
||||
// returns true, if one allert is up and the second is down
|
||||
public static boolean OpositeDirection(AlertType a1, AlertType a2) {
|
||||
if (a1.above != a2.above) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Checks if a1 is more important than a2. returns the higher one
|
||||
public static AlertType HigherAlert(AlertType a1, AlertType a2) {
|
||||
if (a1.above && !a2.above) {
|
||||
return a2;
|
||||
}
|
||||
if (!a1.above && a2.above) {
|
||||
return a1;
|
||||
}
|
||||
if (a1.above && a2.above) {
|
||||
// both are high, the higher the better
|
||||
if (a1.threshold > a2.threshold) {
|
||||
return a1;
|
||||
} else {
|
||||
return a2;
|
||||
}
|
||||
}
|
||||
if (a1.above || a2.above) {
|
||||
Log.wtf(TAG, "a1.above and a2.above must be false");
|
||||
}
|
||||
// both are low, the lower the better
|
||||
if (a1.threshold < a2.threshold) {
|
||||
return a1;
|
||||
} else {
|
||||
return a2;
|
||||
}
|
||||
}
|
||||
|
||||
public static void remove_all() {
|
||||
List<AlertType> Alerts = new Select()
|
||||
.from(AlertType.class)
|
||||
.execute();
|
||||
|
||||
for (AlertType alert : Alerts) {
|
||||
alert.delete();
|
||||
}
|
||||
ActiveBgAlert.ClearData();
|
||||
}
|
||||
|
||||
public static void add_alert(
|
||||
String uuid,
|
||||
String name,
|
||||
boolean above,
|
||||
double threshold,
|
||||
boolean all_day,
|
||||
int minutes_between,
|
||||
String mp3_file,
|
||||
int start_time_minutes,
|
||||
int end_time_minutes,
|
||||
boolean override_silent_mode,
|
||||
boolean force_speaker,
|
||||
int snooze,
|
||||
boolean vibrate,
|
||||
boolean active) {
|
||||
AlertType at = new AlertType();
|
||||
at.name = name;
|
||||
at.above = above;
|
||||
at.threshold = threshold;
|
||||
at.all_day = all_day;
|
||||
at.minutes_between = minutes_between;
|
||||
at.uuid = uuid != null? uuid : UUID.randomUUID().toString();
|
||||
at.active = active;
|
||||
at.mp3_file = mp3_file;
|
||||
at.start_time_minutes = start_time_minutes;
|
||||
at.end_time_minutes = end_time_minutes;
|
||||
at.override_silent_mode = override_silent_mode;
|
||||
at.force_speaker = force_speaker;
|
||||
at.default_snooze = snooze;
|
||||
at.vibrate = vibrate;
|
||||
at.save();
|
||||
}
|
||||
|
||||
public static void update_alert(
|
||||
String uuid,
|
||||
String name,
|
||||
boolean above,
|
||||
double threshold,
|
||||
boolean all_day,
|
||||
int minutes_between,
|
||||
String mp3_file,
|
||||
int start_time_minutes,
|
||||
int end_time_minutes,
|
||||
boolean override_silent_mode,
|
||||
boolean force_speaker,
|
||||
int snooze,
|
||||
boolean vibrate,
|
||||
boolean active) {
|
||||
|
||||
fixUpTable();
|
||||
|
||||
final AlertType at = get_alert(uuid);
|
||||
if (at == null) {
|
||||
Log.e(TAG, "Alert Type null during update");
|
||||
return;
|
||||
}
|
||||
at.name = name;
|
||||
at.above = above;
|
||||
at.threshold = threshold;
|
||||
at.all_day = all_day;
|
||||
at.minutes_between = minutes_between;
|
||||
at.uuid = uuid;
|
||||
at.active = active;
|
||||
at.mp3_file = mp3_file;
|
||||
at.start_time_minutes = start_time_minutes;
|
||||
at.end_time_minutes = end_time_minutes;
|
||||
at.override_silent_mode = override_silent_mode;
|
||||
at.force_speaker = force_speaker;
|
||||
at.default_snooze = snooze;
|
||||
at.vibrate = vibrate;
|
||||
at.save();
|
||||
}
|
||||
public static void remove_alert(String uuid) {
|
||||
AlertType alert = get_alert(uuid);
|
||||
if(alert != null) {
|
||||
alert.delete();
|
||||
}
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
|
||||
String name = "name: " + this.name;
|
||||
String above = "above: " + this.above;
|
||||
String threshold = "threshold: " + this.threshold;
|
||||
String all_day = "all_day: " + this.all_day;
|
||||
String time = "Start time: " + this.start_time_minutes + " end time: "+ this.end_time_minutes;
|
||||
String minutes_between = "minutes_between: " + this.minutes_between;
|
||||
String uuid = "uuid: " + this.uuid;
|
||||
|
||||
return name + " " + above + " " + threshold + " "+ all_day + " " +time +" " + minutes_between + " uuid" + uuid;
|
||||
}
|
||||
|
||||
public String toS() {
|
||||
Gson gson = new GsonBuilder()
|
||||
.excludeFieldsWithoutExposeAnnotation()
|
||||
.registerTypeAdapter(Date.class, new DateTypeAdapter())
|
||||
.serializeSpecialFloatingPointValues()
|
||||
.create();
|
||||
return gson.toJson(this);
|
||||
}
|
||||
|
||||
public static void print_all() {
|
||||
List<AlertType> Alerts = new Select()
|
||||
.from(AlertType.class)
|
||||
.execute();
|
||||
|
||||
Log.d(TAG,"List of all alerts");
|
||||
for (AlertType alert : Alerts) {
|
||||
Log.d(TAG, alert.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// get the first item in the alert list which is active for either high or low alert, sorted by when the threshold will be hit, eg the highest low alert or the lowest high alert
|
||||
public static double getFirstActiveAlertThreshold(final boolean highAlert) {
|
||||
final List<AlertType> list = getAll(highAlert);
|
||||
if (list != null) {
|
||||
for (final AlertType alert : list) {
|
||||
if (alert.active) return alert.threshold;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static List<AlertType> getAllActive() {
|
||||
List<AlertType> alerts = new Select()
|
||||
.from(AlertType.class)
|
||||
.where("active = ?", true)
|
||||
.execute();
|
||||
|
||||
return alerts;
|
||||
}
|
||||
|
||||
public static List<AlertType> getAll(boolean above) {
|
||||
String order;
|
||||
if (above) {
|
||||
order = "threshold asc";
|
||||
} else {
|
||||
order = "threshold desc";
|
||||
}
|
||||
List<AlertType> alerts = new Select()
|
||||
.from(AlertType.class)
|
||||
.where("above = ?", above)
|
||||
.orderBy(order)
|
||||
.execute();
|
||||
|
||||
return alerts;
|
||||
}
|
||||
|
||||
public static boolean activeLowAlertExists() {
|
||||
List<AlertType> alerts = getAll(false);
|
||||
if(alerts == null) {
|
||||
return false;
|
||||
}
|
||||
for (AlertType alert : alerts) {
|
||||
if(alert.active) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// This function is used to make sure that we always have a static alert on 55 low.
|
||||
// This alert will not be editable/removable.
|
||||
public static void CreateStaticAlerts() {
|
||||
if(get_alert(LOW_ALERT_55) == null) {
|
||||
add_alert(LOW_ALERT_55, "low alert ", false, 55, true, 1, null, 0, 0, true, true, 20, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void testAll(Context context) {
|
||||
remove_all();
|
||||
add_alert(null, "high alert 1", true, 180, true, 10, null, 0, 0, true, true, 20, true, true);
|
||||
add_alert(null, "high alert 2", true, 200, true, 10, null, 0, 0, true, true,20, true, true);
|
||||
add_alert(null, "high alert 3", true, 220, true, 10, null, 0, 0, true, true,20, true, true);
|
||||
print_all();
|
||||
AlertType a1 = get_highest_active_alert(context, 190);
|
||||
Log.d(TAG, "a1 = " + a1.toString());
|
||||
AlertType a2 = get_highest_active_alert(context, 210);
|
||||
Log.d(TAG, "a2 = " + a2.toString());
|
||||
|
||||
|
||||
AlertType a3 = get_alert(a1.uuid);
|
||||
Log.d(TAG, "a1 == a3 ? need to see true " + (a1==a3) + a1 + " " + a3);
|
||||
|
||||
add_alert(null, "low alert 1", false, 80, true, 10, null, 0, 0, true, true,20, true, true);
|
||||
add_alert(null, "low alert 2", false, 60, true, 10, null, 0, 0, true, true,20, true, true);
|
||||
|
||||
AlertType al1 = get_highest_active_alert(context, 90);
|
||||
Log.d(TAG, "al1 should be null " + al1);
|
||||
al1 = get_highest_active_alert(context, 80);
|
||||
Log.d(TAG, "al1 = " + al1.toString());
|
||||
AlertType al2 = get_highest_active_alert(context, 50);
|
||||
Log.d(TAG, "al2 = " + al2.toString());
|
||||
|
||||
Log.d(TAG, "HigherAlert(a1, a2) = a1?" + (HigherAlert(a1,a2) == a2));
|
||||
Log.d(TAG, "HigherAlert(al1, al2) = al1?" + (HigherAlert(al1,al2) == al2));
|
||||
Log.d(TAG, "HigherAlert(a1, al1) = al1?" + (HigherAlert(a1,al1) == al1));
|
||||
Log.d(TAG, "HigherAlert(al1, a2) = al1?" + (HigherAlert(al1,a2) == al1));
|
||||
|
||||
// Make sure we do not influance on real data...
|
||||
remove_all();
|
||||
|
||||
}
|
||||
|
||||
|
||||
private boolean in_time_frame() {
|
||||
return s_in_time_frame(all_day, start_time_minutes, end_time_minutes);
|
||||
}
|
||||
|
||||
static public boolean s_in_time_frame(boolean s_all_day, int s_start_time_minutes, int s_end_time_minutes) {
|
||||
if (s_all_day) {
|
||||
//Log.e(TAG, "in_time_frame returning true " );
|
||||
return true;
|
||||
}
|
||||
// time_now is the number of minutes that have passed from the start of the day.
|
||||
Calendar rightNow = Calendar.getInstance();
|
||||
int time_now = toTime(rightNow.get(Calendar.HOUR_OF_DAY), rightNow.get(Calendar.MINUTE));
|
||||
Log.d(TAG, "time_now is " + time_now + " minutes" + " start_time " + s_start_time_minutes + " end_time " + s_end_time_minutes);
|
||||
if(s_start_time_minutes < s_end_time_minutes) {
|
||||
if (time_now >= s_start_time_minutes && time_now <= s_end_time_minutes) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (time_now >= s_start_time_minutes || time_now <= s_end_time_minutes) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean beyond_threshold(double bg) {
|
||||
if (above && bg >= threshold) {
|
||||
// Log.e(TAG, "beyond_threshold returning true " );
|
||||
return true;
|
||||
} else if (!above && bg <= threshold) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean trending_to_threshold(double bg) {
|
||||
if (!predictive) { return false; }
|
||||
if (above && bg >= threshold) {
|
||||
return true;
|
||||
} else if (!above && bg <= threshold) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public long getNextAlertTime(Context ctx) {
|
||||
int time = minutes_between;
|
||||
if (time < 1 || AlertPlayer.isAscendingMode(ctx)) {
|
||||
time = 1;
|
||||
}
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
return calendar.getTimeInMillis() + (time * 60000);
|
||||
}
|
||||
|
||||
public boolean should_alarm(double bg) {
|
||||
// Log.e(TAG, "should_alarm called active = " + active );
|
||||
if(in_time_frame() && active && (beyond_threshold(bg) || trending_to_threshold(bg))) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void testAlert(
|
||||
String name,
|
||||
boolean above,
|
||||
double threshold,
|
||||
boolean all_day,
|
||||
int minutes_between,
|
||||
String mp3_file,
|
||||
int start_time_minutes,
|
||||
int end_time_minutes,
|
||||
boolean override_silent_mode,
|
||||
boolean force_speaker,
|
||||
int snooze,
|
||||
boolean vibrate,
|
||||
Context context) {
|
||||
AlertType at = new AlertType();
|
||||
at.name = name;
|
||||
at.above = above;
|
||||
at.threshold = threshold;
|
||||
at.all_day = all_day;
|
||||
at.minutes_between = minutes_between;
|
||||
at.uuid = UUID.randomUUID().toString();
|
||||
at.active = true;
|
||||
at.mp3_file = mp3_file;
|
||||
at.start_time_minutes = start_time_minutes;
|
||||
at.end_time_minutes = end_time_minutes;
|
||||
at.override_silent_mode = override_silent_mode;
|
||||
at.force_speaker = force_speaker;
|
||||
at.default_snooze = snooze;
|
||||
at.vibrate = vibrate;
|
||||
AlertPlayer.getPlayer().startAlert(context, false, at, "TEST", false);
|
||||
}
|
||||
|
||||
// Time is calculated in minutes. that is 01:20 means 80 minutes.
|
||||
|
||||
// This functions are a bit tricky. We can only set time from 00:00 to 23:59 which leaves one minute out. this is because we ignore the
|
||||
// seconds. so if the user has set 23:59 we will consider this as 24:00
|
||||
// This will be done at the code that reads the time from the ui.
|
||||
|
||||
|
||||
|
||||
// return the minutes part of the time
|
||||
public static int time2Minutes(int minutes) {
|
||||
return (minutes - 60*time2Hours(minutes)) ;
|
||||
}
|
||||
|
||||
// return the hours part of the time
|
||||
public static int time2Hours(int minutes) {
|
||||
return minutes / 60;
|
||||
}
|
||||
|
||||
// create the time from hours and minutes.
|
||||
public static int toTime(int hours, int minutes) {
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
// Convert all settings to a string and save it in the references. This is needed to allow it's backup.
|
||||
public static boolean toSettings(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
List<AlertType> alerts = new Select()
|
||||
.from(AlertType.class)
|
||||
.execute();
|
||||
|
||||
Gson gson = new GsonBuilder()
|
||||
.excludeFieldsWithoutExposeAnnotation()
|
||||
.registerTypeAdapter(Date.class, new DateTypeAdapter())
|
||||
.serializeSpecialFloatingPointValues()
|
||||
.create();
|
||||
String output = gson.toJson(alerts);
|
||||
Log.e(TAG, "Created the string " + output);
|
||||
prefs.edit().putString("saved_alerts", output).commit(); // always leave this as commit
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Read all alerts from preference key and write them to db.
|
||||
public static boolean fromSettings(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
String savedAlerts = prefs.getString("saved_alerts", "");
|
||||
if (savedAlerts.isEmpty()) {
|
||||
Log.i(TAG, "read saved_alerts string and it is empty");
|
||||
return true;
|
||||
}
|
||||
Log.i(TAG, "read alerts string " + savedAlerts);
|
||||
|
||||
AlertType[] newAlerts = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(savedAlerts, AlertType[].class);
|
||||
if (newAlerts == null) {
|
||||
Log.e(TAG, "newAlerts is null");
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.i(TAG, "read successfuly " + newAlerts.length);
|
||||
// Now delete all existing alerts if we managed to unpack the json
|
||||
try {
|
||||
List<AlertType> alerts = new Select()
|
||||
.from(AlertType.class)
|
||||
.execute();
|
||||
for (AlertType alert : alerts) {
|
||||
alert.delete();
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
Log.e(TAG, "Got null pointer exception: " + e);
|
||||
}
|
||||
|
||||
try {
|
||||
for (AlertType alert : newAlerts) {
|
||||
Log.e(TAG, "Saving alert " + alert.name);
|
||||
alert.save();
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
Log.e(TAG, "Got null pointer exception 2: " + e);
|
||||
}
|
||||
// Delete the string, so next time we will not load the data
|
||||
prefs.edit().putString("saved_alerts", "").apply();
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
2334
lib/nightscout/com/eveningoutpost/dexdrip/Models/BgReading.java
Normal file
2334
lib/nightscout/com/eveningoutpost/dexdrip/Models/BgReading.java
Normal file
File diff suppressed because it is too large
Load Diff
577
lib/nightscout/com/eveningoutpost/dexdrip/Models/BloodTest.java
Normal file
577
lib/nightscout/com/eveningoutpost/dexdrip/Models/BloodTest.java
Normal file
@@ -0,0 +1,577 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.provider.BaseColumns;
|
||||
import android.util.Log;
|
||||
|
||||
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.AddCalibration;
|
||||
import com.eveningoutpost.dexdrip.GlucoseMeter.GlucoseReadingRx;
|
||||
import com.eveningoutpost.dexdrip.Home;
|
||||
import com.eveningoutpost.dexdrip.Services.SyncService;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.BgGraphBuilder;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.UploaderQueue;
|
||||
import com.eveningoutpost.dexdrip.calibrations.CalibrationAbstract;
|
||||
import com.eveningoutpost.dexdrip.calibrations.NativeCalibrationPipe;
|
||||
import com.eveningoutpost.dexdrip.calibrations.PluggableCalibration;
|
||||
import com.eveningoutpost.dexdrip.messages.BloodTestMessage;
|
||||
import com.eveningoutpost.dexdrip.messages.BloodTestMultiMessage;
|
||||
import com.eveningoutpost.dexdrip.xdrip;
|
||||
import com.google.common.math.DoubleMath;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.squareup.wire.Wire;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Created by jamorham on 11/12/2016.
|
||||
*/
|
||||
|
||||
@Table(name = "BloodTest", id = BaseColumns._ID)
|
||||
public class BloodTest extends Model {
|
||||
|
||||
public static final long STATE_VALID = 1 << 0;
|
||||
public static final long STATE_CALIBRATION = 1 << 1;
|
||||
public static final long STATE_NOTE = 1 << 2;
|
||||
public static final long STATE_UNDONE = 1 << 3;
|
||||
public static final long STATE_OVERWRITTEN = 1 << 4;
|
||||
|
||||
private static long highest_timestamp = 0;
|
||||
private static boolean patched = false;
|
||||
private final static String TAG = "BloodTest";
|
||||
private final static String LAST_BT_AUTO_CALIB_UUID = "last-bt-auto-calib-uuid";
|
||||
private final static boolean d = false;
|
||||
|
||||
@Expose
|
||||
@Column(name = "timestamp", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE)
|
||||
public long timestamp;
|
||||
|
||||
@Expose
|
||||
@Column(name = "mgdl")
|
||||
public double mgdl;
|
||||
|
||||
@Expose
|
||||
@Column(name = "created_timestamp")
|
||||
public long created_timestamp;
|
||||
|
||||
@Expose
|
||||
@Column(name = "state")
|
||||
public long state; // bitfield
|
||||
|
||||
@Expose
|
||||
@Column(name = "source")
|
||||
public String source;
|
||||
|
||||
@Expose
|
||||
@Column(name = "uuid", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE)
|
||||
public String uuid;
|
||||
|
||||
|
||||
public GlucoseReadingRx glucoseReadingRx;
|
||||
|
||||
// patches and saves
|
||||
public Long saveit() {
|
||||
fixUpTable();
|
||||
return save();
|
||||
}
|
||||
|
||||
public void addState(long flag) {
|
||||
state |= flag;
|
||||
save();
|
||||
}
|
||||
|
||||
public void removeState(long flag) {
|
||||
state &= ~flag;
|
||||
save();
|
||||
}
|
||||
|
||||
public String toS() {
|
||||
final Gson gson = new GsonBuilder()
|
||||
.excludeFieldsWithoutExposeAnnotation()
|
||||
.create();
|
||||
return gson.toJson(this);
|
||||
}
|
||||
|
||||
private BloodTestMessage toMessageNative() {
|
||||
return new BloodTestMessage.Builder()
|
||||
.timestamp(timestamp)
|
||||
.mgdl(mgdl)
|
||||
.created_timestamp(created_timestamp)
|
||||
.state(state)
|
||||
.source(source)
|
||||
.uuid(uuid)
|
||||
.build();
|
||||
}
|
||||
|
||||
public byte[] toMessage() {
|
||||
final List<BloodTest> btl = new ArrayList<>();
|
||||
btl.add(this);
|
||||
return toMultiMessage(btl);
|
||||
}
|
||||
|
||||
|
||||
// static methods
|
||||
private static final long CLOSEST_READING_MS = 30000; // 30 seconds
|
||||
|
||||
public static BloodTest create(long timestamp_ms, double mgdl, String source) {
|
||||
return create(timestamp_ms, mgdl, source, null);
|
||||
}
|
||||
|
||||
public static BloodTest create(long timestamp_ms, double mgdl, String source, String suggested_uuid) {
|
||||
|
||||
if ((timestamp_ms == 0) || (mgdl == 0)) {
|
||||
UserError.Log.e(TAG, "Either timestamp or mgdl is zero - cannot create reading");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (timestamp_ms < 1487759433000L) {
|
||||
UserError.Log.d(TAG, "Timestamp really too far in the past @ " + timestamp_ms);
|
||||
return null;
|
||||
}
|
||||
|
||||
final long now = JoH.tsl();
|
||||
if (timestamp_ms > now) {
|
||||
if ((timestamp_ms - now) > 600000) {
|
||||
UserError.Log.wtf(TAG, "Timestamp is > 10 minutes in the future! Something is wrong: " + JoH.dateTimeText(timestamp_ms));
|
||||
return null;
|
||||
}
|
||||
timestamp_ms = now; // force to now if it showed up to 10 mins in the future
|
||||
}
|
||||
|
||||
final BloodTest match = getForPreciseTimestamp(timestamp_ms, CLOSEST_READING_MS);
|
||||
if (match == null) {
|
||||
final BloodTest bt = new BloodTest();
|
||||
bt.timestamp = timestamp_ms;
|
||||
bt.mgdl = mgdl;
|
||||
bt.uuid = suggested_uuid == null ? UUID.randomUUID().toString() : suggested_uuid;
|
||||
bt.created_timestamp = JoH.tsl();
|
||||
bt.state = STATE_VALID;
|
||||
bt.source = source;
|
||||
bt.saveit();
|
||||
if (UploaderQueue.newEntry("insert", bt) != null) {
|
||||
SyncService.startSyncService(3000); // sync in 3 seconds
|
||||
}
|
||||
|
||||
if (Pref.getBooleanDefaultFalse("bluetooth_meter_for_calibrations_auto")) {
|
||||
if ((JoH.msSince(bt.timestamp) < Constants.MINUTE_IN_MS * 5) && (JoH.msSince(bt.timestamp) > 0)) {
|
||||
UserError.Log.d(TAG, "Blood test value recent enough to send to G5");
|
||||
//Ob1G5StateMachine.addCalibration((int) bt.mgdl, timestamp_ms);
|
||||
NativeCalibrationPipe.addCalibration((int) bt.mgdl, timestamp_ms);
|
||||
}
|
||||
}
|
||||
|
||||
return bt;
|
||||
} else {
|
||||
UserError.Log.d(TAG, "Not creating new reading as timestamp is too close");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static BloodTest createFromCal(double bg, double timeoffset, String source) {
|
||||
return createFromCal(bg, timeoffset, source, null);
|
||||
}
|
||||
|
||||
public static BloodTest createFromCal(double bg, double timeoffset, String source, String suggested_uuid) {
|
||||
final String unit = Pref.getString("units", "mgdl");
|
||||
|
||||
if (unit.compareTo("mgdl") != 0) {
|
||||
bg = bg * Constants.MMOLL_TO_MGDL;
|
||||
}
|
||||
|
||||
if ((bg < 40) || (bg > 400)) {
|
||||
Log.wtf(TAG, "Invalid out of range bloodtest glucose mg/dl value of: " + bg);
|
||||
JoH.static_toast_long("Bloodtest out of range: " + bg + " mg/dl");
|
||||
return null;
|
||||
}
|
||||
|
||||
return create((long) (new Date().getTime() - timeoffset), bg, source, suggested_uuid);
|
||||
}
|
||||
|
||||
public static void pushBloodTestSyncToWatch(BloodTest bt, boolean is_new) {
|
||||
Log.d(TAG, "pushTreatmentSyncToWatch Add treatment to UploaderQueue.");
|
||||
if (Pref.getBooleanDefaultFalse("wear_sync")) {
|
||||
if (UploaderQueue.newEntryForWatch(is_new ? "insert" : "update", bt) != null) {
|
||||
SyncService.startSyncService(3000); // sync in 3 seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static BloodTest last() {
|
||||
final List<BloodTest> btl = last(1);
|
||||
if ((btl != null) && (btl.size() > 0)) {
|
||||
return btl.get(0);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<BloodTest> last(int num) {
|
||||
try {
|
||||
return new Select()
|
||||
.from(BloodTest.class)
|
||||
.orderBy("timestamp desc")
|
||||
.limit(num)
|
||||
.execute();
|
||||
} catch (android.database.sqlite.SQLiteException e) {
|
||||
fixUpTable();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<BloodTest> lastMatching(int num, String match) {
|
||||
try {
|
||||
return new Select()
|
||||
.from(BloodTest.class)
|
||||
.where("source like ?", match)
|
||||
.orderBy("timestamp desc")
|
||||
.limit(num)
|
||||
.execute();
|
||||
} catch (android.database.sqlite.SQLiteException e) {
|
||||
fixUpTable();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static BloodTest lastValid() {
|
||||
final List<BloodTest> btl = lastValid(1);
|
||||
if ((btl != null) && (btl.size() > 0)) {
|
||||
return btl.get(0);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<BloodTest> lastValid(int num) {
|
||||
try {
|
||||
return new Select()
|
||||
.from(BloodTest.class)
|
||||
.where("state & ? != 0", BloodTest.STATE_VALID)
|
||||
.orderBy("timestamp desc")
|
||||
.limit(num)
|
||||
.execute();
|
||||
} catch (android.database.sqlite.SQLiteException e) {
|
||||
fixUpTable();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static BloodTest byUUID(String uuid) {
|
||||
if (uuid == null) return null;
|
||||
try {
|
||||
return new Select()
|
||||
.from(BloodTest.class)
|
||||
.where("uuid = ?", uuid)
|
||||
.executeSingle();
|
||||
} catch (android.database.sqlite.SQLiteException e) {
|
||||
fixUpTable();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static BloodTest byid(long id) {
|
||||
try {
|
||||
return new Select()
|
||||
.from(BloodTest.class)
|
||||
.where("_ID = ?", id)
|
||||
.executeSingle();
|
||||
} catch (android.database.sqlite.SQLiteException e) {
|
||||
fixUpTable();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] toMultiMessage(List<BloodTest> btl) {
|
||||
if (btl == null) return null;
|
||||
final List<BloodTestMessage> BloodTestMessageList = new ArrayList<>();
|
||||
for (BloodTest bt : btl) {
|
||||
BloodTestMessageList.add(bt.toMessageNative());
|
||||
}
|
||||
return BloodTestMultiMessage.ADAPTER.encode(new BloodTestMultiMessage(BloodTestMessageList));
|
||||
}
|
||||
|
||||
private static void processFromMessage(BloodTestMessage btm) {
|
||||
if ((btm != null) && (btm.uuid != null) && (btm.uuid.length() == 36)) {
|
||||
boolean is_new = false;
|
||||
BloodTest bt = byUUID(btm.uuid);
|
||||
if (bt == null) {
|
||||
bt = getForPreciseTimestamp(Wire.get(btm.timestamp, BloodTestMessage.DEFAULT_TIMESTAMP), CLOSEST_READING_MS);
|
||||
if (bt != null) {
|
||||
UserError.Log.wtf(TAG, "Error matches a different uuid with the same timestamp: " + bt.uuid + " vs " + btm.uuid + " skipping!");
|
||||
return;
|
||||
}
|
||||
bt = new BloodTest();
|
||||
is_new = true;
|
||||
} else {
|
||||
if (bt.state != Wire.get(btm.state, BloodTestMessage.DEFAULT_STATE)) {
|
||||
is_new = true;
|
||||
}
|
||||
}
|
||||
bt.timestamp = Wire.get(btm.timestamp, BloodTestMessage.DEFAULT_TIMESTAMP);
|
||||
bt.mgdl = Wire.get(btm.mgdl, BloodTestMessage.DEFAULT_MGDL);
|
||||
bt.created_timestamp = Wire.get(btm.created_timestamp, BloodTestMessage.DEFAULT_CREATED_TIMESTAMP);
|
||||
bt.state = Wire.get(btm.state, BloodTestMessage.DEFAULT_STATE);
|
||||
bt.source = Wire.get(btm.source, BloodTestMessage.DEFAULT_SOURCE);
|
||||
bt.uuid = btm.uuid;
|
||||
bt.saveit(); // de-dupe by uuid
|
||||
if (is_new) { // cannot handle updates yet
|
||||
if (UploaderQueue.newEntry(is_new ? "insert" : "update", bt) != null) {
|
||||
if (JoH.quietratelimit("start-sync-service", 5)) {
|
||||
SyncService.startSyncService(3000); // sync in 3 seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
UserError.Log.wtf(TAG, "processFromMessage uuid is null or invalid");
|
||||
}
|
||||
}
|
||||
|
||||
public static void processFromMultiMessage(byte[] payload) {
|
||||
try {
|
||||
final BloodTestMultiMessage btmm = BloodTestMultiMessage.ADAPTER.decode(payload);
|
||||
if ((btmm != null) && (btmm.bloodtest_message != null)) {
|
||||
for (BloodTestMessage btm : btmm.bloodtest_message) {
|
||||
processFromMessage(btm);
|
||||
}
|
||||
Home.staticRefreshBGCharts();
|
||||
}
|
||||
} catch (IOException | NullPointerException | IllegalStateException e) {
|
||||
UserError.Log.e(TAG, "exception processFromMessage: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
public static BloodTest fromJSON(String json) {
|
||||
if ((json == null) || (json.length() == 0)) {
|
||||
UserError.Log.d(TAG, "Empty json received in bloodtest fromJson");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
UserError.Log.d(TAG, "Processing incoming json: " + json);
|
||||
return new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json, BloodTest.class);
|
||||
} catch (Exception e) {
|
||||
UserError.Log.d(TAG, "Got exception parsing bloodtest json: " + e.toString());
|
||||
Home.toaststaticnext("Error on Bloodtest sync, probably decryption key mismatch");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static BloodTest getForPreciseTimestamp(long timestamp, long precision) {
|
||||
BloodTest bloodTest = new Select()
|
||||
.from(BloodTest.class)
|
||||
.where("timestamp <= ?", (timestamp + precision))
|
||||
.where("timestamp >= ?", (timestamp - precision))
|
||||
.orderBy("abs(timestamp - " + timestamp + ") asc")
|
||||
.executeSingle();
|
||||
if ((bloodTest != null) && (Math.abs(bloodTest.timestamp - timestamp) < precision)) {
|
||||
return bloodTest;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<BloodTest> latestForGraph(int number, double startTime) {
|
||||
return latestForGraph(number, (long) startTime, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static List<BloodTest> latestForGraph(int number, long startTime) {
|
||||
return latestForGraph(number, startTime, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static List<BloodTest> latestForGraph(int number, long startTime, long endTime) {
|
||||
try {
|
||||
return new Select()
|
||||
.from(BloodTest.class)
|
||||
.where("state & ? != 0", BloodTest.STATE_VALID)
|
||||
.where("timestamp >= " + Math.max(startTime, 0))
|
||||
.where("timestamp <= " + endTime)
|
||||
.orderBy("timestamp asc") // warn asc!
|
||||
.limit(number)
|
||||
.execute();
|
||||
} catch (android.database.sqlite.SQLiteException e) {
|
||||
fixUpTable();
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
synchronized static void opportunisticCalibration() {
|
||||
if (Pref.getBooleanDefaultFalse("bluetooth_meter_for_calibrations_auto")) {
|
||||
final BloodTest bt = lastValid();
|
||||
if (bt == null) {
|
||||
Log.d(TAG, "opportunistic: No blood tests");
|
||||
return;
|
||||
}
|
||||
if (JoH.msSince(bt.timestamp) > (Constants.HOUR_IN_MS * 8)) {
|
||||
Log.d(TAG, "opportunistic: Blood test older than 8 hours ago");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((bt.uuid == null) || (bt.uuid.length() < 8)) {
|
||||
Log.d(TAG, "opportunisitic: invalid uuid");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((bt.uuid != null) && (bt.uuid.length() > 1) && PersistentStore.getString(LAST_BT_AUTO_CALIB_UUID).equals(bt.uuid)) {
|
||||
Log.d(TAG, "opportunistic: Already processed uuid: " + bt.uuid);
|
||||
return;
|
||||
}
|
||||
|
||||
final Calibration calibration = Calibration.lastValid();
|
||||
if (calibration == null) {
|
||||
Log.d(TAG, "opportunistic: No calibrations");
|
||||
// TODO do we try to initial calibrate using this?
|
||||
return;
|
||||
}
|
||||
|
||||
if (JoH.msSince(calibration.timestamp) < Constants.HOUR_IN_MS) {
|
||||
Log.d(TAG, "opportunistic: Last calibration less than 1 hour ago");
|
||||
return;
|
||||
}
|
||||
|
||||
if (bt.timestamp <= calibration.timestamp) {
|
||||
Log.d(TAG, "opportunistic: Blood test isn't more recent than last calibration");
|
||||
return;
|
||||
}
|
||||
|
||||
// get closest bgreading - must be within dexcom period and locked to sensor
|
||||
final BgReading bgReading = BgReading.getForPreciseTimestamp(bt.timestamp + (AddCalibration.estimatedInterstitialLagSeconds * 1000), BgGraphBuilder.DEXCOM_PERIOD);
|
||||
if (bgReading == null) {
|
||||
Log.d(TAG, "opportunistic: No matching bg reading");
|
||||
return;
|
||||
}
|
||||
|
||||
if (bt.timestamp > highest_timestamp) {
|
||||
Accuracy.create(bt, bgReading, "xDrip Original");
|
||||
final CalibrationAbstract plugin = PluggableCalibration.getCalibrationPluginFromPreferences();
|
||||
final CalibrationAbstract.CalibrationData cd = (plugin != null) ? plugin.getCalibrationData(bgReading.timestamp) : null;
|
||||
if (plugin != null) {
|
||||
BgReading pluginBgReading = plugin.getBgReadingFromBgReading(bgReading, cd);
|
||||
Accuracy.create(bt, pluginBgReading, plugin.getAlgorithmName());
|
||||
}
|
||||
highest_timestamp = bt.timestamp;
|
||||
}
|
||||
|
||||
if (!CalibrationRequest.isSlopeFlatEnough(bgReading)) {
|
||||
Log.d(TAG, "opportunistic: Slope is not flat enough at: " + JoH.dateTimeText(bgReading.timestamp));
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO store evaluation failure for this record in cache for future optimization
|
||||
|
||||
// TODO Check we have prior reading as well perhaps
|
||||
JoH.clearCache();
|
||||
UserError.Log.ueh(TAG, "Opportunistic calibration for Blood Test at " + JoH.dateTimeText(bt.timestamp) + " of " + BgGraphBuilder.unitized_string_with_units_static(bt.mgdl) + " matching sensor slope at: " + JoH.dateTimeText(bgReading.timestamp) + " from source " + bt.source);
|
||||
final long time_since = JoH.msSince(bt.timestamp);
|
||||
|
||||
|
||||
Log.d(TAG, "opportunistic: attempting auto calibration");
|
||||
PersistentStore.setString(LAST_BT_AUTO_CALIB_UUID, bt.uuid);
|
||||
Home.startHomeWithExtra(xdrip.getAppContext(),
|
||||
Home.BLUETOOTH_METER_CALIBRATION,
|
||||
BgGraphBuilder.unitized_string_static(bt.mgdl),
|
||||
Long.toString(time_since),
|
||||
"auto");
|
||||
}
|
||||
}
|
||||
|
||||
public static String evaluateAccuracy(long period) {
|
||||
|
||||
// CACHE??
|
||||
|
||||
final List<BloodTest> bloodTests = latestForGraph(1000, JoH.tsl() - period, JoH.tsl() - AddCalibration.estimatedInterstitialLagSeconds);
|
||||
final List<Double> difference = new ArrayList<>();
|
||||
final List<Double> plugin_difference = new ArrayList<>();
|
||||
if ((bloodTests == null) || (bloodTests.size() == 0)) return null;
|
||||
|
||||
final boolean show_plugin = true;
|
||||
final CalibrationAbstract plugin = (show_plugin) ? PluggableCalibration.getCalibrationPluginFromPreferences() : null;
|
||||
|
||||
|
||||
for (BloodTest bt : bloodTests) {
|
||||
final BgReading bgReading = BgReading.getForPreciseTimestamp(bt.timestamp + (AddCalibration.estimatedInterstitialLagSeconds * 1000), BgGraphBuilder.DEXCOM_PERIOD);
|
||||
|
||||
if (bgReading != null) {
|
||||
final Calibration calibration = bgReading.calibration;
|
||||
if (calibration == null) {
|
||||
Log.d(TAG, "Calibration for bgReading is null! @ " + JoH.dateTimeText(bgReading.timestamp));
|
||||
continue;
|
||||
}
|
||||
final double diff = Math.abs(bgReading.calculated_value - bt.mgdl);
|
||||
difference.add(diff);
|
||||
if (d) {
|
||||
Log.d(TAG, "Evaluate Accuracy: difference: " + JoH.qs(diff));
|
||||
}
|
||||
final CalibrationAbstract.CalibrationData cd = (plugin != null) ? plugin.getCalibrationData(bgReading.timestamp) : null;
|
||||
if ((plugin != null) && (cd != null)) {
|
||||
final double plugin_diff = Math.abs(bt.mgdl - plugin.getGlucoseFromBgReading(bgReading, cd));
|
||||
plugin_difference.add(plugin_diff);
|
||||
if (d)
|
||||
Log.d(TAG, "Evaluate Plugin Accuracy: " + BgGraphBuilder.unitized_string_with_units_static(bt.mgdl) + " @ " + JoH.dateTimeText(bt.timestamp) + " difference: " + JoH.qs(plugin_diff) + "/" + JoH.qs(plugin_diff * Constants.MGDL_TO_MMOLL, 2) + " calibration: " + JoH.qs(cd.slope, 2) + " " + JoH.qs(cd.intercept, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (difference.size() == 0) return null;
|
||||
double avg = DoubleMath.mean(difference);
|
||||
Log.d(TAG, "Average accuracy: " + accuracyAsString(avg) + " (" + JoH.qs(avg, 5) + ")");
|
||||
|
||||
if (plugin_difference.size() > 0) {
|
||||
double plugin_avg = DoubleMath.mean(plugin_difference);
|
||||
Log.d(TAG, "Plugin Average accuracy: " + accuracyAsString(plugin_avg) + " (" + JoH.qs(plugin_avg, 5) + ")");
|
||||
return accuracyAsString(plugin_avg) + " / " + accuracyAsString(avg);
|
||||
}
|
||||
return accuracyAsString(avg);
|
||||
}
|
||||
|
||||
public static String accuracyAsString(double avg) {
|
||||
final boolean domgdl = Pref.getString("units", "mgdl").equals("mgdl");
|
||||
// +- symbol
|
||||
return "\u00B1" + (!domgdl ? JoH.qs(avg * Constants.MGDL_TO_MMOLL, 2) + " mmol" : JoH.qs(avg, 1) + " mgdl");
|
||||
}
|
||||
|
||||
public static List<BloodTest> cleanup(int retention_days) {
|
||||
return new Delete()
|
||||
.from(BloodTest.class)
|
||||
.where("timestamp < ?", JoH.tsl() - (retention_days * Constants.DAY_IN_MS))
|
||||
.execute();
|
||||
}
|
||||
|
||||
// create the table ourselves without worrying about model versioning and downgrading
|
||||
private static void fixUpTable() {
|
||||
if (patched) return;
|
||||
final String[] patchup = {
|
||||
"CREATE TABLE BloodTest (_id INTEGER PRIMARY KEY AUTOINCREMENT);",
|
||||
"ALTER TABLE BloodTest ADD COLUMN timestamp INTEGER;",
|
||||
"ALTER TABLE BloodTest ADD COLUMN created_timestamp INTEGER;",
|
||||
"ALTER TABLE BloodTest ADD COLUMN state INTEGER;",
|
||||
"ALTER TABLE BloodTest ADD COLUMN mgdl REAL;",
|
||||
"ALTER TABLE BloodTest ADD COLUMN source TEXT;",
|
||||
"ALTER TABLE BloodTest ADD COLUMN uuid TEXT;",
|
||||
"CREATE UNIQUE INDEX index_Bloodtest_uuid on BloodTest(uuid);",
|
||||
"CREATE UNIQUE INDEX index_Bloodtest_timestamp on BloodTest(timestamp);",
|
||||
"CREATE INDEX index_Bloodtest_created_timestamp on BloodTest(created_timestamp);",
|
||||
"CREATE INDEX index_Bloodtest_state on BloodTest(state);"};
|
||||
|
||||
for (String patch : patchup) {
|
||||
try {
|
||||
SQLiteUtils.execSql(patch);
|
||||
// UserError.Log.e(TAG, "Processed patch should not have succeeded!!: " + patch);
|
||||
} catch (Exception e) {
|
||||
// UserError.Log.d(TAG, "Patch: " + patch + " generated exception as it should: " + e.toString());
|
||||
}
|
||||
}
|
||||
patched = true;
|
||||
}
|
||||
}
|
||||
|
||||
173
lib/nightscout/com/eveningoutpost/dexdrip/Models/Bubble.java
Executable file
173
lib/nightscout/com/eveningoutpost/dexdrip/Models/Bubble.java
Executable file
@@ -0,0 +1,173 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.util.HexDump;
|
||||
import com.eveningoutpost.dexdrip.Models.UserError.Log;
|
||||
import com.eveningoutpost.dexdrip.NFCReaderX;
|
||||
import com.eveningoutpost.dexdrip.R;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.BridgeResponse;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.LibreUtils;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static com.eveningoutpost.dexdrip.xdrip.gs;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
||||
public class Bubble {
|
||||
private static final String TAG = "Bubble";//?????"Bubble";
|
||||
|
||||
private static volatile byte[] s_full_data = null;
|
||||
private static volatile int s_acumulatedSize = 0;
|
||||
|
||||
public static boolean isBubble() {
|
||||
final ActiveBluetoothDevice activeBluetoothDevice = ActiveBluetoothDevice.first();
|
||||
if (activeBluetoothDevice == null || activeBluetoothDevice.name == null) {
|
||||
return false;
|
||||
}
|
||||
return activeBluetoothDevice.name.contentEquals("Bubble");
|
||||
}
|
||||
|
||||
|
||||
public static BridgeResponse getBubbleResponse() {
|
||||
final BridgeResponse reply = new BridgeResponse();
|
||||
ByteBuffer ackMessage = ByteBuffer.allocate(6);
|
||||
ackMessage.put(0, (byte) 0x02);
|
||||
ackMessage.put(1, (byte) 0x01);
|
||||
ackMessage.put(2, (byte) 0x00);
|
||||
ackMessage.put(3, (byte) 0x00);
|
||||
ackMessage.put(4, (byte) 0x00);
|
||||
ackMessage.put(5, (byte) 0x2B);
|
||||
reply.add(ackMessage);
|
||||
return reply;
|
||||
}
|
||||
|
||||
public static int lens = 344;
|
||||
public static int BUBBLE_FOOTER = 8;
|
||||
|
||||
static int errorCount = 0;
|
||||
|
||||
|
||||
static byte[] patchUid = null;
|
||||
static byte[] patchInfo = null;
|
||||
|
||||
public static BridgeResponse decodeBubblePacket(byte[] buffer, int len) {
|
||||
final BridgeResponse reply = new BridgeResponse();
|
||||
int first = 0xff & buffer[0];
|
||||
if (first == 0x80) {
|
||||
PersistentStore.setString("Bubblebattery", Integer.toString(buffer[4]));
|
||||
Pref.setInt("bridge_battery", buffer[4]);
|
||||
String bubblefirmware = buffer[2] + "." + buffer[3];
|
||||
String bubbleHArdware = buffer[buffer.length-2] + "." + buffer[buffer.length-1];
|
||||
PersistentStore.setString("BubbleHArdware", bubbleHArdware);
|
||||
PersistentStore.setString("BubbleFirmware", bubblefirmware);
|
||||
ByteBuffer ackMessage = ByteBuffer.allocate(6);
|
||||
ackMessage.put(0, (byte) 0x02);
|
||||
ackMessage.put(1, (byte) 0x01);
|
||||
ackMessage.put(2, (byte) 0x00);
|
||||
ackMessage.put(3, (byte) 0x00);
|
||||
ackMessage.put(4, (byte) 0x00);
|
||||
ackMessage.put(5, (byte) 0x2B);
|
||||
reply.add(ackMessage);
|
||||
s_full_data = null;
|
||||
return getBubbleResponse();
|
||||
}
|
||||
if (first == 0xC0) {
|
||||
patchUid = Arrays.copyOfRange(buffer, 2, 10);
|
||||
String SensorSn = LibreUtils.decodeSerialNumberKey(patchUid);
|
||||
PersistentStore.setString("LibreSN", SensorSn);
|
||||
return reply;
|
||||
}
|
||||
if (first == 0xC1) {
|
||||
double fv = JoH.tolerantParseDouble(PersistentStore.getString("BubbleFirmware"));
|
||||
if (fv < 1.35) {
|
||||
patchInfo = Arrays.copyOfRange(buffer, 3, 9);
|
||||
} else {
|
||||
if (buffer.length >= 11) {
|
||||
patchInfo = Arrays.copyOfRange(buffer, 5, 11);
|
||||
}
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
if (first == 0x82) {
|
||||
int expectedSize = lens + BUBBLE_FOOTER;
|
||||
if (s_full_data == null) {
|
||||
InitBuffer(expectedSize);
|
||||
}
|
||||
addData(buffer);
|
||||
return reply;
|
||||
|
||||
}
|
||||
|
||||
if (first == 0xBF) {
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
Log.e(TAG, "No sensor has been found");
|
||||
reply.setError_message(gs(R.string.no_sensor_found));
|
||||
s_full_data = null;
|
||||
errorCount++;
|
||||
if (errorCount <= 2) {
|
||||
return getBubbleResponse();
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
|
||||
static void addData(byte[] buffer) {
|
||||
System.arraycopy(buffer, 4, s_full_data, s_acumulatedSize, buffer.length-4);
|
||||
s_acumulatedSize = s_acumulatedSize + buffer.length - 4;
|
||||
AreWeDone();
|
||||
}
|
||||
|
||||
|
||||
static void AreWeDone() {
|
||||
if (s_acumulatedSize < lens) {
|
||||
return;
|
||||
}
|
||||
long now = JoH.tsl();
|
||||
String SensorSn = PersistentStore.getString("LibreSN");
|
||||
|
||||
|
||||
byte[] data = Arrays.copyOfRange(s_full_data, 0, 344);
|
||||
|
||||
boolean checksum_ok = NFCReaderX.HandleGoodReading(SensorSn, data, now, true, patchUid, patchInfo);
|
||||
int expectedSize = lens + BUBBLE_FOOTER;
|
||||
InitBuffer(expectedSize);
|
||||
errorCount = 0;
|
||||
Log.e(TAG, "We have all the data that we need " + s_acumulatedSize + " checksum_ok = " + checksum_ok + HexDump.dumpHexString(data));
|
||||
|
||||
}
|
||||
|
||||
|
||||
static void InitBuffer(int expectedSize) {
|
||||
s_full_data = new byte[expectedSize];
|
||||
s_acumulatedSize = 0;
|
||||
}
|
||||
|
||||
public static ArrayList<ByteBuffer> initialize() {
|
||||
Log.e(TAG, "initialize!");
|
||||
Pref.setInt("bridge_battery", 0); //force battery to no-value before first reading
|
||||
return resetBubbleState();
|
||||
}
|
||||
|
||||
private static ArrayList<ByteBuffer> resetBubbleState() {
|
||||
ArrayList<ByteBuffer> ret = new ArrayList<>();
|
||||
|
||||
// Make Bubble send data every 5 minutes
|
||||
ByteBuffer ackMessage = ByteBuffer.allocate(3);
|
||||
ackMessage.put(0, (byte) 0x00);
|
||||
ackMessage.put(1, (byte) 0x01);
|
||||
ackMessage.put(2, (byte) 0x05);
|
||||
ret.add(ackMessage);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
1414
lib/nightscout/com/eveningoutpost/dexdrip/Models/Calibration.java
Normal file
1414
lib/nightscout/com/eveningoutpost/dexdrip/Models/Calibration.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.activeandroid.Model;
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Select;
|
||||
import com.eveningoutpost.dexdrip.Home;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by Emma Black on 12/9/14.
|
||||
*/
|
||||
|
||||
@Table(name = "CalibrationRequest", id = BaseColumns._ID)
|
||||
public class CalibrationRequest extends Model {
|
||||
private static final int max = 250;
|
||||
private static final int min = 70;
|
||||
private static final String TAG = CalibrationRequest.class.getSimpleName();
|
||||
|
||||
@Column(name = "requestIfAbove")
|
||||
public double requestIfAbove;
|
||||
|
||||
@Column(name = "requestIfBelow")
|
||||
public double requestIfBelow;
|
||||
|
||||
public static void createRange(double low, double high) {
|
||||
CalibrationRequest calibrationRequest = new CalibrationRequest();
|
||||
calibrationRequest.requestIfAbove = low;
|
||||
calibrationRequest.requestIfBelow = high;
|
||||
calibrationRequest.save();
|
||||
}
|
||||
static void createOffset(double center, double distance) {
|
||||
CalibrationRequest calibrationRequest = new CalibrationRequest();
|
||||
calibrationRequest.requestIfAbove = center + distance;
|
||||
calibrationRequest.requestIfBelow = max;
|
||||
calibrationRequest.save();
|
||||
|
||||
calibrationRequest = new CalibrationRequest();
|
||||
calibrationRequest.requestIfAbove = min;
|
||||
calibrationRequest.requestIfBelow = center - distance;
|
||||
calibrationRequest.save();
|
||||
}
|
||||
|
||||
static void clearAll(){
|
||||
List<CalibrationRequest> calibrationRequests = new Select()
|
||||
.from(CalibrationRequest.class)
|
||||
.execute();
|
||||
if (calibrationRequests.size() >=1) {
|
||||
for (CalibrationRequest calibrationRequest : calibrationRequests) {
|
||||
calibrationRequest.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean shouldRequestCalibration(BgReading bgReading) {
|
||||
CalibrationRequest calibrationRequest = new Select()
|
||||
.from(CalibrationRequest.class)
|
||||
.where("requestIfAbove < ?", bgReading.calculated_value)
|
||||
.where("requestIfBelow > ?", bgReading.calculated_value)
|
||||
.executeSingle();
|
||||
return (calibrationRequest != null && isSlopeFlatEnough(bgReading, 1));
|
||||
}
|
||||
|
||||
public static boolean isSlopeFlatEnough() {
|
||||
BgReading bgReading = BgReading.last(true);
|
||||
if (bgReading == null) return false;
|
||||
if (JoH.msSince(bgReading.timestamp) > Home.stale_data_millis()) {
|
||||
UserError.Log.d(TAG, "Slope cannot be flat enough as data is stale");
|
||||
return false;
|
||||
}
|
||||
// TODO check if stale, check previous slope also, check that reading parameters also
|
||||
return isSlopeFlatEnough(bgReading);
|
||||
}
|
||||
|
||||
public static boolean isSlopeFlatEnough(BgReading bgReading) {
|
||||
return isSlopeFlatEnough(bgReading, 1);
|
||||
}
|
||||
|
||||
public static boolean isSlopeFlatEnough(BgReading bgReading, double limit) {
|
||||
if (bgReading == null) return false;
|
||||
// TODO use BestGlucose
|
||||
return Math.abs(bgReading.calculated_value_slope * 60000) < limit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
/**
|
||||
* Created by jamorham on 04/01/16.
|
||||
*/
|
||||
public class CobCalc {
|
||||
double initialCarbs;
|
||||
double decayedBy;
|
||||
double isDecaying;
|
||||
double carbTime;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
// from package info.nightscout.client.utils;
|
||||
|
||||
/**
|
||||
* Created by mike on 30.12.2015.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The Class DateUtil. A simple wrapper around SimpleDateFormat to ease the handling of iso date string <-> date obj
|
||||
* with TZ
|
||||
*/
|
||||
public class DateUtil {
|
||||
|
||||
private static final String FORMAT_DATE_ISO = "yyyy-MM-dd'T'HH:mm:ss'Z'"; // eg 2017-03-24T22:03:27Z
|
||||
private static final String FORMAT_DATE_ISO2 = "yyyy-MM-dd'T'HH:mm:ssZ"; // eg 2017-03-27T17:38:14+0300
|
||||
private static final String FORMAT_DATE_ISO3 = "yyyy-MM-dd'T'HH:mmZ"; // eg 2017-05-12T08:16-0400
|
||||
|
||||
/**
|
||||
* Takes in an ISO date string of the following format:
|
||||
* yyyy-mm-ddThh:mm:ss.ms+HoMo
|
||||
*
|
||||
* @param isoDateString the iso date string
|
||||
* @return the date
|
||||
* @throws Exception the exception
|
||||
*/
|
||||
private static Date fromISODateString(String isoDateString)
|
||||
throws Exception {
|
||||
SimpleDateFormat f = new SimpleDateFormat(FORMAT_DATE_ISO);
|
||||
f.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
return f.parse(isoDateString);
|
||||
}
|
||||
|
||||
private static Date fromISODateString3(String isoDateString)
|
||||
throws Exception {
|
||||
SimpleDateFormat f = new SimpleDateFormat(FORMAT_DATE_ISO3);
|
||||
f.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
return f.parse(isoDateString);
|
||||
}
|
||||
|
||||
private static Date fromISODateString2(String isoDateString)
|
||||
throws Exception {
|
||||
try {
|
||||
SimpleDateFormat f = new SimpleDateFormat(FORMAT_DATE_ISO2);
|
||||
f.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
return f.parse(isoDateString);
|
||||
} catch (java.text.ParseException e) {
|
||||
return fromISODateString3(isoDateString);
|
||||
}
|
||||
}
|
||||
|
||||
public static Date tolerantFromISODateString(String isoDateString)
|
||||
throws Exception {
|
||||
try {
|
||||
return fromISODateString(isoDateString.replaceFirst("\\.[0-9][0-9][0-9]Z$", "Z"));
|
||||
} catch (java.text.ParseException e) {
|
||||
return fromISODateString2(isoDateString);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render date
|
||||
*
|
||||
* @param date the date obj
|
||||
* @param format - if not specified, will use FORMAT_DATE_ISO
|
||||
* @param tz - tz to set to, if not specified uses local timezone
|
||||
* @return the iso-formatted date string
|
||||
*/
|
||||
public static String toISOString(Date date, String format, TimeZone tz) {
|
||||
if (format == null) format = FORMAT_DATE_ISO;
|
||||
if (tz == null) tz = TimeZone.getDefault();
|
||||
DateFormat f = new SimpleDateFormat(format);
|
||||
f.setTimeZone(tz);
|
||||
return f.format(date);
|
||||
}
|
||||
|
||||
public static String toISOString(Date date) {
|
||||
return toISOString(date, FORMAT_DATE_ISO, TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
public static String toISOString(long date) {
|
||||
return toISOString(new Date(date), FORMAT_DATE_ISO, TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
public static String toNightscoutFormat(long date) {
|
||||
final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US);
|
||||
format.setTimeZone(TimeZone.getDefault());
|
||||
return format.format(date);
|
||||
}
|
||||
}
|
||||
574
lib/nightscout/com/eveningoutpost/dexdrip/Models/DesertSync.java
Normal file
574
lib/nightscout/com/eveningoutpost/dexdrip/Models/DesertSync.java
Normal file
@@ -0,0 +1,574 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Delete;
|
||||
import com.activeandroid.query.Select;
|
||||
import com.eveningoutpost.dexdrip.GcmActivity;
|
||||
import com.eveningoutpost.dexdrip.GcmListenerSvc;
|
||||
import com.eveningoutpost.dexdrip.Home;
|
||||
import com.eveningoutpost.dexdrip.JamListenerSvc;
|
||||
import com.eveningoutpost.dexdrip.R;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Inevitable;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.StatusItem;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.desertsync.DesertComms;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.desertsync.RouteTools;
|
||||
import com.eveningoutpost.dexdrip.utils.CipherUtils;
|
||||
import com.eveningoutpost.dexdrip.webservices.XdripWebService;
|
||||
import com.eveningoutpost.dexdrip.xdrip;
|
||||
import com.google.firebase.messaging.RemoteMessage;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import static com.eveningoutpost.dexdrip.GoogleDriveInterface.getDriveIdentityString;
|
||||
import static com.eveningoutpost.dexdrip.Models.JoH.emptyString;
|
||||
import static com.eveningoutpost.dexdrip.UtilityModels.desertsync.RouteTools.getBestInterfaceAddress;
|
||||
import static com.eveningoutpost.dexdrip.UtilityModels.desertsync.RouteTools.ip;
|
||||
|
||||
// created by jamorham 18/08/2018
|
||||
|
||||
// not to be confused with dessert sync, yum!
|
||||
|
||||
|
||||
@NoArgsConstructor
|
||||
@Table(name = "DesertSync", id = BaseColumns._ID)
|
||||
public class DesertSync extends PlusModel {
|
||||
|
||||
private static boolean patched = false;
|
||||
private static final String TAG = DesertSync.class.getSimpleName();
|
||||
public static final String NO_DATA_MARKER = "NO DATA";
|
||||
private static final String PREF_SENDER_UUID = "DesertSync-sender-uuid";
|
||||
private static final int MAX_CATCHUP = 20;
|
||||
private static final ReentrantLock sequence_lock = new ReentrantLock();
|
||||
private static final boolean d = false;
|
||||
private static volatile int duplicateIndicator = 0;
|
||||
private static volatile int catchupCounter = 0;
|
||||
private static String static_sender = null;
|
||||
private static RollCall myRollCall = null;
|
||||
private static JamListenerSvc service;
|
||||
private static HashMap<InetAddress, Long> peers;
|
||||
private static int spinner = 0;
|
||||
private static volatile String lastUsedIP = null;
|
||||
|
||||
private static volatile long highestPullTimeStamp = -1;
|
||||
|
||||
private static final String[] schema = {
|
||||
"CREATE TABLE DesertSync (_id INTEGER PRIMARY KEY AUTOINCREMENT);",
|
||||
"ALTER TABLE DesertSync ADD COLUMN timestamp INTEGER;",
|
||||
"ALTER TABLE DesertSync ADD COLUMN topic TEXT;",
|
||||
"ALTER TABLE DesertSync ADD COLUMN sender TEXT;",
|
||||
"ALTER TABLE DesertSync ADD COLUMN payload TEXT;",
|
||||
"ALTER TABLE DesertSync ADD COLUMN processed TEXT;",
|
||||
"CREATE UNIQUE INDEX index_DesertSync_timestamp on DesertSync(timestamp);",
|
||||
"CREATE INDEX index_DesertSync_payload on DesertSync(payload);",
|
||||
"CREATE INDEX index_DesertSync_processed on DesertSync(processed);",
|
||||
"CREATE INDEX index_DesertSync_topic on DesertSync(topic);"};
|
||||
|
||||
private static final int MAX_ITEMS = 50;
|
||||
|
||||
public static final String PREF_WEBSERVICE_SECRET = "xdrip_webservice_secret";
|
||||
|
||||
@Expose
|
||||
@Column(name = "timestamp", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE)
|
||||
public long timestamp;
|
||||
|
||||
@Expose
|
||||
@Column(name = "topic")
|
||||
public String topic;
|
||||
|
||||
@Expose
|
||||
@Column(name = "sender")
|
||||
public String sender;
|
||||
|
||||
@Expose
|
||||
@Column(name = "payload")
|
||||
public String payload;
|
||||
|
||||
@Column(name = "processed")
|
||||
private String processed;
|
||||
|
||||
|
||||
@Builder
|
||||
private DesertSync(final long timestamp, final String topic, final String sender, final String payload, final boolean processedFlag) {
|
||||
this.timestamp = timestamp;
|
||||
this.topic = topic;
|
||||
this.sender = sender;
|
||||
if (processedFlag) {
|
||||
this.processed = payload;
|
||||
} else {
|
||||
this.payload = payload;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<DesertSync> since(final long position, final String topic) {
|
||||
if (topic == null) {
|
||||
return new Select()
|
||||
.from(DesertSync.class)
|
||||
.where("timestamp > ?", position)
|
||||
.orderBy("timestamp asc")
|
||||
.limit(MAX_ITEMS)
|
||||
.execute();
|
||||
} else {
|
||||
return new Select()
|
||||
.from(DesertSync.class)
|
||||
.where("topic = ?", topic)
|
||||
.where("timestamp > ?", position)
|
||||
.orderBy("timestamp asc")
|
||||
.limit(MAX_ITEMS)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean alreadyInDatabase(final boolean processedFlag) {
|
||||
return new Select()
|
||||
.from(DesertSync.class)
|
||||
.where("topic = ?", topic)
|
||||
.where("processed = ?", processedFlag ? processed : processData())
|
||||
.executeSingle() != null;
|
||||
}
|
||||
|
||||
private static DesertSync last() {
|
||||
return new Select()
|
||||
.from(DesertSync.class)
|
||||
.where("topic = ?", getTopic())
|
||||
.orderBy("timestamp desc")
|
||||
.executeSingle();
|
||||
}
|
||||
|
||||
|
||||
public String toS() {
|
||||
return JoH.defaultGsonInstance().toJson(this);
|
||||
}
|
||||
|
||||
public String getAction() {
|
||||
return getPayload(0);
|
||||
}
|
||||
|
||||
public String getPayload() {
|
||||
return getPayload(1);
|
||||
}
|
||||
|
||||
private String processData() {
|
||||
if (processed == null) {
|
||||
processed = CipherUtils.decryptString(payload);
|
||||
}
|
||||
return processed;
|
||||
}
|
||||
|
||||
private String transmissionPayload() {
|
||||
if (payload == null) {
|
||||
payload = CipherUtils.compressEncryptString(processed);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
private String getPayload(int section) {
|
||||
if (processed == null) return "<null>";
|
||||
processData();
|
||||
try {
|
||||
final String[] ps = processed.split("\\^");
|
||||
return ps[section];
|
||||
} catch (Exception e) {
|
||||
return "<invalid payload>";
|
||||
}
|
||||
}
|
||||
|
||||
private RemoteMessage getMessage() {
|
||||
final HashMap<String, String> map = new HashMap<>();
|
||||
map.put("message", "From DesertSync");
|
||||
map.put("xfrom", sender);
|
||||
map.put("yfrom", getYfrom());
|
||||
map.put("datum", getPayload());
|
||||
map.put("action", getAction());
|
||||
return new RemoteMessage.Builder("internal").setData(map).build();
|
||||
}
|
||||
|
||||
// utility methods
|
||||
|
||||
public static String toJson(List<DesertSync> list) {
|
||||
return JoH.defaultGsonInstance().toJson(list);
|
||||
}
|
||||
|
||||
public static boolean isEnabled() {
|
||||
return Pref.getBooleanDefaultFalse("desert_sync_enabled");
|
||||
}
|
||||
|
||||
// input / output
|
||||
|
||||
public static void pullAsEnabled() {
|
||||
if (Home.get_follower()) {
|
||||
if (isEnabled()) {
|
||||
// TODO check if no data received? or maybe we don't - should this instead be called from do nothing service??
|
||||
DesertComms.pullFromOasis(getTopic(), getHighestPullTimeStamp());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized static long getHighestPullTimeStamp() {
|
||||
if (highestPullTimeStamp == -1) {
|
||||
try {
|
||||
highestPullTimeStamp = last().timestamp;
|
||||
} catch (NullPointerException e) {
|
||||
highestPullTimeStamp = 1;
|
||||
}
|
||||
}
|
||||
return highestPullTimeStamp;
|
||||
}
|
||||
|
||||
private static DesertSync createFromBundle(final Bundle data) {
|
||||
final String payload = data.getString("payload", data.getString("datum", ""));
|
||||
if (payload.length() > 0) {
|
||||
return new DesertSync(JoH.tsl(), data.getString("identity", getTopic()), mySender(), data.getString("action") + "^" + payload, true);
|
||||
} else {
|
||||
UserError.Log.d(TAG, "Invalid bundle");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean fromGCM(final Bundle data) {
|
||||
if (isEnabled()) {
|
||||
final DesertSync ds = createFromBundle(data);
|
||||
if (ds != null && !ds.alreadyInDatabase(true)) {
|
||||
DesertComms.pushToOasis(ds.topic, ds.sender, ds.transmissionPayload());
|
||||
ds.save();
|
||||
} else {
|
||||
UserError.Log.d(TAG, "Not pushing entry without payload / duplicate");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean fromPush(String topic, String sender, String payload) {
|
||||
if (isEnabled()) {
|
||||
|
||||
UserError.Log.d(TAG, String.format("sender: %s, topic: %s, payload: %s", sender, topic, payload));
|
||||
if (sender == null || sender.length() != 32 || sender.equals(mySender())) return false;
|
||||
if (topic == null || topic.length() != 32) return false;
|
||||
if (payload == null || payload.length() == 0) return false;
|
||||
// TODO VALIDATE PARAMS
|
||||
|
||||
final DesertSync item = new DesertSync(JoH.tsl(), topic, sender, payload, false);
|
||||
processItem(item);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("NonAtomicOperationOnVolatileField")
|
||||
private static void processItem(final DesertSync item) {
|
||||
if (item != null) {
|
||||
if (item.topic != null && item.topic.equals(getTopic())) {
|
||||
if (!item.alreadyInDatabase(false)) {
|
||||
UserError.Log.d(TAG, "New item: " + item.payload);
|
||||
item.save();
|
||||
new Thread(() -> onMessageReceived(item.getMessage())).start();
|
||||
} else {
|
||||
duplicateIndicator++;
|
||||
UserError.Log.d(TAG, "Duplicate item: " + duplicateIndicator);
|
||||
}
|
||||
} else {
|
||||
UserError.Log.d(TAG, "Invalid topic");
|
||||
}
|
||||
} else {
|
||||
UserError.Log.d(TAG, "processItem NULL");
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("NonAtomicOperationOnVolatileField")
|
||||
public static void fromPull(final String json) {
|
||||
if (!json.startsWith(NO_DATA_MARKER)) {
|
||||
try {
|
||||
final List<DesertSync> items = JoH.defaultGsonInstance().fromJson(json, new TypeToken<ArrayList<DesertSync>>() {
|
||||
}.getType());
|
||||
if (items != null) {
|
||||
duplicateIndicator = 0;
|
||||
for (final DesertSync item : items) {
|
||||
if (item.timestamp > highestPullTimeStamp) {
|
||||
highestPullTimeStamp = item.timestamp;
|
||||
Inevitable.task("desert-sync-timestamp", 500, () -> {
|
||||
UserError.Log.d(TAG, "Synced up till: " + JoH.dateTimeText(highestPullTimeStamp));
|
||||
});
|
||||
}
|
||||
processItem(item);
|
||||
}
|
||||
if (items.size() == MAX_ITEMS) {
|
||||
UserError.Log.d(TAG, "Attempting to catch up as all history is duplicates or max size: " + catchupCounter);
|
||||
if (catchupCounter < MAX_CATCHUP) {
|
||||
catchupCounter++;
|
||||
Inevitable.task("Desert catchup", 6000, DesertSync::pullAsEnabled);
|
||||
}
|
||||
} else {
|
||||
catchupCounter = 0;
|
||||
}
|
||||
}
|
||||
} catch (JsonSyntaxException e) {
|
||||
UserError.Log.e(TAG, "fromPull error: " + e + "\n" + json);
|
||||
}
|
||||
} else {
|
||||
UserError.Log.d(TAG, "Web service reported no data matching our query - either we are synced or other mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
public static void pullFailed(final String host) {
|
||||
UserError.Log.d(TAG, "Pull failed: host: " + host);
|
||||
if (host == null) return;
|
||||
final String hint = RollCall.getBestMasterHintIP();
|
||||
UserError.Log.d(TAG, "Best hint: " + hint);
|
||||
if (hint == null) return;
|
||||
if (host.equals(hint)) {
|
||||
UserError.Log.d(TAG, "Looking for hint but master is still the same: " + hint);
|
||||
final String backupIP = DesertComms.getOasisBackupIP();
|
||||
if (!emptyString(backupIP) && !backupIP.equals(host)) {
|
||||
UserError.Log.d(TAG, "Trying backup: " + backupIP);
|
||||
takeMasterHint(backupIP);
|
||||
}
|
||||
} else {
|
||||
UserError.Log.d(TAG, "Got master hint for: " + hint);
|
||||
takeMasterHint(hint);
|
||||
}
|
||||
}
|
||||
|
||||
private static void takeMasterHint(String hint) {
|
||||
if (RouteTools.reachable(hint)) {
|
||||
UserError.Log.d(TAG, "Master hint of: " + hint + " is reachable - setting up probe");
|
||||
DesertComms.probeOasis(getTopic(), hint);
|
||||
}
|
||||
}
|
||||
|
||||
// identity
|
||||
|
||||
private static final String PREF_LAST_DESERT_MY_IP = "last-desert-sync-my-ip";
|
||||
|
||||
public static void checkIpChange(final String result) {
|
||||
// failed to reach peer
|
||||
UserError.Log.d(TAG, "CheckIpChange enter: " + result);
|
||||
if (result == null || (JoH.ratelimit("desert-check-ip-change", 60))) {
|
||||
final String currentIP = getBestInterfaceAddress();
|
||||
UserError.Log.d(TAG, "check ip change: current: " + currentIP);
|
||||
|
||||
if (!emptyString(currentIP)) {
|
||||
if (lastUsedIP == null) {
|
||||
lastUsedIP = PersistentStore.getString(PREF_LAST_DESERT_MY_IP);
|
||||
}
|
||||
UserError.Log.d(TAG, "check ip change last: " + lastUsedIP);
|
||||
if (emptyString(lastUsedIP) || !currentIP.equals(lastUsedIP)) {
|
||||
if (!emptyString(lastUsedIP)) {
|
||||
UserError.Log.uel(TAG, "Our IP appears to have changed from: " + lastUsedIP + " to " + currentIP + " sending notification to peers");
|
||||
UserError.Log.d(TAG, "check ip change send ping");
|
||||
GcmActivity.desertPing();
|
||||
}
|
||||
lastUsedIP = currentIP;
|
||||
|
||||
PersistentStore.setString(PREF_LAST_DESERT_MY_IP, lastUsedIP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String getTopic() {
|
||||
return getDriveIdentityString();
|
||||
}
|
||||
|
||||
public static void masterIdReply(final String result, final String host) {
|
||||
if (result == null) return;
|
||||
if (Home.get_follower()) {
|
||||
final RollCall rc = RollCall.fromJson(result);
|
||||
if (rc == null) return;
|
||||
if (rc.role.equals("Master")) {
|
||||
DesertComms.setOasisIP(host);
|
||||
pullAsEnabled();
|
||||
}
|
||||
} else {
|
||||
UserError.Log.e(TAG, "Refusing to process id reply as we are not a follower");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public static String mySender() {
|
||||
if (static_sender == null) {
|
||||
synchronized (DesertSync.class) {
|
||||
if (static_sender == null) {
|
||||
String sender = PersistentStore.getString(PREF_SENDER_UUID);
|
||||
//UserError.Log.d(TAG, "From store: " + sender);
|
||||
if (sender.length() != 32) {
|
||||
sender = CipherUtils.getRandomHexKey();
|
||||
UserError.Log.d(TAG, "From key: " + sender);
|
||||
PersistentStore.setString(PREF_SENDER_UUID, sender);
|
||||
}
|
||||
static_sender = sender;
|
||||
}
|
||||
}
|
||||
}
|
||||
UserError.Log.d(TAG, "Returning sender: " + static_sender);
|
||||
return static_sender;
|
||||
}
|
||||
|
||||
public static String getMyRollCall(final String topic) {
|
||||
if (topic != null && topic.equals(getTopic())) {
|
||||
if (myRollCall == null || JoH.msSince(myRollCall.created) > Constants.MINUTE_IN_MS * 15) {
|
||||
myRollCall = new RollCall();
|
||||
}
|
||||
return myRollCall.populate().toS();
|
||||
} else {
|
||||
return "Invalid topic";
|
||||
}
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
private static JamListenerSvc getInstance() {
|
||||
if (service == null) {
|
||||
service = new GcmListenerSvc();
|
||||
service.setInjectable();
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
private static void onMessageReceived(final RemoteMessage message) {
|
||||
if (sequence_lock.getQueueLength() > 0) {
|
||||
UserError.Log.d(TAG, "Sequence lock has: " + sequence_lock.getQueueLength() + " waiting");
|
||||
}
|
||||
try {
|
||||
sequence_lock.tryLock(20, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
//
|
||||
} finally {
|
||||
getInstance().onMessageReceived(message);
|
||||
try {
|
||||
sequence_lock.unlock();
|
||||
} catch (IllegalMonitorStateException e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void learnPeer(final InetAddress address) {
|
||||
if (peers == null) {
|
||||
peers = new HashMap<>();
|
||||
}
|
||||
if (!peers.containsKey(address)) {
|
||||
if (RouteTools.isLocal(address)) {
|
||||
UserError.Log.d(TAG, "Learned new peer: " + ip(address));
|
||||
} else {
|
||||
UserError.Log.d(TAG, "Refusing to Learn new peer: " + ip(address));
|
||||
return;
|
||||
}
|
||||
}
|
||||
peers.put(address, JoH.tsl());
|
||||
|
||||
spinner++;
|
||||
if (spinner % 10 == 0) {
|
||||
prunePeers();
|
||||
}
|
||||
}
|
||||
|
||||
private static void prunePeers() {
|
||||
InetAddress toRemove = null;
|
||||
for (final Map.Entry<InetAddress, Long> entry : peers.entrySet()) {
|
||||
if (JoH.msSince(entry.getValue()) > Constants.DAY_IN_MS * 3) {
|
||||
toRemove = entry.getKey();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (toRemove != null) peers.remove(toRemove);
|
||||
}
|
||||
|
||||
public static List<String> getActivePeers() {
|
||||
final List<String> list = new ArrayList<>();
|
||||
if (peers != null) {
|
||||
for (final Map.Entry<InetAddress, Long> entry : peers.entrySet()) {
|
||||
if (JoH.msSince(entry.getValue()) < Constants.HOUR_IN_MS * 3) {
|
||||
list.add(ip(entry.getKey()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public static String getActivePeersString() {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
for (final String str : getActivePeers()) {
|
||||
sb.append(str);
|
||||
sb.append(",");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String getYfrom() {
|
||||
return xdrip.gs(R.string.gcmtpc) + topic;
|
||||
}
|
||||
|
||||
public static void settingsChanged() {
|
||||
if (isEnabled()) {
|
||||
correctWebServiceSettings();
|
||||
}
|
||||
}
|
||||
|
||||
private static void correctWebServiceSettings() {
|
||||
Pref.setBoolean("xdrip_webservice", true);
|
||||
Pref.setBoolean("xdrip_webservice_open", true);
|
||||
if (Pref.getString(PREF_WEBSERVICE_SECRET, "").length() == 0) {
|
||||
Pref.setString(PREF_WEBSERVICE_SECRET, CipherUtils.getRandomHexKey());
|
||||
}
|
||||
Inevitable.task("web service changed", 2000, XdripWebService::settingsChanged);
|
||||
}
|
||||
|
||||
// maintenance
|
||||
|
||||
// create the table ourselves without worrying about model versioning and downgrading
|
||||
public static void updateDB() {
|
||||
patched = fixUpTable(schema, patched);
|
||||
}
|
||||
|
||||
public static void cleanup() {
|
||||
try {
|
||||
new Delete()
|
||||
.from(DesertSync.class)
|
||||
.where("timestamp < ?", JoH.tsl() - 86400000L)
|
||||
.execute();
|
||||
} catch (Exception e) {
|
||||
UserError.Log.d(TAG, "Exception cleaning uploader queue: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void deleteAll() {
|
||||
new Delete()
|
||||
.from(DesertSync.class)
|
||||
.execute();
|
||||
}
|
||||
|
||||
// megastatus
|
||||
|
||||
// data for MegaStatus
|
||||
public static List<StatusItem> megaStatus() {
|
||||
final List<StatusItem> l = new ArrayList<>();
|
||||
if (isEnabled()) {
|
||||
if (Home.get_follower()) {
|
||||
l.addAll(DesertComms.megaStatus());
|
||||
}
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
}
|
||||
159
lib/nightscout/com/eveningoutpost/dexdrip/Models/Forecast.java
Normal file
159
lib/nightscout/com/eveningoutpost/dexdrip/Models/Forecast.java
Normal file
@@ -0,0 +1,159 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.apache.commons.math3.linear.MatrixUtils;
|
||||
import org.apache.commons.math3.linear.RealMatrix;
|
||||
import org.apache.commons.math3.stat.regression.OLSMultipleLinearRegression;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Created by jamorham on 08/02/2016.
|
||||
*/
|
||||
public class Forecast {
|
||||
|
||||
private static final String TAG = "jamorham forecast";
|
||||
// from stackoverflow.com/questions/17592139/trend-lines-regression-curve-fitting-java-library
|
||||
|
||||
public interface TrendLine {
|
||||
void setValues(double[] y, double[] x); // y ~ f(x)
|
||||
|
||||
double predict(double x); // get a predicted y for a given x
|
||||
|
||||
double errorVarience();
|
||||
}
|
||||
|
||||
public abstract static class OLSTrendLine implements TrendLine {
|
||||
|
||||
RealMatrix coef = null; // will hold prediction coefs once we get values
|
||||
Double last_error_rate = null;
|
||||
|
||||
protected abstract double[] xVector(double x); // create vector of values from x
|
||||
|
||||
protected abstract boolean logY(); // set true to predict log of y (note: y must be positive)
|
||||
|
||||
|
||||
@Override
|
||||
public void setValues(double[] y, double[] x) {
|
||||
if (x.length != y.length) {
|
||||
throw new IllegalArgumentException(String.format("The numbers of y and x values must be equal (%d != %d)", y.length, x.length));
|
||||
}
|
||||
double[][] xData = new double[x.length][];
|
||||
for (int i = 0; i < x.length; i++) {
|
||||
// the implementation determines how to produce a vector of predictors from a single x
|
||||
xData[i] = xVector(x[i]);
|
||||
}
|
||||
if (logY()) { // in some models we are predicting ln y, so we replace each y with ln y
|
||||
y = Arrays.copyOf(y, y.length); // user might not be finished with the array we were given
|
||||
for (int i = 0; i < x.length; i++) {
|
||||
y[i] = Math.log(y[i]);
|
||||
}
|
||||
}
|
||||
final OLSMultipleLinearRegression ols = new OLSMultipleLinearRegression();
|
||||
ols.setNoIntercept(true); // let the implementation include a constant in xVector if desired
|
||||
ols.newSampleData(y, xData); // provide the data to the model
|
||||
coef = MatrixUtils.createColumnRealMatrix(ols.estimateRegressionParameters()); // get our coefs
|
||||
last_error_rate = ols.estimateErrorVariance();
|
||||
Log.d(TAG, getClass().getSimpleName() + " Forecast Error rate: errorvar:"
|
||||
+ JoH.qs(last_error_rate, 4)
|
||||
+ " regssionvar:" + JoH.qs(ols.estimateRegressandVariance(), 4)
|
||||
+ " stderror:" + JoH.qs(ols.estimateRegressionStandardError(), 4));
|
||||
}
|
||||
|
||||
@Override
|
||||
public double predict(double x) {
|
||||
double yhat = coef.preMultiply(xVector(x))[0]; // apply coefs to xVector
|
||||
if (logY()) yhat = (Math.exp(yhat)); // if we predicted ln y, we still need to get y
|
||||
return yhat;
|
||||
}
|
||||
|
||||
public static double[] toPrimitive(Double[] array) {
|
||||
if (array == null) {
|
||||
return null;
|
||||
} else if (array.length == 0) {
|
||||
return new double[0];
|
||||
}
|
||||
final double[] result = new double[array.length];
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
result[i] = array[i];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static double[] toPrimitiveFromList(Collection<Double> array) {
|
||||
if (array == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return toPrimitive(array.toArray(new Double[array.size()]));
|
||||
}
|
||||
|
||||
public double errorVarience() {
|
||||
return last_error_rate;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class PolyTrendLine extends OLSTrendLine {
|
||||
final int degree;
|
||||
|
||||
public PolyTrendLine(int degree) {
|
||||
if (degree < 0)
|
||||
throw new IllegalArgumentException("The degree of the polynomial must not be negative");
|
||||
this.degree = degree;
|
||||
}
|
||||
|
||||
protected double[] xVector(double x) { // {1, x, x*x, x*x*x, ...}
|
||||
double[] poly = new double[degree + 1];
|
||||
double xi = 1;
|
||||
for (int i = 0; i <= degree; i++) {
|
||||
poly[i] = xi;
|
||||
xi *= x;
|
||||
}
|
||||
return poly;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean logY() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ExpTrendLine extends OLSTrendLine {
|
||||
@Override
|
||||
protected double[] xVector(double x) {
|
||||
return new double[]{1, x};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean logY() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class PowerTrendLine extends OLSTrendLine {
|
||||
@Override
|
||||
protected double[] xVector(double x) {
|
||||
return new double[]{1, Math.log(x)};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean logY() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class LogTrendLine extends OLSTrendLine {
|
||||
@Override
|
||||
protected double[] xVector(double x) {
|
||||
return new double[]{1, Math.log(x)};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean logY() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
// class from LibreAlarm
|
||||
|
||||
public class GlucoseData implements Comparable<GlucoseData> {
|
||||
|
||||
public long realDate;
|
||||
public String sensorId;
|
||||
public long sensorTime;
|
||||
public int glucoseLevel = -1;
|
||||
public int glucoseLevelRaw = -1;
|
||||
public long phoneDatabaseId;
|
||||
public int glucoseLevelRawSmoothed;
|
||||
|
||||
public GlucoseData(){}
|
||||
|
||||
// jamorham added constructor
|
||||
public GlucoseData(int glucoseLevelRaw, long timestamp) {
|
||||
this.glucoseLevelRaw = glucoseLevelRaw;
|
||||
this.realDate = timestamp;
|
||||
}
|
||||
|
||||
public String glucose(boolean mmol) {
|
||||
return glucose(glucoseLevel, mmol);
|
||||
}
|
||||
|
||||
public static String glucose(int mgdl, boolean mmol) {
|
||||
return mmol ? new DecimalFormat("##.0").format(mgdl/18f) : String.valueOf(mgdl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(GlucoseData another) {
|
||||
return (int) (realDate - another.realDate);
|
||||
}
|
||||
}
|
||||
129
lib/nightscout/com/eveningoutpost/dexdrip/Models/HeartRate.java
Normal file
129
lib/nightscout/com/eveningoutpost/dexdrip/Models/HeartRate.java
Normal file
@@ -0,0 +1,129 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
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.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.annotations.Expose;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by jamorham on 01/11/2016.
|
||||
*/
|
||||
|
||||
|
||||
@Table(name = "HeartRate", id = BaseColumns._ID)
|
||||
public class HeartRate extends Model {
|
||||
|
||||
private final static String TAG = "HeartRate";
|
||||
private static boolean patched = false;
|
||||
@Expose
|
||||
@Column(name = "timestamp", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE)
|
||||
public long timestamp;
|
||||
|
||||
@Expose
|
||||
@Column(name = "bpm")
|
||||
public int bpm;
|
||||
|
||||
@Expose
|
||||
@Column(name = "accuracy")
|
||||
public int accuracy;
|
||||
|
||||
public static HeartRate last() {
|
||||
try {
|
||||
return new Select()
|
||||
.from(HeartRate.class)
|
||||
.orderBy("timestamp desc")
|
||||
.executeSingle();
|
||||
} catch (android.database.sqlite.SQLiteException e) {
|
||||
fixUpTable();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void create(long timestamp, int bpm, int accuracy) {
|
||||
final HeartRate hr = new HeartRate();
|
||||
hr.timestamp = timestamp;
|
||||
hr.bpm = bpm;
|
||||
hr.accuracy = accuracy;
|
||||
hr.saveit();
|
||||
}
|
||||
|
||||
public static List<HeartRate> latestForGraph(int number, double startTime) {
|
||||
return latestForGraph(number, (long) startTime, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static List<HeartRate> latestForGraph(int number, long startTime) {
|
||||
return latestForGraph(number, startTime, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
// TODO efficient record creation?
|
||||
|
||||
public static List<HeartRate> latestForGraph(int number, long startTime, long endTime) {
|
||||
try {
|
||||
return new Select()
|
||||
.from(HeartRate.class)
|
||||
.where("timestamp >= " + Math.max(startTime, 0))
|
||||
.where("timestamp <= " + endTime)
|
||||
.orderBy("timestamp asc") // warn asc!
|
||||
.limit(number)
|
||||
.execute();
|
||||
} catch (android.database.sqlite.SQLiteException e) {
|
||||
fixUpTable();
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
public static List<HeartRate> cleanup(int retention_days) {
|
||||
return new Delete()
|
||||
.from(HeartRate.class)
|
||||
.where("timestamp < ?", JoH.tsl() - (retention_days * 86400000L))
|
||||
.execute();
|
||||
}
|
||||
|
||||
// create the table ourselves without worrying about model versioning and downgrading
|
||||
private static void fixUpTable() {
|
||||
if (patched) return;
|
||||
String[] patchup = {
|
||||
"CREATE TABLE HeartRate (_id INTEGER PRIMARY KEY AUTOINCREMENT);",
|
||||
"ALTER TABLE HeartRate ADD COLUMN timestamp INTEGER;",
|
||||
"ALTER TABLE HeartRate ADD COLUMN bpm INTEGER;",
|
||||
"ALTER TABLE HeartRate ADD COLUMN accuracy INTEGER;",
|
||||
"CREATE UNIQUE INDEX index_HeartRate_timestamp on HeartRate(timestamp);"};
|
||||
|
||||
for (String patch : patchup) {
|
||||
try {
|
||||
SQLiteUtils.execSql(patch);
|
||||
// UserError.Log.e(TAG, "Processed patch should not have succeeded!!: " + patch);
|
||||
} catch (Exception e) {
|
||||
// UserError.Log.d(TAG, "Patch: " + patch + " generated exception as it should: " + e.toString());
|
||||
}
|
||||
}
|
||||
patched = true;
|
||||
}
|
||||
|
||||
// patches and saves
|
||||
public Long saveit() {
|
||||
fixUpTable();
|
||||
return save();
|
||||
}
|
||||
|
||||
// TODO cache gson statically
|
||||
public String toS() {
|
||||
final Gson gson = new GsonBuilder()
|
||||
.excludeFieldsWithoutExposeAnnotation()
|
||||
.create();
|
||||
return gson.toJson(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import com.eveningoutpost.dexdrip.insulin.Insulin;
|
||||
import com.eveningoutpost.dexdrip.insulin.InsulinManager;
|
||||
import com.google.gson.annotations.Expose;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public class InsulinInjection {
|
||||
private Insulin profile;
|
||||
|
||||
@Expose
|
||||
@Getter
|
||||
private double units;
|
||||
|
||||
@Expose
|
||||
@Getter
|
||||
private String insulin;
|
||||
|
||||
public InsulinInjection(final Insulin p, final double u) {
|
||||
profile = p;
|
||||
units = u;
|
||||
insulin = p.getName();
|
||||
}
|
||||
|
||||
|
||||
public Insulin getProfile() {
|
||||
// populate on demand
|
||||
if (profile == null) {
|
||||
profile = InsulinManager.getProfile(insulin);
|
||||
}
|
||||
return profile;
|
||||
}
|
||||
|
||||
// This is just a rough way to decide if it is a basal insulin without user needing to set it
|
||||
// question as to whether this should be here or call to encapsulated method in Insulin
|
||||
public boolean isBasal() {
|
||||
return getProfile().getMaxEffect() > 1000;
|
||||
}
|
||||
|
||||
}
|
||||
16
lib/nightscout/com/eveningoutpost/dexdrip/Models/Iob.java
Normal file
16
lib/nightscout/com/eveningoutpost/dexdrip/Models/Iob.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
/**
|
||||
* Created by jamorham on 02/01/16.
|
||||
*/
|
||||
public class Iob {
|
||||
public long timestamp;
|
||||
public double iob = 0;
|
||||
public CobCalc cobCalc;
|
||||
public double cob = 0;
|
||||
public double rawCarbImpact = 0;
|
||||
public double jCarbImpact = 0;
|
||||
public double jActivity = 0;
|
||||
}
|
||||
|
||||
|
||||
1690
lib/nightscout/com/eveningoutpost/dexdrip/Models/JoH.java
Normal file
1690
lib/nightscout/com/eveningoutpost/dexdrip/Models/JoH.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Select;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Table(name = "Libre2RawValue2", id = BaseColumns._ID)
|
||||
public class Libre2RawValue extends PlusModel {
|
||||
|
||||
static final String[] schema = {
|
||||
"DROP TABLE Libre2RawValue;",
|
||||
"CREATE TABLE Libre2RawValue2 (_id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER, serial STRING, glucose REAL);",
|
||||
"CREATE INDEX index_Libre2RawValue2_ts on Libre2RawValue2(ts);"
|
||||
};
|
||||
|
||||
@Column(name = "serial", index = true)
|
||||
public String serial;
|
||||
|
||||
@Column(name = "ts", index = true)
|
||||
public long timestamp;
|
||||
|
||||
@Column(name = "glucose", index = false)
|
||||
public double glucose;
|
||||
|
||||
public static List<Libre2RawValue> last20Minutes() {
|
||||
double timestamp = (new Date().getTime()) - (60000 * 20);
|
||||
return new Select()
|
||||
.from(Libre2RawValue.class)
|
||||
.where("ts >= " + timestamp)
|
||||
.orderBy("ts asc")
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static List<Libre2RawValue> latestForGraph(int number, double startTime) {
|
||||
return latestForGraph(number, (long) startTime, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static List<Libre2RawValue> latestForGraph(int number, long startTime) {
|
||||
return latestForGraph(number, startTime, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static List<Libre2RawValue> latestForGraph(int number, long startTime, long endTime) {
|
||||
return new Select()
|
||||
.from(Libre2RawValue.class)
|
||||
.where("ts >= " + Math.max(startTime, 0))
|
||||
.where("ts <= " + endTime)
|
||||
.where("glucose != 0")
|
||||
.orderBy("ts desc")
|
||||
.limit(number)
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void updateDB() {
|
||||
fixUpTable(schema, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.provider.BaseColumns;
|
||||
import android.text.format.DateFormat;
|
||||
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Select;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
@Table(name = "Libre2Sensors", id = BaseColumns._ID)
|
||||
public class Libre2Sensor extends PlusModel {
|
||||
static final String TAG = "Libre2Sensor";
|
||||
|
||||
static final String[] schema = {
|
||||
"CREATE VIEW Libre2Sensors AS SELECT MIN(_id) as _id, serial, MIN(ts) as ts_from, MAX(ts) AS ts_to, COUNT(*) AS readings FROM Libre2RawValue2 GROUP BY serial ORDER BY ts DESC;"
|
||||
};
|
||||
|
||||
@Column(name = "serial", index = true)
|
||||
public String serial;
|
||||
|
||||
@Column(name = "ts_from", index = false)
|
||||
public long ts_from;
|
||||
|
||||
@Column(name = "ts_to", index = false)
|
||||
public long ts_to;
|
||||
|
||||
@Column(name = "readings", index = false)
|
||||
public long readings;
|
||||
|
||||
private static volatile String cachedStringSensors = null;
|
||||
|
||||
public static String Libre2Sensors() {
|
||||
String Sum = "";
|
||||
|
||||
if ((cachedStringSensors == null) || (JoH.ratelimit("libre2sensor-report", 120))) {
|
||||
|
||||
List<Libre2Sensor> rs = new Select()
|
||||
.from(Libre2Sensor.class)
|
||||
.execute();
|
||||
|
||||
for (Libre2Sensor Sensorpart : rs) {
|
||||
Long Diff_ts = Sensorpart.ts_to - Sensorpart.ts_from;
|
||||
Sum = Sum + Sensorpart.serial +
|
||||
"\n" + DateFormat.format("dd.MM.yy", Sensorpart.ts_from) +
|
||||
" to: " + DateFormat.format("dd.MM.yy", Sensorpart.ts_to) +
|
||||
" (" + JoH.niceTimeScalarShortWithDecimalHours(Diff_ts) + ")" +
|
||||
" readings: " + ((Sensorpart.readings * 100) / (Diff_ts / 60000)) + "%\n" +
|
||||
"------------------\n";
|
||||
}
|
||||
cachedStringSensors=Sum;
|
||||
}
|
||||
|
||||
return cachedStringSensors;
|
||||
}
|
||||
|
||||
public static void updateDB() {
|
||||
fixUpTable(schema, false);
|
||||
}
|
||||
}
|
||||
192
lib/nightscout/com/eveningoutpost/dexdrip/Models/LibreBlock.java
Normal file
192
lib/nightscout/com/eveningoutpost/dexdrip/Models/LibreBlock.java
Normal file
@@ -0,0 +1,192 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Select;
|
||||
import com.eveningoutpost.dexdrip.Models.UserError.Log;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.UploaderQueue;
|
||||
import com.google.gson.annotations.Expose;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
/**
|
||||
* Created by jamorham on 19/10/2017.
|
||||
*/
|
||||
|
||||
@Table(name = "LibreBlock", id = BaseColumns._ID)
|
||||
public class LibreBlock extends PlusModel {
|
||||
|
||||
private static final String TAG = "LibreBlock";
|
||||
static final String[] schema = {
|
||||
"CREATE TABLE LibreBlock (_id INTEGER PRIMARY KEY AUTOINCREMENT);",
|
||||
"ALTER TABLE LibreBlock ADD COLUMN timestamp INTEGER;",
|
||||
"ALTER TABLE LibreBlock ADD COLUMN reference TEXT;",
|
||||
"ALTER TABLE LibreBlock ADD COLUMN blockbytes BLOB;",
|
||||
"ALTER TABLE LibreBlock ADD COLUMN bytestart INTEGER;",
|
||||
"ALTER TABLE LibreBlock ADD COLUMN byteend INTEGER;",
|
||||
"ALTER TABLE LibreBlock ADD COLUMN calculatedbg REAL;",
|
||||
"ALTER TABLE LibreBlock ADD COLUMN uuid TEXT;",
|
||||
"ALTER TABLE LibreBlock ADD COLUMN patchUid BLOB;",
|
||||
"ALTER TABLE LibreBlock ADD COLUMN patchInfo BLOB;",
|
||||
"CREATE INDEX index_LibreBlock_timestamp on LibreBlock(timestamp);",
|
||||
"CREATE INDEX index_LibreBlock_bytestart on LibreBlock(bytestart);",
|
||||
"CREATE INDEX index_LibreBlock_byteend on LibreBlock(byteend);",
|
||||
"CREATE INDEX index_LibreBlock_uuid on LibreBlock(uuid);"
|
||||
};
|
||||
|
||||
|
||||
@Expose
|
||||
@Column(name = "timestamp", index = true)
|
||||
public long timestamp;
|
||||
|
||||
@Expose
|
||||
@Column(name = "bytestart", index = true)
|
||||
public long byte_start;
|
||||
|
||||
@Expose
|
||||
@Column(name = "byteend", index = true)
|
||||
public long byte_end;
|
||||
|
||||
@Expose
|
||||
@Column(name = "reference", index = true)
|
||||
public String reference;
|
||||
|
||||
@Expose
|
||||
@Column(name = "blockbytes")
|
||||
public byte[] blockbytes;
|
||||
|
||||
@Expose
|
||||
@Column(name = "calculatedbg")
|
||||
public double calculated_bg;
|
||||
|
||||
@Expose
|
||||
@Column(name = "uuid", index = true)
|
||||
public String uuid;
|
||||
|
||||
@Expose
|
||||
@Column(name = "patchUid")
|
||||
public byte[] patchUid;
|
||||
|
||||
@Expose
|
||||
@Column(name = "patchInfo")
|
||||
public byte[] patchInfo;
|
||||
|
||||
public static LibreBlock createAndSave(String reference, long timestamp, byte[] blocks, int byte_start) {
|
||||
return createAndSave(reference, timestamp, blocks, byte_start, false, null, null);
|
||||
}
|
||||
|
||||
// if you are indexing by block then just * 8 to get byte start
|
||||
public static LibreBlock createAndSave(String reference, long timestamp, byte[] blocks, int byte_start, boolean allowUpload, byte[] patchUid, byte[] patchInfo) {
|
||||
final LibreBlock lb = create(reference, timestamp, blocks, byte_start, patchUid, patchInfo);
|
||||
if (lb != null) {
|
||||
lb.save();
|
||||
if(byte_start == 0 && blocks.length == 344 && allowUpload) {
|
||||
Log.d(TAG, "sending new item to queue");
|
||||
UploaderQueue.newTransmitterDataEntry("create" ,lb);
|
||||
}
|
||||
}
|
||||
return lb;
|
||||
}
|
||||
|
||||
private static LibreBlock create(String reference, long timestamp, byte[] blocks, int byte_start, byte[] patchUid, byte[] patchInfo) {
|
||||
UserError.Log.e(TAG,"Backtrack: "+JoH.backTrace());
|
||||
if (reference == null) {
|
||||
UserError.Log.e(TAG, "Cannot save block with null reference");
|
||||
return null;
|
||||
}
|
||||
if (blocks == null) {
|
||||
UserError.Log.e(TAG, "Cannot save block with null data");
|
||||
return null;
|
||||
}
|
||||
|
||||
final LibreBlock lb = new LibreBlock();
|
||||
lb.reference = reference;
|
||||
lb.blockbytes = blocks;
|
||||
lb.byte_start = byte_start;
|
||||
lb.byte_end = byte_start + blocks.length;
|
||||
lb.timestamp = timestamp;
|
||||
lb.patchUid = patchUid;
|
||||
lb.patchInfo = patchInfo;
|
||||
lb.uuid = UUID.randomUUID().toString();
|
||||
return lb;
|
||||
}
|
||||
|
||||
public static LibreBlock getLatestForTrend() {
|
||||
return getLatestForTrend(JoH.tsl() - Constants.DAY_IN_MS, JoH.tsl() );
|
||||
}
|
||||
|
||||
|
||||
public static LibreBlock getLatestForTrend(long start_time, long end_time) {
|
||||
|
||||
return new Select()
|
||||
.from(LibreBlock.class)
|
||||
.where("bytestart == 0")
|
||||
.where("byteend >= 344")
|
||||
.where("timestamp >= ?", start_time)
|
||||
.where("timestamp <= ?", end_time)
|
||||
.orderBy("timestamp desc")
|
||||
.executeSingle();
|
||||
}
|
||||
|
||||
public static List<LibreBlock> getForTrend(long start_time, long end_time) {
|
||||
|
||||
return new Select()
|
||||
.from(LibreBlock.class)
|
||||
.where("bytestart == 0")
|
||||
.where("byteend >= 344")
|
||||
.where("timestamp >= ?", start_time)
|
||||
.where("timestamp <= ?", end_time)
|
||||
.orderBy("timestamp asc")
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static LibreBlock getForTimestamp(long timestamp) {
|
||||
final long margin = (3 * 1000);
|
||||
return new Select()
|
||||
.from(LibreBlock.class)
|
||||
.where("timestamp >= ?", (timestamp - margin))
|
||||
.where("timestamp <= ?", (timestamp + margin))
|
||||
.executeSingle();
|
||||
}
|
||||
|
||||
public static void UpdateBgVal(long timestamp, double calculated_value) {
|
||||
LibreBlock libreBlock = getForTimestamp(timestamp);
|
||||
if (libreBlock == null) {
|
||||
return;
|
||||
}
|
||||
Log.e(TAG, "Updating bg for timestamp " + timestamp);
|
||||
libreBlock.calculated_bg = calculated_value;
|
||||
libreBlock.save();
|
||||
}
|
||||
|
||||
public static LibreBlock findByUuid(String uuid) {
|
||||
try {
|
||||
return new Select()
|
||||
.from(LibreBlock.class)
|
||||
.where("uuid = ?", uuid)
|
||||
.executeSingle();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG,"findByUuid() Got exception on Select : "+e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final boolean d = false;
|
||||
|
||||
public static void updateDB() {
|
||||
fixUpTable(schema, false);
|
||||
}
|
||||
|
||||
public static LibreBlock byid(long id) {
|
||||
return new Select()
|
||||
.from(LibreBlock.class)
|
||||
.where("_ID = ?", id)
|
||||
.executeSingle();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.google.gson.annotations.Expose;
|
||||
|
||||
/**
|
||||
* Created by jamorham on 19/10/2017.
|
||||
*/
|
||||
|
||||
@Table(name = "LibreData", id = BaseColumns._ID)
|
||||
public class LibreData extends PlusModel {
|
||||
private static final String TAG = "LibreData";
|
||||
static final String[] schema = {
|
||||
"CREATE TABLE LibreData (_id INTEGER PRIMARY KEY AUTOINCREMENT);",
|
||||
"ALTER TABLE LibreData ADD COLUMN timestamp INTEGER;",
|
||||
"ALTER TABLE LibreData ADD COLUMN temperature REAL;",
|
||||
"ALTER TABLE LibreData ADD COLUMN temperatureraw INTEGER;",
|
||||
|
||||
"CREATE INDEX index_LibreData_timestamp on LibreData(timestamp);"
|
||||
};
|
||||
|
||||
|
||||
@Expose
|
||||
@Column(name = "timestamp", index = true)
|
||||
public long timestamp;
|
||||
|
||||
@Expose
|
||||
@Column(name = "temperature")
|
||||
public double temperature;
|
||||
|
||||
@Expose
|
||||
@Column(name = "temperatureraw")
|
||||
public long temperatureraw;
|
||||
|
||||
|
||||
public static LibreData create(byte[] temp_bytes) {
|
||||
final LibreData ld = new LibreData();
|
||||
ld.timestamp = JoH.tsl();
|
||||
// TODO
|
||||
//ld.temperatureraw = get byte order value from temp_bytes
|
||||
//ld.temperature = evaluate temperature from temperature raw
|
||||
return ld;
|
||||
}
|
||||
|
||||
private static final boolean d = false;
|
||||
|
||||
public static void updateDB() {
|
||||
fixUpTable(schema, false);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.util.HexDump;
|
||||
import com.eveningoutpost.dexdrip.LibreAlarmReceiver;
|
||||
import com.eveningoutpost.dexdrip.Models.UserError.Log;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.CompatibleApps;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Intents;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
|
||||
import com.eveningoutpost.dexdrip.xdrip;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class LibreOOPAlgorithm {
|
||||
private static final String TAG = "LibreOOPAlgorithm";
|
||||
|
||||
static public void SendData(byte[] fullData, long timestamp) {
|
||||
SendData(fullData, timestamp, null, null);
|
||||
}
|
||||
|
||||
static public void SendData(byte[] fullData, long timestamp, byte []patchUid, byte []patchInfo) {
|
||||
if(fullData == null) {
|
||||
Log.e(TAG, "SendData called with null data");
|
||||
return;
|
||||
}
|
||||
|
||||
if(fullData.length < 344) {
|
||||
Log.e(TAG, "SendData called with data size too small. " + fullData.length);
|
||||
return;
|
||||
}
|
||||
Log.i(TAG, "Sending full data to OOP Algorithm data-len = " + fullData.length);
|
||||
|
||||
fullData = java.util.Arrays.copyOfRange(fullData, 0, 0x158);
|
||||
Log.i(TAG, "Data that will be sent is " + HexDump.dumpHexString(fullData));
|
||||
|
||||
Intent intent = new Intent(Intents.XDRIP_PLUS_LIBRE_DATA);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putByteArray(Intents.LIBRE_DATA_BUFFER, fullData);
|
||||
bundle.putLong(Intents.LIBRE_DATA_TIMESTAMP, timestamp);
|
||||
bundle.putString(Intents.LIBRE_SN, PersistentStore.getString("LibreSN"));
|
||||
bundle.putInt(Intents.LIBRE_RAW_ID, android.os.Process.myPid());
|
||||
|
||||
if(patchUid != null) {
|
||||
bundle.putByteArray(Intents.LIBRE_PATCH_UID_BUFFER, patchUid);
|
||||
}
|
||||
if(patchInfo != null) {
|
||||
bundle.putByteArray(Intents.LIBRE_PATCH_INFO_BUFFER, patchInfo);
|
||||
}
|
||||
|
||||
intent.putExtras(bundle);
|
||||
intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
|
||||
|
||||
final String packages = PersistentStore.getString(CompatibleApps.EXTERNAL_ALG_PACKAGES);
|
||||
if (packages.length() > 0) {
|
||||
final String[] packagesE = packages.split(",");
|
||||
for (final String destination : packagesE) {
|
||||
if (destination.length() > 3) {
|
||||
intent.setPackage(destination);
|
||||
Log.d(TAG, "Sending to package: " + destination);
|
||||
xdrip.getAppContext().sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Sending to generic package");
|
||||
xdrip.getAppContext().sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static public void HandleData(String oopData) {
|
||||
Log.e(TAG, "HandleData called with " + oopData);
|
||||
OOPResults oOPResults = null;
|
||||
try {
|
||||
final Gson gson = new GsonBuilder().create();
|
||||
OOPResultsContainer oOPResultsContainer = gson.fromJson(oopData, OOPResultsContainer.class);
|
||||
|
||||
if(oOPResultsContainer.Message != null) {
|
||||
Log.e(TAG, "recieved a message from oop algorithm:" + oOPResultsContainer.Message);
|
||||
}
|
||||
|
||||
if(oOPResultsContainer.oOPResultsArray.length > 0) {
|
||||
oOPResults = oOPResultsContainer.oOPResultsArray[0];
|
||||
} else {
|
||||
Log.e(TAG, "oOPResultsArray exists, but size is zero");
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) { //TODO: what exception should we catch here.
|
||||
Log.e(TAG, "HandleData cought exception ", e);
|
||||
return;
|
||||
}
|
||||
boolean use_raw = Pref.getBooleanDefaultFalse("calibrate_external_libre_algorithm");
|
||||
ReadingData.TransferObject libreAlarmObject = new ReadingData.TransferObject();
|
||||
libreAlarmObject.data = new ReadingData();
|
||||
libreAlarmObject.data.trend = new ArrayList<GlucoseData>();
|
||||
|
||||
double factor = 1;
|
||||
if(use_raw) {
|
||||
// When handeling raw, data is expected to be bigger in a factor of 1000 and
|
||||
// is then devided by Constants.LIBRE_MULTIPLIER
|
||||
factor = 1000 / Constants.LIBRE_MULTIPLIER;
|
||||
}
|
||||
|
||||
// Add the first object, that is the current time
|
||||
GlucoseData glucoseData = new GlucoseData();
|
||||
glucoseData.sensorTime = oOPResults.currentTime;
|
||||
glucoseData.realDate = oOPResults.timestamp;
|
||||
glucoseData.glucoseLevel = (int)(oOPResults.currentBg * factor);
|
||||
glucoseData.glucoseLevelRaw = (int)(oOPResults.currentBg * factor);
|
||||
|
||||
libreAlarmObject.data.trend.add(glucoseData);
|
||||
|
||||
// TODO: Add here data of last 10 minutes or whatever.
|
||||
|
||||
|
||||
// Add the historic data
|
||||
libreAlarmObject.data.history = new ArrayList<GlucoseData>();
|
||||
for(HistoricBg historicBg : oOPResults.historicBg) {
|
||||
if(historicBg.quality == 0) {
|
||||
glucoseData = new GlucoseData();
|
||||
glucoseData.realDate = oOPResults.timestamp + (historicBg.time - oOPResults.currentTime) * 60000;
|
||||
glucoseData.glucoseLevel = (int)(historicBg.bg * factor);
|
||||
glucoseData.glucoseLevelRaw = (int)(historicBg.bg * factor);
|
||||
libreAlarmObject.data.history.add(glucoseData);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the current point again. This is needed in order to have the last gaps closed.
|
||||
// TODO: Base this on real BG values.
|
||||
glucoseData = new GlucoseData();
|
||||
glucoseData.realDate = oOPResults.timestamp;
|
||||
glucoseData.glucoseLevel = (int)(oOPResults.currentBg * factor);
|
||||
glucoseData.glucoseLevelRaw = (int)(oOPResults.currentBg * factor);
|
||||
libreAlarmObject.data.history.add(glucoseData);
|
||||
|
||||
Log.e(TAG, "HandleData Created the following object " + libreAlarmObject.toString());
|
||||
LibreAlarmReceiver.CalculateFromDataTransferObject(libreAlarmObject, use_raw);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ResolveInfo;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Intents;
|
||||
import com.eveningoutpost.dexdrip.xdrip;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by jamorham on 23/02/2016.
|
||||
*/
|
||||
public class NSClientChat {
|
||||
|
||||
private static final String TAG = "jamorham nsclient";
|
||||
|
||||
|
||||
public static void pushTreatmentAsync(final Treatments thistreatment) {
|
||||
Thread testAddTreatment = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
try {
|
||||
Context context = xdrip.getAppContext();
|
||||
JSONObject data = new JSONObject();
|
||||
if (thistreatment.carbs > 0) {
|
||||
if (thistreatment.insulin > 0) {
|
||||
data.put("eventType", "Meal Bolus");
|
||||
} else {
|
||||
data.put("eventType", "Carb Correction");
|
||||
}
|
||||
} else {
|
||||
if (thistreatment.insulin > 0) {
|
||||
data.put("eventType", "Correction Bolus");
|
||||
} else {
|
||||
if ((thistreatment.notes != null) && (thistreatment.notes.length() > 1)) {
|
||||
data.put("eventType", "Note");
|
||||
} else {
|
||||
data.put("eventType", "<None>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data.put("insulin", thistreatment.insulin);
|
||||
if (thistreatment.insulinJSON != null) {
|
||||
data.put("insulinInjections", thistreatment.insulinJSON);
|
||||
}
|
||||
data.put("carbs", thistreatment.carbs);
|
||||
if (thistreatment.notes != null) {
|
||||
data.put("notes", thistreatment.notes);
|
||||
}
|
||||
// data.put("_id", thistreatment.uuid.replace("-",""));
|
||||
//data.put("uuid",thistreatment.uuid);
|
||||
data.put("created_at", DateUtil.toISOString(thistreatment.timestamp));
|
||||
// data.put("NSCLIENTTESTRECORD", "NSCLIENTTESTRECORD");
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("action", "dbAdd");
|
||||
bundle.putString("collection", "treatments"); // "treatments" || "entries" || "devicestatus" || "profile" || "food"
|
||||
bundle.putString("data", data.toString());
|
||||
Intent intent = new Intent(Intents.ACTION_DATABASE);
|
||||
intent.putExtras(bundle);
|
||||
intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
|
||||
context.sendBroadcast(intent);
|
||||
List<ResolveInfo> q = context.getPackageManager().queryBroadcastReceivers(intent, 0);
|
||||
if (q.size() < 1) {
|
||||
Log.e(TAG, "DBADD No receivers");
|
||||
} else Log.e(TAG, "DBADD dbAdd " + q.size() + " receivers");
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Got exception with parsing: " + e.toString());
|
||||
}
|
||||
}
|
||||
};
|
||||
testAddTreatment.start();
|
||||
}
|
||||
|
||||
}
|
||||
26
lib/nightscout/com/eveningoutpost/dexdrip/Models/Noise.java
Normal file
26
lib/nightscout/com/eveningoutpost/dexdrip/Models/Noise.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
|
||||
|
||||
/**
|
||||
* Created by jamorham on 04/03/2018.
|
||||
*/
|
||||
|
||||
// TODO future move noise trigger constants here
|
||||
|
||||
public class Noise {
|
||||
|
||||
private static final String TAG = "xDripNoise";
|
||||
|
||||
public static int getNoiseBlockLevel() {
|
||||
int value = 200;
|
||||
try {
|
||||
value = Integer.parseInt(Pref.getString("noise_block_level", "200"));
|
||||
} catch (NumberFormatException e) {
|
||||
UserError.Log.e(TAG, "Cannot process noise block level: " + e);
|
||||
}
|
||||
return value;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
class HistoricBg {
|
||||
|
||||
public int quality;
|
||||
public int time;
|
||||
public double bg;
|
||||
}
|
||||
|
||||
class OOPResults {
|
||||
double currentBg;
|
||||
int currentTime;
|
||||
int currentTrend;
|
||||
HistoricBg [] historicBg;
|
||||
long timestamp;
|
||||
String serialNumber;
|
||||
|
||||
String toGson() {
|
||||
Gson gson = new GsonBuilder().create();
|
||||
return gson.toJson(this);
|
||||
}
|
||||
}
|
||||
|
||||
public class OOPResultsContainer {
|
||||
OOPResultsContainer() {
|
||||
oOPResultsArray = new OOPResults[0];
|
||||
version = 1;
|
||||
}
|
||||
|
||||
String toGson() {
|
||||
Gson gson = new GsonBuilder().create();
|
||||
return gson.toJson(this);
|
||||
}
|
||||
|
||||
int version;
|
||||
String Message;
|
||||
|
||||
OOPResults[] oOPResultsArray;
|
||||
}
|
||||
159
lib/nightscout/com/eveningoutpost/dexdrip/Models/PenData.java
Normal file
159
lib/nightscout/com/eveningoutpost/dexdrip/Models/PenData.java
Normal file
@@ -0,0 +1,159 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.activeandroid.Model;
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Select;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
|
||||
import com.google.gson.annotations.Expose;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Table(name = "PenData", id = BaseColumns._ID)
|
||||
public class PenData extends Model {
|
||||
|
||||
private static final String TAG = "PenData";
|
||||
|
||||
@Expose
|
||||
@Column(name = "pen_mac", index = true)
|
||||
public String mac;
|
||||
|
||||
@Expose
|
||||
@Column(name = "typ", index = true)
|
||||
public String type;
|
||||
|
||||
@Expose
|
||||
@Column(name = "timestamp", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE)
|
||||
public long timestamp;
|
||||
|
||||
@Expose
|
||||
@Column(name = "idx")
|
||||
public long index;
|
||||
|
||||
@Expose
|
||||
@Column(name = "created_timestamp")
|
||||
public long created_timestamp;
|
||||
|
||||
@Expose
|
||||
@Column(name = "units")
|
||||
public double units;
|
||||
|
||||
@Expose
|
||||
@Column(name = "temperature")
|
||||
public double temperature;
|
||||
|
||||
@Expose
|
||||
@Column(name = "battery")
|
||||
public int battery;
|
||||
|
||||
@Expose
|
||||
@Column(name = "flags")
|
||||
public long flags;
|
||||
|
||||
@Expose
|
||||
@Column(name = "bitmap")
|
||||
public long bitmap_flags;
|
||||
|
||||
@Expose
|
||||
@Column(name = "uuid", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE)
|
||||
public String uuid;
|
||||
|
||||
@Expose
|
||||
@Column(name = "insulin_name")
|
||||
public String insulin_name;
|
||||
|
||||
|
||||
@Expose
|
||||
@Column(name = "raw")
|
||||
public byte[] raw;
|
||||
|
||||
private static final String[] schema = {
|
||||
"CREATE TABLE PenData (_id INTEGER PRIMARY KEY AUTOINCREMENT, battery INTEGER, created_timestamp INTEGER, flags INTEGER, idx INTEGER, pen_mac TEXT, raw BLOB, temperature REAL, timestamp INTEGER UNIQUE ON CONFLICT FAIL, typ TEXT, units REAL, uuid TEXT UNIQUE ON CONFLICT FAIL);",
|
||||
"ALTER TABLE PenData ADD COLUMN bitmap INTEGER;",
|
||||
"ALTER TABLE PenData ADD COLUMN insulin_name TEXT;",
|
||||
"CREATE INDEX index_PenData_pen_mac on PenData(pen_mac);",
|
||||
"CREATE INDEX index_PenData_timestamp on PenData(timestamp);",
|
||||
"CREATE INDEX index_PenData_typ on PenData(typ);",
|
||||
"CREATE UNIQUE INDEX index_unq on PenData(pen_mac,idx);"
|
||||
};
|
||||
|
||||
public static void updateDB() {
|
||||
PlusModel.fixUpTable(schema, false);
|
||||
}
|
||||
|
||||
|
||||
public static PenData create(final String mac, final String type, final int index, final double units, final long timestamp, final double temperature, final byte[] raw) {
|
||||
|
||||
// TODO baulk on very old records
|
||||
|
||||
if (mac == null || type == null || index < 0 || units == -1 || timestamp < 0) {
|
||||
UserError.Log.wtf(TAG, "Invalid data sent to PenData.create() - skipping");
|
||||
return null;
|
||||
}
|
||||
|
||||
// NOTE negative units indicates rewind
|
||||
final PenData penData = new PenData();
|
||||
penData.created_timestamp = JoH.tsl();
|
||||
penData.uuid = UUID.randomUUID().toString();
|
||||
penData.index = index;
|
||||
penData.units = units;
|
||||
penData.timestamp = timestamp;
|
||||
penData.temperature = temperature;
|
||||
penData.mac = mac;
|
||||
penData.type = type;
|
||||
penData.raw = raw;
|
||||
return penData;
|
||||
}
|
||||
|
||||
public static long getHighestIndex(final String mac) {
|
||||
if (mac == null) return -1;
|
||||
final PenData penData = new Select()
|
||||
.from(PenData.class)
|
||||
.where("pen_mac = ?", mac)
|
||||
.orderBy("idx desc")
|
||||
.executeSingle();
|
||||
return penData != null ? penData.index : -1;
|
||||
}
|
||||
|
||||
|
||||
public static long getMissingIndex(final String mac) {
|
||||
if (mac == null) return -1;
|
||||
final List<PenData> list = new Select()
|
||||
.from(PenData.class)
|
||||
.where("pen_mac = ?", mac)
|
||||
.where("timestamp > ?", JoH.tsl() - Constants.WEEK_IN_MS)
|
||||
.orderBy("idx desc")
|
||||
.execute();
|
||||
long got = -1;
|
||||
for (final PenData pd : list) {
|
||||
if (got != -1 && pd.index != got - 1) {
|
||||
UserError.Log.d(TAG, "Tripped missing index on: " + got + " vs " + pd.index);
|
||||
return got - 1;
|
||||
}
|
||||
got = pd.index;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static List<PenData> getAllRecordsBetween(final long start, final long end) {
|
||||
final List<PenData> list = new Select()
|
||||
.from(PenData.class)
|
||||
.where("timestamp >= ?", start)
|
||||
.where("timestamp <= ?", end)
|
||||
.orderBy("typ asc, pen_mac asc, timestamp asc")
|
||||
.execute();
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
public String brief() {
|
||||
return mac + " " + JoH.dateTimeText(timestamp) + " " + units + "U";
|
||||
}
|
||||
|
||||
public String penName() {
|
||||
return type + " " + mac; // TODO have some way to name pen better
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import com.activeandroid.Model;
|
||||
import com.activeandroid.util.SQLiteUtils;
|
||||
|
||||
/**
|
||||
* Created by jamorham on 01/02/2017.
|
||||
*/
|
||||
|
||||
public class PlusModel extends Model {
|
||||
|
||||
protected synchronized static boolean fixUpTable(String[] schema, boolean patched) {
|
||||
if (patched) return true;
|
||||
|
||||
for (String patch : schema) {
|
||||
try {
|
||||
SQLiteUtils.execSql(patch);
|
||||
} catch (Exception e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
132
lib/nightscout/com/eveningoutpost/dexdrip/Models/Prediction.java
Normal file
132
lib/nightscout/com/eveningoutpost/dexdrip/Models/Prediction.java
Normal file
@@ -0,0 +1,132 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Delete;
|
||||
import com.activeandroid.query.Select;
|
||||
import com.google.gson.annotations.Expose;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Created by jamorham on 11/06/2018.
|
||||
*/
|
||||
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Table(name = "Prediction", id = BaseColumns._ID)
|
||||
public class Prediction extends PlusModel {
|
||||
|
||||
private static boolean patched = false;
|
||||
private final static String TAG = Prediction.class.getSimpleName();
|
||||
private final static boolean d = false;
|
||||
|
||||
private static final String[] schema = {
|
||||
"CREATE TABLE Prediction (_id INTEGER PRIMARY KEY AUTOINCREMENT);",
|
||||
"ALTER TABLE Prediction ADD COLUMN timestamp INTEGER;",
|
||||
"ALTER TABLE Prediction ADD COLUMN glucose REAL;",
|
||||
"ALTER TABLE Prediction ADD COLUMN source TEXT;",
|
||||
"ALTER TABLE Prediction ADD COLUMN note TEXT;",
|
||||
"CREATE INDEX index_Prediction_source on Prediction(source);",
|
||||
"CREATE UNIQUE INDEX index_Prediction_timestamp on Prediction(timestamp);"};
|
||||
|
||||
|
||||
@Expose
|
||||
@Column(name = "timestamp", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE)
|
||||
public long timestamp;
|
||||
|
||||
@Expose
|
||||
@Column(name = "glucose")
|
||||
public double glucose;
|
||||
|
||||
@Expose
|
||||
@Column(name = "source")
|
||||
public String source;
|
||||
|
||||
@Expose
|
||||
@Column(name = "note")
|
||||
public String note;
|
||||
|
||||
|
||||
public static Prediction create(long timestamp, int glucose, String source) {
|
||||
final Prediction prediction = new Prediction();
|
||||
prediction.timestamp = timestamp;
|
||||
prediction.glucose = glucose;
|
||||
prediction.source = source;
|
||||
return prediction;
|
||||
}
|
||||
|
||||
public Prediction addNote(String note) {
|
||||
this.note = note;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public String toS() {
|
||||
return JoH.defaultGsonInstance().toJson(this);
|
||||
}
|
||||
|
||||
// static methods
|
||||
|
||||
public static Prediction last() {
|
||||
try {
|
||||
return new Select()
|
||||
.from(Prediction.class)
|
||||
.orderBy("timestamp desc")
|
||||
.executeSingle();
|
||||
} catch (android.database.sqlite.SQLiteException e) {
|
||||
updateDB();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Prediction> latestForGraph(int number, double startTime) {
|
||||
return latestForGraph(number, (long) startTime, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static List<Prediction> latestForGraph(int number, long startTime) {
|
||||
return latestForGraph(number, startTime, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static List<Prediction> latestForGraph(int number, long startTime, long endTime) {
|
||||
try {
|
||||
final List<Prediction> results = new Select()
|
||||
.from(Prediction.class)
|
||||
.where("timestamp >= " + Math.max(startTime, 0))
|
||||
.where("timestamp <= " + endTime)
|
||||
.orderBy("timestamp asc") // warn asc!
|
||||
.limit(number)
|
||||
.execute();
|
||||
|
||||
return results;
|
||||
} catch (android.database.sqlite.SQLiteException e) {
|
||||
updateDB();
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static List<Prediction> cleanup(int retention_days) {
|
||||
return new Delete()
|
||||
.from(Prediction.class)
|
||||
.where("timestamp < ?", JoH.tsl() - (retention_days * 86400000L))
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
||||
// create the table ourselves without worrying about model versioning and downgrading
|
||||
public static void updateDB() {
|
||||
patched = fixUpTable(schema, patched);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
// class from LibreAlarm
|
||||
|
||||
public class PredictionData extends GlucoseData {
|
||||
|
||||
public enum Result {
|
||||
OK,
|
||||
ERROR_NO_NFC,
|
||||
ERROR_NFC_READ
|
||||
}
|
||||
|
||||
public double trend = -1;
|
||||
public double confidence = -1;
|
||||
public Result errorCode;
|
||||
public int attempt;
|
||||
|
||||
public PredictionData() {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.databinding.BaseObservable;
|
||||
|
||||
import com.eveningoutpost.dexdrip.utils.DexCollectionType;
|
||||
import com.eveningoutpost.dexdrip.xdrip;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.eveningoutpost.dexdrip.UtilityModels.BgGraphBuilder.DEXCOM_PERIOD;
|
||||
import static com.eveningoutpost.dexdrip.UtilityModels.Constants.STALE_CALIBRATION_CUT_OFF;
|
||||
|
||||
/**
|
||||
* Created by jamorham on 01/10/2017.
|
||||
* <p>
|
||||
* Provides InitialDataQuality status object used to determine
|
||||
* whether we can process an initial calibration right now
|
||||
* <p>
|
||||
* Currently does not handle interpolation or interstitial lag
|
||||
*/
|
||||
|
||||
public class ProcessInitialDataQuality {
|
||||
|
||||
public static InitialDataQuality getInitialDataQuality() {
|
||||
// get uncalculated data
|
||||
JoH.clearCache();
|
||||
final List<BgReading> uncalculated = BgReading.latestUnCalculated(3);
|
||||
return getInitialDataQuality(uncalculated);
|
||||
}
|
||||
|
||||
// Check if data looks suitable for initial calibration
|
||||
// List must be supplied with newest data first and without duplicates
|
||||
public static InitialDataQuality getInitialDataQuality(final List<BgReading> uncalculated) {
|
||||
final InitialDataQuality result = new InitialDataQuality();
|
||||
String alert = "";
|
||||
final Boolean service_running = DexCollectionType.getServiceRunningState();
|
||||
if (service_running != null) result.collector_running = service_running;
|
||||
|
||||
// if there is no data at all within calibration cut off then return negative result
|
||||
if (!((uncalculated == null) || (uncalculated.size() < 1))) {
|
||||
// we have at least one record
|
||||
result.received_raw_data = true;
|
||||
result.last_activity = uncalculated.get(0).timestamp;
|
||||
|
||||
// work out next likely time to receive a reading
|
||||
final long OUR_PERIOD = DEXCOM_PERIOD; // eventually to come from DexCollectionType
|
||||
result.next_activity_expected = result.last_activity + OUR_PERIOD;
|
||||
// if time already past based on last timestamp then work out in to the future based on period
|
||||
if (JoH.tsl() > result.next_activity_expected) {
|
||||
result.missed_last = true;
|
||||
final long offset = result.last_activity % OUR_PERIOD;
|
||||
result.next_activity_expected = ((JoH.tsl() / OUR_PERIOD) * OUR_PERIOD) + offset;
|
||||
|
||||
// this logic could probably be improved
|
||||
while (result.next_activity_expected < JoH.tsl()) {
|
||||
result.next_activity_expected += OUR_PERIOD;
|
||||
}
|
||||
|
||||
}
|
||||
if (!(JoH.msSince(uncalculated.get(0).timestamp) > STALE_CALIBRATION_CUT_OFF)) {
|
||||
// we got some data now see if it is recent enough
|
||||
result.recent_data = true;
|
||||
boolean adjusted = true; // run one time
|
||||
while (adjusted) {
|
||||
adjusted = false;
|
||||
for (int i = 0; i < uncalculated.size(); i++) {
|
||||
if (JoH.msSince(uncalculated.get(i).timestamp) > STALE_CALIBRATION_CUT_OFF) {
|
||||
uncalculated.remove(i);
|
||||
adjusted = true;
|
||||
break;
|
||||
}
|
||||
if (!SensorSanity.isRawValueSane(uncalculated.get(i).raw_data)) {
|
||||
uncalculated.remove(i);
|
||||
adjusted = true;
|
||||
alert = " "+"Raw Sensor data is outside valid range! Sensor problem!";
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
result.number_of_records_inside_window = uncalculated.size();
|
||||
// do we have enough good data?
|
||||
if (uncalculated.size() >= 3) {
|
||||
if (JoH.msSince(uncalculated.get(2).timestamp) > STALE_CALIBRATION_CUT_OFF) {
|
||||
result.advice = "Oldest of last 3 readings is more than " + JoH.niceTimeScalar(STALE_CALIBRATION_CUT_OFF) + " ago" + alert;
|
||||
} else {
|
||||
result.advice = "Readings look suitable for calibration" + alert;
|
||||
result.pass = true;
|
||||
}
|
||||
} else {
|
||||
result.advice = "Need 3 recent readings, got only " + uncalculated.size() + " so far" + alert;
|
||||
}
|
||||
} else {
|
||||
result.advice = "No data received in last " + JoH.niceTimeScalar(STALE_CALIBRATION_CUT_OFF) + alert;
|
||||
}
|
||||
} else {
|
||||
result.advice = "No data received yet" + alert;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String gs(int id) {
|
||||
return xdrip.getAppContext().getString(id);
|
||||
}
|
||||
|
||||
|
||||
public static class InitialDataQuality extends BaseObservable {
|
||||
public boolean collector_running = false;
|
||||
public boolean received_raw_data = false;
|
||||
public boolean recent_data = false;
|
||||
public boolean pass = false;
|
||||
public boolean missed_last = false;
|
||||
public int number_of_records_inside_window = 0;
|
||||
public long last_activity = 0;
|
||||
public long next_activity_expected = 0;
|
||||
public String advice = "";
|
||||
|
||||
public String getNextExpectedTill() {
|
||||
return JoH.niceTimeTill(next_activity_expected);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
250
lib/nightscout/com/eveningoutpost/dexdrip/Models/Profile.java
Normal file
250
lib/nightscout/com/eveningoutpost/dexdrip/Models/Profile.java
Normal file
@@ -0,0 +1,250 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
||||
import com.eveningoutpost.dexdrip.Home;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
|
||||
import com.eveningoutpost.dexdrip.profileeditor.ProfileEditor;
|
||||
import com.eveningoutpost.dexdrip.profileeditor.ProfileItem;
|
||||
import com.eveningoutpost.dexdrip.xdrip;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by jamorham on 04/01/16.
|
||||
*/
|
||||
|
||||
// user profile for insulin related parameters which can be configured and set at times of day
|
||||
// currently a placeholder with hardcoded values
|
||||
|
||||
// TODO Proper support for MG/DL
|
||||
|
||||
|
||||
public class Profile {
|
||||
|
||||
private final static String TAG = "jamorham pred";
|
||||
public static double minimum_shown_iob = 0.005;
|
||||
public static double minimum_shown_cob = 0.01;
|
||||
public static double minimum_insulin_recommendation = 0.1;
|
||||
public static double minimum_carb_recommendation = 1;
|
||||
public static double scale_factor = 18;
|
||||
private static double the_carb_ratio = 10; // now defunct
|
||||
private static double stored_default_sensitivity = 54; // now defunct
|
||||
private static double stored_default_absorption_rate = 35;
|
||||
private static double stored_default_insulin_action_time = 3.0;
|
||||
private static double stored_default_carb_delay_minutes = 15;
|
||||
private static boolean preferences_loaded = false;
|
||||
private static List<ProfileItem> profileItemList;
|
||||
|
||||
public static double getSensitivity(double when) {
|
||||
final double sensitivity = findItemListElementForTime(when).sensitivity;
|
||||
// Log.d(TAG,"sensitivity: "+sensitivity);
|
||||
return sensitivity;
|
||||
// expressed in native units lowering effect of 1U
|
||||
}
|
||||
|
||||
public static void setSensitivityDefault(double value) {
|
||||
// sanity check goes here
|
||||
stored_default_sensitivity = value;
|
||||
}
|
||||
|
||||
public static void setInsulinActionTimeDefault(double value) {
|
||||
// sanity check goes here
|
||||
if (value < 0.1) return;
|
||||
if (value > 24) return;
|
||||
stored_default_insulin_action_time = value;
|
||||
}
|
||||
|
||||
static double getCarbAbsorptionRate(double when) {
|
||||
return stored_default_absorption_rate; // carbs per hour
|
||||
}
|
||||
|
||||
public static void setCarbAbsorptionDefault(double value) {
|
||||
// sanity check goes here
|
||||
if (value < 0.01) return;
|
||||
stored_default_absorption_rate = value;
|
||||
}
|
||||
|
||||
static double insulinActionTime(double when) {
|
||||
return stored_default_insulin_action_time;
|
||||
}
|
||||
|
||||
static double carbDelayMinutes(double when) {
|
||||
return stored_default_carb_delay_minutes;
|
||||
}
|
||||
|
||||
static double maxLiverImpactRatio(double when) {
|
||||
return 0.3; // how much can the liver block carbs going in to blood stream?
|
||||
}
|
||||
|
||||
public static double getCarbRatio(double when) {
|
||||
return findItemListElementForTime(when).carb_ratio;
|
||||
//return the_carb_ratio; // g per unit
|
||||
}
|
||||
|
||||
|
||||
private static void populateProfile() {
|
||||
if (profileItemList == null) {
|
||||
profileItemList = ProfileEditor.loadData(false);
|
||||
Log.d(TAG, "Loaded profile data, blocks: " + profileItemList.size());
|
||||
}
|
||||
}
|
||||
|
||||
public static void invalidateProfile() {
|
||||
profileItemList = null;
|
||||
}
|
||||
|
||||
private static ProfileItem findItemListElementForTime(double when) {
|
||||
populateProfile();
|
||||
// TODO does this want/need a hash table lookup cache?
|
||||
if (profileItemList.size() == 1)
|
||||
profileItemList.get(0); // always will be first/only element.
|
||||
// get time of day
|
||||
final int min = ProfileItem.timeStampToMin(when);
|
||||
// find element
|
||||
for (ProfileItem item : profileItemList) {
|
||||
if (item.start_min < item.end_min) {
|
||||
// regular
|
||||
if ((item.start_min <= min) && (item.end_min >= min)) {
|
||||
// Log.d(TAG, "Match on item " + item.getTimePeriod() + " " + profileItemList.indexOf(item));
|
||||
return item;
|
||||
}
|
||||
} else {
|
||||
// item spans midnight
|
||||
if ((min >= item.start_min) || (min <= item.end_min)) {
|
||||
// Log.d(TAG, "midnight span Match on item " + item.getTimePeriod() + " " + profileItemList.indexOf(item));
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.wtf(TAG, "Null return from findItemListElementForTime");
|
||||
return null; // should be impossible
|
||||
}
|
||||
|
||||
|
||||
static public void setDefaultCarbRatio(Double value) {
|
||||
if (value <= 0) {
|
||||
Log.e(TAG, "Invalid default carb ratio: " + value);
|
||||
return;
|
||||
}
|
||||
the_carb_ratio = value; // g per unit
|
||||
}
|
||||
|
||||
static double getLiverSensRatio(double when) {
|
||||
return 2.0;
|
||||
}
|
||||
|
||||
public static void validateTargetRange() {
|
||||
final double default_target_glucose = tolerantParseDouble(Pref.getString("plus_target_range", Double.toString(5.5 / scale_factor)));
|
||||
if (default_target_glucose > tolerantParseDouble(Pref.getString("highValue", Double.toString(5.5 / scale_factor)))) {
|
||||
Pref.setString("plus_target_range", JoH.qs(default_target_glucose * Constants.MGDL_TO_MMOLL, 1));
|
||||
UserError.Log.i(TAG, "Converted initial value of target glucose to mmol");
|
||||
}
|
||||
}
|
||||
|
||||
static double getTargetRangeInMmol(double when) {
|
||||
// return tolerantParseDouble(Home.getString("plus_target_range",Double.toString(5.5 / scale_factor)));
|
||||
return getTargetRangeInUnits(when) / scale_factor;
|
||||
}
|
||||
|
||||
public static double getTargetRangeInUnits(double when) {
|
||||
return tolerantParseDouble(Pref.getString("plus_target_range", Double.toString(5.5 / scale_factor)));
|
||||
//return getTargetRangeInMmol(when) * scale_factor; // TODO deal with rounding errors here? (3 decimal places?)
|
||||
}
|
||||
|
||||
static double getCarbSensitivity(double when) {
|
||||
return getCarbRatio(when) / getSensitivity(when);
|
||||
}
|
||||
|
||||
static double getCarbsToRaiseByMmol(double mmol, double when) {
|
||||
|
||||
double result = getCarbSensitivity(when) * mmol;
|
||||
return result;
|
||||
}
|
||||
|
||||
static double getInsulinToLowerByMmol(double mmol, double when) {
|
||||
return mmol / getSensitivity(when);
|
||||
}
|
||||
|
||||
// take an average of carb suggestions when our scope is between two times
|
||||
static double getCarbsToRaiseByMmolBetweenTwoTimes(double mmol, double whennow, double whenthen) {
|
||||
double result = (getCarbsToRaiseByMmol(mmol, whennow) + getCarbsToRaiseByMmol(mmol, whenthen)) / 2;
|
||||
UserError.Log.d(TAG, "GetCarbsToRaiseByMmolBetweenTwoTimes: " + JoH.qs(mmol) + " result: " + JoH.qs(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
static double getInsulinToLowerByMmolBetweenTwoTimes(double mmol, double whennow, double whenthen) {
|
||||
return (getInsulinToLowerByMmol(mmol, whennow) + getInsulinToLowerByMmol(mmol, whenthen)) / 2;
|
||||
}
|
||||
|
||||
public static double[] evaluateEndGameMmol(double mmol, double endGameTime, double timeNow) {
|
||||
double addcarbs = 0;
|
||||
double addinsulin = 0;
|
||||
final double target_mmol = getTargetRangeInMmol(endGameTime) * scale_factor;
|
||||
double diff_mmol = target_mmol - mmol;
|
||||
if (diff_mmol > 0) {
|
||||
addcarbs = getCarbsToRaiseByMmolBetweenTwoTimes(diff_mmol, timeNow, endGameTime);
|
||||
} else if (diff_mmol < 0) {
|
||||
addinsulin = getInsulinToLowerByMmolBetweenTwoTimes(diff_mmol * -1, timeNow, endGameTime);
|
||||
}
|
||||
return new double[]{addcarbs, addinsulin};
|
||||
}
|
||||
|
||||
public static void reloadPreferencesIfNeeded(SharedPreferences prefs) {
|
||||
if (!preferences_loaded) reloadPreferences(prefs);
|
||||
}
|
||||
|
||||
public static void reloadPreferences() {
|
||||
Log.d(TAG, "Reloaded profile preferences");
|
||||
reloadPreferences(PreferenceManager.getDefaultSharedPreferences(xdrip.getAppContext()));
|
||||
}
|
||||
|
||||
public static synchronized void reloadPreferences(SharedPreferences prefs) {
|
||||
validateTargetRange();
|
||||
try {
|
||||
Profile.setSensitivityDefault(tolerantParseDouble(prefs.getString("profile_insulin_sensitivity_default", "0")));
|
||||
} catch (Exception e) {
|
||||
if (JoH.ratelimit("invalid-insulin-profile", 60)) {
|
||||
Home.toaststatic("Invalid insulin sensitivity");
|
||||
}
|
||||
}
|
||||
try {
|
||||
Profile.setDefaultCarbRatio(tolerantParseDouble(prefs.getString("profile_carb_ratio_default", "0")));
|
||||
} catch (Exception e) {
|
||||
if (JoH.ratelimit("invalid-insulin-profile", 60)) {
|
||||
Home.toaststatic("Invalid default carb ratio!");
|
||||
}
|
||||
}
|
||||
try {
|
||||
Profile.setCarbAbsorptionDefault(tolerantParseDouble(prefs.getString("profile_carb_absorption_default", "0")));
|
||||
} catch (Exception e) {
|
||||
if (JoH.ratelimit("invalid-insulin-profile", 60)) {
|
||||
Home.toaststatic("Invalid carb absorption rate");
|
||||
}
|
||||
}
|
||||
try {
|
||||
Profile.setInsulinActionTimeDefault(tolerantParseDouble(prefs.getString("xplus_insulin_dia", "3.0")));
|
||||
} catch (Exception e) {
|
||||
if (JoH.ratelimit("invalid-insulin-profile", 60)) {
|
||||
Home.toaststatic("Invalid insulin action time");
|
||||
}
|
||||
}
|
||||
profileItemList = null;
|
||||
populateProfile();
|
||||
preferences_loaded = true;
|
||||
}
|
||||
|
||||
private static double tolerantParseDouble(String str) throws NumberFormatException {
|
||||
return Double.parseDouble(str.replace(",", "."));
|
||||
|
||||
}
|
||||
|
||||
// TODO placeholder
|
||||
public static double getBasalRate(final long when) {
|
||||
return 0d;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
// class from LibreAlarm
|
||||
|
||||
import com.eveningoutpost.dexdrip.Models.UserError.Log;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ReadingData {
|
||||
|
||||
public PredictionData prediction;
|
||||
public List<GlucoseData> trend;
|
||||
public List<GlucoseData> history;
|
||||
public byte[] raw_data;
|
||||
|
||||
public ReadingData(PredictionData.Result result) {
|
||||
this.prediction = new PredictionData();
|
||||
this.prediction.realDate = System.currentTimeMillis();
|
||||
this.prediction.errorCode = result;
|
||||
this.trend = new ArrayList<>();
|
||||
this.history = new ArrayList<>();
|
||||
// The two bytes are needed here since some components don't like a null pointer.
|
||||
this.raw_data = new byte[2];
|
||||
}
|
||||
|
||||
public ReadingData(PredictionData prediction, List<GlucoseData> trend, List<GlucoseData> history) {
|
||||
this.prediction = prediction;
|
||||
this.trend = trend;
|
||||
this.history = history;
|
||||
}
|
||||
|
||||
public ReadingData() {}
|
||||
|
||||
public static class TransferObject {
|
||||
public long id;
|
||||
public ReadingData data;
|
||||
|
||||
public TransferObject() {}
|
||||
|
||||
public TransferObject(long id, ReadingData data) {
|
||||
this.id = id;
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
// A function to calculate the smoothing based only on 3 points.
|
||||
private void CalculateSmothedData3Points() {
|
||||
for (int i=0; i < trend.size() - 2 ; i++) {
|
||||
trend.get(i).glucoseLevelRawSmoothed =
|
||||
(trend.get(i).glucoseLevelRaw + trend.get(i+1).glucoseLevelRaw + trend.get(i+2).glucoseLevelRaw) / 3;
|
||||
}
|
||||
// Set the last two points. (doing our best - this will only be used if there are no previous readings).
|
||||
if(trend.size() >= 2) {
|
||||
// We have two points, use their average for both
|
||||
int average = (trend.get(trend.size()-2).glucoseLevelRaw + trend.get(trend.size()-1).glucoseLevelRaw ) / 2;
|
||||
trend.get(trend.size()-2).glucoseLevelRawSmoothed = average;
|
||||
trend.get(trend.size()-1).glucoseLevelRawSmoothed = average;
|
||||
} else if(trend.size() == 1){
|
||||
// Only one point, use it
|
||||
trend.get(trend.size()-1).glucoseLevelRawSmoothed = trend.get(trend.size()-1).glucoseLevelRaw;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void CalculateSmothedData5Points() {
|
||||
// In all places in the code, there should be exactly 16 points.
|
||||
// Since that might change, and I'm doing an average of 5, then in the case of less then 5 points,
|
||||
// I'll only copy the data as is (to make sure there are reasonable values when the function returns).
|
||||
if(trend.size() < 5) {
|
||||
for (int i=0; i < trend.size() - 4 ; i++) {
|
||||
trend.get(i).glucoseLevelRawSmoothed = trend.get(i).glucoseLevelRaw;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i=0; i < trend.size() - 4 ; i++) {
|
||||
trend.get(i).glucoseLevelRawSmoothed =
|
||||
(trend.get(i).glucoseLevelRaw +
|
||||
trend.get(i+1).glucoseLevelRaw +
|
||||
trend.get(i+2).glucoseLevelRaw +
|
||||
trend.get(i+3).glucoseLevelRaw +
|
||||
trend.get(i+4).glucoseLevelRaw ) / 5;
|
||||
}
|
||||
// We now have to calculate the last 4 points, will do our best...
|
||||
trend.get(trend.size()-4).glucoseLevelRawSmoothed =
|
||||
(trend.get(trend.size()-4).glucoseLevelRaw +
|
||||
trend.get(trend.size()-3).glucoseLevelRaw +
|
||||
trend.get(trend.size()-2).glucoseLevelRaw +
|
||||
trend.get(trend.size()-1).glucoseLevelRaw ) / 4;
|
||||
|
||||
trend.get(trend.size()-3).glucoseLevelRawSmoothed =
|
||||
(trend.get(trend.size()-3).glucoseLevelRaw +
|
||||
trend.get(trend.size()-2).glucoseLevelRaw +
|
||||
trend.get(trend.size()-1).glucoseLevelRaw ) / 3;
|
||||
|
||||
// Use the last two points for both last points
|
||||
trend.get(trend.size()-2).glucoseLevelRawSmoothed =
|
||||
(trend.get(trend.size()-2).glucoseLevelRaw +
|
||||
trend.get(trend.size()-1).glucoseLevelRaw ) / 2;
|
||||
|
||||
trend.get(trend.size()-1).glucoseLevelRawSmoothed = trend.get(trend.size()-2).glucoseLevelRawSmoothed;
|
||||
}
|
||||
|
||||
public void CalculateSmothedData() {
|
||||
CalculateSmothedData5Points();
|
||||
// print the values, remove before release
|
||||
for (int i=0; i < trend.size() ; i++) {
|
||||
Log.e("xxx","" + i + " raw val " + trend.get(i).glucoseLevelRaw + " smoothed " + trend.get(i).glucoseLevelRawSmoothed);
|
||||
}
|
||||
}
|
||||
}
|
||||
399
lib/nightscout/com/eveningoutpost/dexdrip/Models/Reminder.java
Normal file
399
lib/nightscout/com/eveningoutpost/dexdrip/Models/Reminder.java
Normal file
@@ -0,0 +1,399 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.content.Context;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.activeandroid.Model;
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Select;
|
||||
import com.activeandroid.util.SQLiteUtils;
|
||||
import com.eveningoutpost.dexdrip.Home;
|
||||
import com.eveningoutpost.dexdrip.Reminders;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
|
||||
import com.eveningoutpost.dexdrip.utils.HomeWifi;
|
||||
import com.google.gson.annotations.Expose;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* Created by jamorham on 01/02/2017.
|
||||
*/
|
||||
|
||||
@Table(name = "Reminder", id = BaseColumns._ID)
|
||||
public class Reminder extends Model {
|
||||
|
||||
|
||||
private static final String TAG = "Reminder";
|
||||
private static boolean patched = false;
|
||||
public static final String REMINDERS_ALL_DISABLED = "reminders-all-disabled";
|
||||
public static final String REMINDERS_NIGHT_DISABLED = "reminders-at-night-disabled";
|
||||
public static final String REMINDERS_RESTART_TOMORROW = "reminders-restart-tomorrow";
|
||||
public static final String REMINDERS_ADVANCED_MODE = "reminders-advanced-mode";
|
||||
private static final String[] schema = {
|
||||
"CREATE TABLE Reminder (_id INTEGER PRIMARY KEY AUTOINCREMENT)",
|
||||
"ALTER TABLE Reminder ADD COLUMN next_due INTEGER",
|
||||
"ALTER TABLE Reminder ADD COLUMN period INTEGER",
|
||||
"ALTER TABLE Reminder ADD COLUMN snoozed_till INTEGER DEFAULT 0",
|
||||
"ALTER TABLE Reminder ADD COLUMN last_snoozed_for INTEGER DEFAULT 0",
|
||||
"ALTER TABLE Reminder ADD COLUMN last_fired INTEGER DEFAULT 0",
|
||||
"ALTER TABLE Reminder ADD COLUMN fired_times INTEGER DEFAULT 0",
|
||||
"ALTER TABLE Reminder ADD COLUMN alerted_times INTEGER DEFAULT 0",
|
||||
"ALTER TABLE Reminder ADD COLUMN title TEXT",
|
||||
"ALTER TABLE Reminder ADD COLUMN alt_title TEXT",
|
||||
"ALTER TABLE Reminder ADD COLUMN sound_uri TEXT",
|
||||
"ALTER TABLE Reminder ADD COLUMN ideal_time TEXT",
|
||||
"ALTER TABLE Reminder ADD COLUMN priority INTEGER DEFAULT 0",
|
||||
"ALTER TABLE Reminder ADD COLUMN enabled INTEGER DEFAULT 0",
|
||||
"ALTER TABLE Reminder ADD COLUMN weekdays INTEGER DEFAULT 0",
|
||||
"ALTER TABLE Reminder ADD COLUMN weekends INTEGER DEFAULT 0",
|
||||
"ALTER TABLE Reminder ADD COLUMN repeating INTEGER DEFAULT 0",
|
||||
"ALTER TABLE Reminder ADD COLUMN alternating INTEGER DEFAULT 0",
|
||||
"ALTER TABLE Reminder ADD COLUMN alternate INTEGER DEFAULT 0",
|
||||
"ALTER TABLE Reminder ADD COLUMN chime INTEGER DEFAULT 0",
|
||||
"ALTER TABLE Reminder ADD COLUMN homeonly INTEGER DEFAULT 0",
|
||||
"ALTER TABLE Reminder ADD COLUMN speak INTEGER DEFAULT 0",
|
||||
"CREATE INDEX index_Reminder_next_due on Reminder(next_due)",
|
||||
"CREATE INDEX index_Reminder_enabled on Reminder(enabled)",
|
||||
"CREATE INDEX index_Reminder_weekdays on Reminder(weekdays)",
|
||||
"CREATE INDEX index_Reminder_homeonly on Reminder(homeonly)",
|
||||
"CREATE INDEX index_Reminder_weekends on Reminder(weekends)",
|
||||
"CREATE INDEX index_Reminder_priority on Reminder(priority)",
|
||||
"CREATE INDEX index_Reminder_timestamp on Reminder(timestamp)",
|
||||
"CREATE INDEX index_Reminder_snoozed_till on Reminder(snoozed_till)"
|
||||
};
|
||||
|
||||
@Expose
|
||||
@Column(name = "title")
|
||||
public String title;
|
||||
|
||||
@Expose
|
||||
@Column(name = "alt_title")
|
||||
public String alternate_title;
|
||||
|
||||
@Expose
|
||||
@Column(name = "next_due", index = true)
|
||||
public long next_due;
|
||||
|
||||
@Expose
|
||||
@Column(name = "period")
|
||||
public long period;
|
||||
|
||||
@Expose
|
||||
@Column(name = "sound_uri")
|
||||
public String sound_uri;
|
||||
|
||||
@Expose
|
||||
@Column(name = "enabled", index = true)
|
||||
public boolean enabled;
|
||||
|
||||
@Expose
|
||||
@Column(name = "weekdays", index = true)
|
||||
public boolean weekdays;
|
||||
|
||||
@Expose
|
||||
@Column(name = "weekends", index = true)
|
||||
public boolean weekends;
|
||||
|
||||
@Expose
|
||||
@Column(name = "homeonly", index = true)
|
||||
public boolean homeonly;
|
||||
|
||||
@Expose
|
||||
@Column(name = "speak")
|
||||
public boolean speak;
|
||||
|
||||
@Expose
|
||||
@Column(name = "repeating")
|
||||
public boolean repeating;
|
||||
|
||||
@Expose
|
||||
@Column(name = "chime")
|
||||
public boolean chime_only;
|
||||
|
||||
@Expose
|
||||
@Column(name = "alternating")
|
||||
public boolean alternating;
|
||||
|
||||
@Expose
|
||||
@Column(name = "alternate")
|
||||
public boolean alternate;
|
||||
|
||||
@Expose
|
||||
@Column(name = "snoozed_till", index = true)
|
||||
public long snoozed_till;
|
||||
|
||||
@Expose
|
||||
@Column(name = "last_fired", index = true)
|
||||
public long last_fired;
|
||||
|
||||
@Expose
|
||||
@Column(name = "last_snoozed_for")
|
||||
public long last_snoozed_for;
|
||||
|
||||
@Expose
|
||||
@Column(name = "fired_times")
|
||||
public long fired_times;
|
||||
|
||||
@Expose
|
||||
@Column(name = "alerted_times")
|
||||
public long alerted_times;
|
||||
|
||||
@Expose
|
||||
@Column(name = "priority")
|
||||
public long priority;
|
||||
|
||||
@Expose
|
||||
@Column(name = "ideal_time")
|
||||
public String ideal_time;
|
||||
|
||||
|
||||
public boolean isAlternate() {
|
||||
if (alternating && (alternate_title != null)) {
|
||||
return alternate;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return isAlternate() ? alternate_title : title;
|
||||
}
|
||||
|
||||
public String getAlternateTitle() {
|
||||
return alternate_title != null ? alternate_title : title + " alternate";
|
||||
}
|
||||
|
||||
public void updateTitle(String new_title) {
|
||||
if (isAlternate()) {
|
||||
alternate_title = new_title;
|
||||
} else {
|
||||
title = new_title;
|
||||
}
|
||||
save();
|
||||
}
|
||||
|
||||
public boolean isHoursPeriod() {
|
||||
return (period >= Constants.HOUR_IN_MS) && (period < Constants.DAY_IN_MS);
|
||||
}
|
||||
|
||||
public boolean isDaysPeriod() {
|
||||
return (period >= Constants.DAY_IN_MS) && (period < (Constants.WEEK_IN_MS * 2));
|
||||
}
|
||||
|
||||
public boolean isWeeksPeriod() {
|
||||
return (period >= (2 * Constants.WEEK_IN_MS));
|
||||
}
|
||||
|
||||
public long periodInUnits() {
|
||||
if (isDaysPeriod()) return period / Constants.DAY_IN_MS;
|
||||
if (isHoursPeriod()) return period / Constants.HOUR_IN_MS;
|
||||
if (isWeeksPeriod()) return period / Constants.WEEK_IN_MS;
|
||||
return -1; // ERROR
|
||||
}
|
||||
|
||||
public boolean isDue() {
|
||||
if ((enabled) && (next_due <= JoH.tsl())) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSnoozed() {
|
||||
if (snoozed_till > JoH.tsl()) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean shouldNotify() {
|
||||
return enabled && isDue() && !isSnoozed();
|
||||
}
|
||||
|
||||
public synchronized void notified() {
|
||||
if (last_fired < next_due) fired_times++;
|
||||
alerted_times++;
|
||||
last_fired = JoH.tsl();
|
||||
if (chime_only) {
|
||||
if (repeating) {
|
||||
UserError.Log.d(TAG, "Rescheduling next");
|
||||
schedule_next();
|
||||
} else {
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
save();
|
||||
}
|
||||
|
||||
public synchronized void reminder_alert() {
|
||||
Reminders.doAlert(this);
|
||||
}
|
||||
|
||||
public synchronized void schedule_next() {
|
||||
this.next_due = this.next_due + this.period;
|
||||
// check it is actually in the future
|
||||
while (this.next_due < JoH.tsl()) {
|
||||
this.next_due = this.next_due + this.period;
|
||||
}
|
||||
if (alternating) alternate = !alternate;
|
||||
alerted_times = 0; // reset counter
|
||||
save();
|
||||
}
|
||||
|
||||
protected synchronized static void fixUpTable(String[] schema) {
|
||||
if (patched) return;
|
||||
for (String patch : schema) {
|
||||
try {
|
||||
SQLiteUtils.execSql(patch);
|
||||
} catch (Exception e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
patched = true;
|
||||
}
|
||||
|
||||
public static Reminder create(String title, long period) {
|
||||
fixUpTable(schema);
|
||||
Reminder reminder = new Reminder();
|
||||
reminder.title = title;
|
||||
reminder.alternate_title = title + " alternate";
|
||||
reminder.period = period;
|
||||
reminder.next_due = JoH.tsl() + period;
|
||||
reminder.enabled = true;
|
||||
reminder.snoozed_till = 0;
|
||||
reminder.last_snoozed_for = 0;
|
||||
reminder.last_fired = 0;
|
||||
reminder.fired_times = 0;
|
||||
reminder.repeating = true;
|
||||
reminder.alternating = false;
|
||||
reminder.alternate = false;
|
||||
reminder.chime_only = false;
|
||||
reminder.homeonly = false;
|
||||
reminder.ideal_time = JoH.hourMinuteString();
|
||||
reminder.priority = 5; // default
|
||||
reminder.save();
|
||||
return reminder;
|
||||
}
|
||||
|
||||
public static List<Reminder> getActiveReminders() {
|
||||
fixUpTable(schema);
|
||||
final long now = JoH.tsl();
|
||||
final List<Reminder> reminders = new Select()
|
||||
.from(Reminder.class)
|
||||
.where("enabled = ?", true)
|
||||
.where("next_due < ?", now)
|
||||
.where("snoozed_till < ?", now)
|
||||
.orderBy("enabled desc, next_due asc")
|
||||
.execute();
|
||||
return reminders;
|
||||
}
|
||||
|
||||
private static boolean isNight() {
|
||||
final int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
|
||||
return hour < 9; // midnight to 9am we say is night
|
||||
}
|
||||
|
||||
public static synchronized void processAnyDueReminders() {
|
||||
if (JoH.quietratelimit("reminder_due_check", 10)) {
|
||||
if (!Pref.getBooleanDefaultFalse(REMINDERS_ALL_DISABLED)
|
||||
&& (!Pref.getBooleanDefaultFalse(REMINDERS_NIGHT_DISABLED) || !isNight())) {
|
||||
final Reminder due_reminder = getNextActiveReminder();
|
||||
if (due_reminder != null) {
|
||||
UserError.Log.d(TAG, "Found due reminder! " + due_reminder.title);
|
||||
due_reminder.reminder_alert();
|
||||
}
|
||||
} else {
|
||||
// reminders are disabled - should we re-enable them?
|
||||
if (Pref.getBooleanDefaultFalse(REMINDERS_RESTART_TOMORROW)) {
|
||||
// temporary testing logic
|
||||
final int hour = Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
|
||||
if (hour == 10) {
|
||||
if (JoH.pratelimit("restart-reminders", 7200)) {
|
||||
UserError.Log.d(TAG, "Re-enabling reminders as its morning time");
|
||||
Pref.setBoolean(REMINDERS_ALL_DISABLED, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Reminder getNextActiveReminder() {
|
||||
fixUpTable(schema);
|
||||
final boolean onHomeWifi = !HomeWifi.isSet() || HomeWifi.isConnected();
|
||||
final long now = JoH.tsl();
|
||||
final Reminder reminder = new Select()
|
||||
.from(Reminder.class)
|
||||
.where("enabled = ?", true)
|
||||
.where("next_due < ?", now)
|
||||
.where("snoozed_till < ?", now)
|
||||
.where("last_fired < (? - (600000 * alerted_times))", now)
|
||||
// if on home wifi or not set then anything otherwise only home only = false
|
||||
.where(onHomeWifi ? "homeonly > -1 " : "homeonly = 0")
|
||||
.orderBy("enabled desc, priority desc, next_due asc")
|
||||
.executeSingle();
|
||||
return reminder;
|
||||
}
|
||||
|
||||
public static List<Reminder> getAllReminders() {
|
||||
fixUpTable(schema);
|
||||
final List<Reminder> reminders = new Select()
|
||||
.from(Reminder.class)
|
||||
.orderBy("enabled desc, priority desc, next_due asc")
|
||||
.execute();
|
||||
return reminders;
|
||||
}
|
||||
|
||||
public synchronized static void firstInit(Context context) {
|
||||
fixUpTable(schema);
|
||||
/* Inevitable.task("reminders-first-init", 2000, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
final Reminder reminder = new Select()
|
||||
.from(Reminder.class)
|
||||
.where("enabled = ?", true)
|
||||
.executeSingle();
|
||||
if (reminder != null) {
|
||||
// PendingIntent serviceIntent = PendingIntent.getService(xdrip.getAppContext(), 0, new Intent(xdrip.getAppContext(), MissedReadingService.class), PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
// PendingIntent serviceIntent = WakeLockTrampoline.getPendingIntent(MissedReadingService.class);
|
||||
// JoH.wakeUpIntent(xdrip.getAppContext(), Constants.MINUTE_IN_MS, serviceIntent);
|
||||
// UserError.Log.ueh(TAG, "Starting missed readings service");
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
UserError.Log.wtf(TAG, "Got nasty initial concurrency exception: " + e);
|
||||
}
|
||||
}
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
public static Reminder byid(long id) {
|
||||
return new Select()
|
||||
.from(Reminder.class)
|
||||
.where("_ID = ?", id)
|
||||
.executeSingle();
|
||||
}
|
||||
|
||||
|
||||
/*static Reminder getForPreciseTimestamp(double timestamp, double precision, String plugin) {
|
||||
fixUpTable(schema);
|
||||
final Reminder Reminder = new Select()
|
||||
.from(Reminder.class)
|
||||
.where("timestamp <= ?", (timestamp + precision))
|
||||
.where("timestamp >= ?", (timestamp - precision))
|
||||
.where("plugin = ?", plugin)
|
||||
.orderBy("abs(timestamp - " + timestamp + ") asc")
|
||||
.executeSingle();
|
||||
if (Reminder != null && Math.abs(Reminder.timestamp - timestamp) < precision) {
|
||||
return Reminder;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
*/
|
||||
}
|
||||
309
lib/nightscout/com/eveningoutpost/dexdrip/Models/RollCall.java
Normal file
309
lib/nightscout/com/eveningoutpost/dexdrip/Models/RollCall.java
Normal file
@@ -0,0 +1,309 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Build;
|
||||
|
||||
import com.eveningoutpost.dexdrip.GcmActivity;
|
||||
import com.eveningoutpost.dexdrip.Home;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.BridgeBattery;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.StatusItem;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.desertsync.RouteTools;
|
||||
import com.eveningoutpost.dexdrip.utils.CipherUtils;
|
||||
import com.eveningoutpost.dexdrip.xdrip;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.annotations.Expose;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.eveningoutpost.dexdrip.Models.JoH.emptyString;
|
||||
|
||||
/**
|
||||
* Created by jamorham on 20/01/2017.
|
||||
*/
|
||||
|
||||
public class RollCall {
|
||||
|
||||
private static final String TAG = "RollCall";
|
||||
private static final int MAX_SSID_LENGTH = 20;
|
||||
private static volatile HashMap<String, RollCall> indexed;
|
||||
|
||||
@Expose
|
||||
String device_manufactuer;
|
||||
@Expose
|
||||
String device_model;
|
||||
@Expose
|
||||
String device_serial;
|
||||
@Expose
|
||||
String device_name;
|
||||
@Expose
|
||||
String android_version;
|
||||
@Expose
|
||||
String xdrip_version;
|
||||
@Expose
|
||||
String role;
|
||||
@Expose
|
||||
String ssid;
|
||||
@Expose
|
||||
String mhint;
|
||||
@Expose
|
||||
int battery = -1;
|
||||
@Expose
|
||||
int bridge_battery = -1;
|
||||
|
||||
// not set by instantiation
|
||||
@Expose
|
||||
String hash;
|
||||
@Expose
|
||||
Long last_seen;
|
||||
|
||||
final long created = JoH.tsl();
|
||||
|
||||
public RollCall() {
|
||||
this.device_manufactuer = Build.MANUFACTURER;
|
||||
this.device_model = Build.MODEL;
|
||||
this.device_name = JoH.getLocalBluetoothName(); // sanity check length
|
||||
this.device_serial = Build.SERIAL;
|
||||
this.android_version = Build.VERSION.RELEASE;
|
||||
this.xdrip_version = JoH.getVersionDetails();
|
||||
|
||||
if (Home.get_follower()) {
|
||||
this.role = "Follower";
|
||||
} else if (Home.get_master()) {
|
||||
this.role = "Master";
|
||||
} else {
|
||||
this.role = "None";
|
||||
}
|
||||
|
||||
if (DesertSync.isEnabled()) {
|
||||
try {
|
||||
this.ssid = wifiString();
|
||||
} catch (Exception e) {
|
||||
//
|
||||
}
|
||||
try {
|
||||
if (this.role.equals("Master")) {
|
||||
this.mhint = RouteTools.getBestInterfaceAddress();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
//
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// populate with values from this device
|
||||
public RollCall populate() {
|
||||
this.battery = getBatteryLevel();
|
||||
this.bridge_battery = BridgeBattery.getBestBridgeBattery();
|
||||
return this;
|
||||
}
|
||||
|
||||
private boolean batteryValid() {
|
||||
return battery != -1;
|
||||
}
|
||||
|
||||
private boolean bridgeBatteryValid() {
|
||||
return bridge_battery > 0;
|
||||
}
|
||||
|
||||
private static String wifiString() {
|
||||
String ssid = JoH.getWifiSSID();
|
||||
if (ssid != null && ssid.length() > MAX_SSID_LENGTH) {
|
||||
ssid = ssid.substring(0, 20);
|
||||
}
|
||||
return ssid;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private static int getBatteryLevel() {
|
||||
final Intent batteryIntent = xdrip.getAppContext().registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
|
||||
try {
|
||||
final int level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
|
||||
final int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
|
||||
if (level == -1 || scale == -1) {
|
||||
return -1;
|
||||
}
|
||||
return (int) (((float) level / (float) scale) * 100.0f);
|
||||
} catch (NullPointerException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private String getRemoteIpStatus() {
|
||||
if (mhint != null) {
|
||||
return "\n" + mhint;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String getRemoteWifiIndicate(final String our_wifi_ssid) {
|
||||
if (emptyString(our_wifi_ssid)) return "";
|
||||
if (emptyString(ssid)) return "";
|
||||
if (!our_wifi_ssid.equals(ssid)) return "\n" + ssid;
|
||||
return "";
|
||||
}
|
||||
|
||||
public String toS() {
|
||||
final Gson gson = new GsonBuilder()
|
||||
.excludeFieldsWithoutExposeAnnotation()
|
||||
.create();
|
||||
return gson.toJson(this);
|
||||
}
|
||||
|
||||
public String getHash() {
|
||||
if (this.hash == null) {
|
||||
this.hash = CipherUtils.getSHA256(this.device_manufactuer + this.android_version + this.device_model + this.device_serial);
|
||||
}
|
||||
return this.hash;
|
||||
}
|
||||
|
||||
public String bestName() {
|
||||
if ((device_name != null) && (device_name.length() > 2)) {
|
||||
return device_name;
|
||||
}
|
||||
return (!device_manufactuer.equals("unknown") ? device_manufactuer + " " : "") + device_model;
|
||||
}
|
||||
|
||||
public static RollCall fromJson(String json) {
|
||||
final Gson gson = new GsonBuilder()
|
||||
.excludeFieldsWithoutExposeAnnotation()
|
||||
.create();
|
||||
try {
|
||||
return gson.fromJson(json, RollCall.class);
|
||||
} catch (Exception e) {
|
||||
UserError.Log.e(TAG, "Got exception processing fromJson() " + e);
|
||||
UserError.Log.e(TAG, "json = " + json);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized static void Seen(String item_json) {
|
||||
try {
|
||||
UserError.Log.d(TAG, "Processing Seen: " + item_json);
|
||||
Seen(fromJson(item_json));
|
||||
} catch (Exception e) {
|
||||
UserError.Log.e(TAG, "Got exception processing Seen() " + e);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized static void Seen(RollCall item) {
|
||||
// sanity check object contains some data
|
||||
if (item == null) return;
|
||||
if ((item.android_version == null) || (item.android_version.length() == 0)) return;
|
||||
if (indexed == null) loadIndex();
|
||||
indexed.put(item.getHash(), item);
|
||||
item.last_seen = JoH.tsl();
|
||||
saveIndex();
|
||||
}
|
||||
|
||||
private static final String ROLLCALL_SAVED_INDEX = "RollCall-saved-index";
|
||||
|
||||
private static void saveIndex() {
|
||||
final Gson gson = new GsonBuilder().create();
|
||||
final String[] array = new String[indexed.size()];
|
||||
int i = 0;
|
||||
for (Map.Entry entry : indexed.entrySet()) {
|
||||
array[i++] = (((RollCall) entry.getValue()).toS());
|
||||
}
|
||||
PersistentStore.setString(ROLLCALL_SAVED_INDEX, gson.toJson(array));
|
||||
UserError.Log.d(TAG, "Saving");
|
||||
}
|
||||
|
||||
private synchronized static void loadIndex() {
|
||||
UserError.Log.d(TAG, "Loading index");
|
||||
final String loaded = PersistentStore.getString(ROLLCALL_SAVED_INDEX);
|
||||
final HashMap<String, RollCall> hashmap = new HashMap<>();
|
||||
try {
|
||||
if ((loaded != null) && (loaded.length() > 0)) {
|
||||
final Gson gson = new GsonBuilder().create();
|
||||
final String[] array = gson.fromJson(loaded, String[].class);
|
||||
if (array != null) {
|
||||
for (String json : array) {
|
||||
RollCall item = gson.fromJson(json, RollCall.class);
|
||||
hashmap.put(item.getHash(), item);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
UserError.Log.e(TAG, "Error loading index: " + e);
|
||||
}
|
||||
indexed = hashmap;
|
||||
UserError.Log.d(TAG, "Loaded: count: " + hashmap.size());
|
||||
}
|
||||
|
||||
public static String getBestMasterHintIP() {
|
||||
// TODO some intelligence regarding wifi ssid
|
||||
//final String our_wifi_ssid = wifiString();
|
||||
if (indexed == null) loadIndex();
|
||||
RollCall bestMatch = null;
|
||||
for (Map.Entry entry : indexed.entrySet()) {
|
||||
final RollCall rc = (RollCall) entry.getValue();
|
||||
if (!rc.role.equals("Master")) continue;
|
||||
if (emptyString(rc.mhint)) continue;
|
||||
if (bestMatch == null || rc.last_seen > bestMatch.last_seen) {
|
||||
bestMatch = rc;
|
||||
}
|
||||
}
|
||||
UserError.Log.d(TAG, "Returning best master hint ip: " + (bestMatch != null ? bestMatch.toS() : "no match"));
|
||||
return bestMatch != null ? bestMatch.mhint : null;
|
||||
}
|
||||
|
||||
|
||||
public static void pruneOld(int depth) {
|
||||
if (indexed == null) loadIndex();
|
||||
if (depth > 10) return;
|
||||
boolean changed = false;
|
||||
for (Map.Entry entry : indexed.entrySet()) {
|
||||
RollCall rc = (RollCall) entry.getValue();
|
||||
long since = JoH.msSince(rc.last_seen);
|
||||
|
||||
if ((since < 0) || (since > (1000 * 60 * 60 * 24))) {
|
||||
UserError.Log.d(TAG, "Pruning entry: " + rc.bestName());
|
||||
indexed.remove(entry.getKey().toString());
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
saveIndex();
|
||||
pruneOld(depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// data for MegaStatus
|
||||
public static List<StatusItem> megaStatus() {
|
||||
if (indexed == null) loadIndex();
|
||||
GcmActivity.requestRollCall();
|
||||
// TODO sort data
|
||||
final boolean engineering = Home.get_engineering_mode();
|
||||
final boolean desert_sync = DesertSync.isEnabled();
|
||||
final String our_wifi_ssid = desert_sync ? wifiString() : "";
|
||||
final List<StatusItem> lf = new ArrayList<>();
|
||||
for (Map.Entry entry : indexed.entrySet()) {
|
||||
final RollCall rc = (RollCall) entry.getValue();
|
||||
// TODO refactor with stringbuilder
|
||||
lf.add(new StatusItem(rc.role + (desert_sync ? rc.getRemoteWifiIndicate(our_wifi_ssid) : "") + (engineering ? ("\n" + JoH.niceTimeSince(rc.last_seen) + " ago") : ""), rc.bestName() + (desert_sync ? rc.getRemoteIpStatus() : "") + (engineering && rc.batteryValid() ? ("\n" + rc.battery + "%") : "") + (engineering && rc.bridgeBatteryValid() ? (" " + rc.bridge_battery+"%") : "")));
|
||||
}
|
||||
|
||||
Collections.sort(lf, new Comparator<StatusItem>() {
|
||||
public int compare(StatusItem left, StatusItem right) {
|
||||
int val = right.name.replaceFirst("\n.*$", "").compareTo(left.name.replaceFirst("\n.*$", "")); // descending sort ignore second line
|
||||
if (val == 0) val = left.value.compareTo(right.value); // ascending sort
|
||||
return val;
|
||||
}
|
||||
});
|
||||
// TODO could scan for duplicates and append serial to bestName
|
||||
|
||||
return new ArrayList<>(lf);
|
||||
}
|
||||
|
||||
}
|
||||
301
lib/nightscout/com/eveningoutpost/dexdrip/Models/Sensor.java
Normal file
301
lib/nightscout/com/eveningoutpost/dexdrip/Models/Sensor.java
Normal file
@@ -0,0 +1,301 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.activeandroid.Model;
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Select;
|
||||
import com.eveningoutpost.dexdrip.GcmActivity;
|
||||
import com.eveningoutpost.dexdrip.Home;
|
||||
import com.eveningoutpost.dexdrip.Models.UserError.Log;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.SensorSendQueue;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.internal.bind.DateTypeAdapter;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Created by Emma Black on 10/29/14.
|
||||
*/
|
||||
|
||||
@Table(name = "Sensors", id = BaseColumns._ID)
|
||||
public class Sensor extends Model {
|
||||
private final static String TAG = Sensor.class.getSimpleName();
|
||||
|
||||
@Expose
|
||||
@Column(name = "started_at", index = true)
|
||||
public long started_at;
|
||||
|
||||
@Expose
|
||||
@Column(name = "stopped_at")
|
||||
public long stopped_at;
|
||||
|
||||
@Expose
|
||||
//latest minimal battery level
|
||||
@Column(name = "latest_battery_level")
|
||||
public int latest_battery_level;
|
||||
|
||||
@Expose
|
||||
@Column(name = "uuid", index = true)
|
||||
public String uuid;
|
||||
|
||||
@Expose
|
||||
@Column(name = "sensor_location")
|
||||
public String sensor_location;
|
||||
|
||||
public static Sensor create(long started_at) {
|
||||
Sensor sensor = new Sensor();
|
||||
sensor.started_at = started_at;
|
||||
sensor.uuid = UUID.randomUUID().toString();
|
||||
|
||||
sensor.save();
|
||||
SensorSendQueue.addToQueue(sensor);
|
||||
Log.d("SENSOR MODEL:", sensor.toString());
|
||||
return sensor;
|
||||
}
|
||||
|
||||
public static Sensor create(long started_at, String uuid) {//KS
|
||||
Sensor sensor = new Sensor();
|
||||
sensor.started_at = started_at;
|
||||
sensor.uuid = uuid;
|
||||
|
||||
sensor.save();
|
||||
SensorSendQueue.addToQueue(sensor);
|
||||
Log.d("SENSOR MODEL:", sensor.toString());
|
||||
return sensor;
|
||||
}
|
||||
|
||||
public static Sensor createDefaultIfMissing() {
|
||||
final Sensor sensor = currentSensor();
|
||||
if (sensor == null) {
|
||||
Sensor.create(JoH.tsl());
|
||||
}
|
||||
return currentSensor();
|
||||
}
|
||||
|
||||
// Used by xDripViewer
|
||||
public static void createUpdate(long started_at, long stopped_at, int latest_battery_level, String uuid) {
|
||||
|
||||
Sensor sensor = getByTimestamp(started_at);
|
||||
if (sensor != null) {
|
||||
Log.d("SENSOR", "updatinga an existing sensor");
|
||||
} else {
|
||||
Log.d("SENSOR", "creating a new sensor");
|
||||
sensor = new Sensor();
|
||||
}
|
||||
sensor.started_at = started_at;
|
||||
sensor.stopped_at = stopped_at;
|
||||
sensor.latest_battery_level = latest_battery_level;
|
||||
sensor.uuid = uuid;
|
||||
sensor.save();
|
||||
}
|
||||
|
||||
public synchronized static void stopSensor() {
|
||||
final Sensor sensor = currentSensor();
|
||||
if (sensor == null) {
|
||||
return;
|
||||
}
|
||||
sensor.stopped_at = JoH.tsl();
|
||||
UserError.Log.ueh("SENSOR", "Sensor stopped at " + JoH.dateTimeText(sensor.stopped_at));
|
||||
sensor.save();
|
||||
if (currentSensor() != null) {
|
||||
UserError.Log.wtf(TAG, "Failed to update sensor stop in database");
|
||||
}
|
||||
SensorSendQueue.addToQueue(sensor);
|
||||
JoH.clearCache();
|
||||
|
||||
}
|
||||
|
||||
public String toS() {//KS
|
||||
Gson gson = new GsonBuilder()
|
||||
.excludeFieldsWithoutExposeAnnotation()
|
||||
.registerTypeAdapter(Date.class, new DateTypeAdapter())
|
||||
.serializeSpecialFloatingPointValues()
|
||||
.create();
|
||||
Log.d("SENSOR", "Sensor toS uuid=" + this.uuid + " started_at=" + this.started_at + " active=" + this.isActive() + " battery=" + this.latest_battery_level + " location=" + this.sensor_location + " stopped_at=" + this.stopped_at);
|
||||
return gson.toJson(this);
|
||||
}
|
||||
|
||||
public static Sensor lastStopped() {
|
||||
Sensor sensor = new Select()
|
||||
.from(Sensor.class)
|
||||
.where("started_at != 0")
|
||||
.where("stopped_at != 0")
|
||||
.orderBy("_ID desc")
|
||||
.limit(1)
|
||||
.executeSingle();
|
||||
return sensor;
|
||||
}
|
||||
|
||||
public static boolean stoppedRecently() {
|
||||
final Sensor last = lastStopped();
|
||||
return last != null && last.stopped_at < JoH.tsl() && (JoH.msSince(last.stopped_at) < (Constants.HOUR_IN_MS * 2));
|
||||
}
|
||||
|
||||
public static Sensor currentSensor() {
|
||||
Sensor sensor = new Select()
|
||||
.from(Sensor.class)
|
||||
.where("started_at != 0")
|
||||
.where("stopped_at = 0")
|
||||
.orderBy("_ID desc")
|
||||
.limit(1)
|
||||
.executeSingle();
|
||||
return sensor;
|
||||
}
|
||||
|
||||
public static boolean isActive() {
|
||||
Sensor sensor = new Select()
|
||||
.from(Sensor.class)
|
||||
.where("started_at != 0")
|
||||
.where("stopped_at = 0")
|
||||
.orderBy("_ID desc")
|
||||
.limit(1)
|
||||
.executeSingle();
|
||||
if(sensor == null) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static Sensor getByTimestamp(double started_at) {
|
||||
return new Select()
|
||||
.from(Sensor.class)
|
||||
.where("started_at = ?", started_at)
|
||||
.executeSingle();
|
||||
}
|
||||
|
||||
public static Sensor getByUuid(String xDrip_sensor_uuid) {
|
||||
if(xDrip_sensor_uuid == null) {
|
||||
Log.e("SENSOR", "xDrip_sensor_uuid is null");
|
||||
return null;
|
||||
}
|
||||
Log.d("SENSOR", "xDrip_sensor_uuid is " + xDrip_sensor_uuid);
|
||||
|
||||
return new Select()
|
||||
.from(Sensor.class)
|
||||
.where("uuid = ?", xDrip_sensor_uuid)
|
||||
.executeSingle();
|
||||
}
|
||||
|
||||
public static void updateBatteryLevel(int sensorBatteryLevel, boolean from_sync) {
|
||||
Sensor sensor = Sensor.currentSensor();
|
||||
if (sensor == null)
|
||||
{
|
||||
Log.d("Sensor","Cant sync battery level from master as sensor data is null");
|
||||
return;
|
||||
}
|
||||
updateBatteryLevel(sensor, sensorBatteryLevel, from_sync);
|
||||
}
|
||||
|
||||
public static void updateBatteryLevel(Sensor sensor, int sensorBatteryLevel) {
|
||||
updateBatteryLevel(sensor, sensorBatteryLevel, false);
|
||||
}
|
||||
|
||||
public static void updateBatteryLevel(Sensor sensor, int sensorBatteryLevel, boolean from_sync) {
|
||||
if (sensorBatteryLevel < 120) {
|
||||
// This must be a wrong battery level. Some transmitter send those every couple of readings
|
||||
// even if the battery is ok.
|
||||
return;
|
||||
}
|
||||
int startBatteryLevel = sensor.latest_battery_level;
|
||||
// if(sensor.latest_battery_level == 0) {
|
||||
// allow sensor battery level to go up and down
|
||||
sensor.latest_battery_level = sensorBatteryLevel;
|
||||
// } else {
|
||||
// sensor.latest_battery_level = Math.min(sensor.latest_battery_level, sensorBatteryLevel);
|
||||
// }
|
||||
if (startBatteryLevel == sensor.latest_battery_level) {
|
||||
// no need to update anything if nothing has changed.
|
||||
return;
|
||||
}
|
||||
sensor.save();
|
||||
SensorSendQueue.addToQueue(sensor);
|
||||
if ((!from_sync) && (Home.get_master())) {
|
||||
GcmActivity.sendSensorBattery(sensor.latest_battery_level);
|
||||
}
|
||||
}
|
||||
|
||||
public static void updateSensorLocation(String sensor_location) {
|
||||
Sensor sensor = currentSensor();
|
||||
if (sensor == null) {
|
||||
Log.e("SENSOR MODEL:", "updateSensorLocation called but sensor is null");
|
||||
return;
|
||||
}
|
||||
sensor.sensor_location = sensor_location;
|
||||
sensor.save();
|
||||
}
|
||||
|
||||
public static void upsertFromMaster(Sensor jsonSensor) {
|
||||
if (jsonSensor == null) {
|
||||
Log.wtf(TAG,"Got null sensor from json");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Sensor existingSensor = getByUuid(jsonSensor.uuid);
|
||||
if (existingSensor == null) {
|
||||
Log.d(TAG, "saving new sensor record.");
|
||||
jsonSensor.save();
|
||||
} else {
|
||||
Log.d(TAG, "updating existing sensor record.");
|
||||
existingSensor.started_at = jsonSensor.started_at;
|
||||
existingSensor.stopped_at = jsonSensor.stopped_at;
|
||||
existingSensor.latest_battery_level = jsonSensor.latest_battery_level;
|
||||
existingSensor.sensor_location = jsonSensor.sensor_location;
|
||||
existingSensor.save();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Could not save Sensor: " + e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public String toJSON() {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
try {
|
||||
jsonObject.put("started_at", started_at);
|
||||
jsonObject.put("stopped_at", stopped_at);
|
||||
jsonObject.put("latest_battery_level", latest_battery_level);
|
||||
jsonObject.put("uuid", uuid);
|
||||
jsonObject.put("sensor_location", sensor_location);
|
||||
return jsonObject.toString();
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG,"Got JSONException handeling sensor", e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public static Sensor fromJSON(String json) {
|
||||
if (json.length()==0) {
|
||||
Log.d(TAG,"Empty json received in Sensor fromJson");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Log.d(TAG, "Processing incoming json: " + json);
|
||||
return new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create().fromJson(json,Sensor.class);
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "Got exception parsing Sensor json: " + e.toString());
|
||||
Home.toaststaticnext("Error on Sensor sync.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void shutdownAllSensors() {
|
||||
final List<Sensor> l = new Select().from(Sensor.class).execute();
|
||||
for (final Sensor s : l) {
|
||||
s.stopped_at = s.started_at;
|
||||
s.save();
|
||||
System.out.println(s.toJSON());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
|
||||
import com.eveningoutpost.dexdrip.utils.DexCollectionType;
|
||||
|
||||
/**
|
||||
* Created by jamorham on 02/03/2018.
|
||||
*
|
||||
* Checks for whether sensor data is within a sane range
|
||||
*
|
||||
*/
|
||||
|
||||
public class SensorSanity {
|
||||
|
||||
public static final double DEXCOM_MIN_RAW = 5; // raw values below this will be treated as error
|
||||
public static final double DEXCOM_MAX_RAW = 1000; // raw values above this will be treated as error
|
||||
|
||||
public static final double DEXCOM_G6_MIN_RAW = 5; // raw values below this will be treated as error
|
||||
public static final double DEXCOM_G6_MAX_RAW = 1000; // raw values above this will be treated as error
|
||||
|
||||
public static final double LIBRE_MIN_RAW = 5; // raw values below this will be treated as error
|
||||
|
||||
private static final String TAG = "SensorSanity";
|
||||
|
||||
public static boolean isRawValueSane(double raw_value) {
|
||||
return isRawValueSane(raw_value, DexCollectionType.getDexCollectionType(), false);
|
||||
}
|
||||
|
||||
public static boolean isRawValueSane(double raw_value, boolean hard) {
|
||||
return isRawValueSane(raw_value, DexCollectionType.getDexCollectionType(), hard);
|
||||
}
|
||||
|
||||
public static boolean isRawValueSane(double raw_value, DexCollectionType type) {
|
||||
return isRawValueSane(raw_value, type, false);
|
||||
|
||||
}
|
||||
|
||||
public static boolean isRawValueSane(double raw_value, DexCollectionType type, boolean hard_check) {
|
||||
|
||||
// bypass checks if the allowing dead sensor engineering mode is enabled
|
||||
if (allowTestingWithDeadSensor()) {
|
||||
if (JoH.pratelimit("dead-sensor-sanity-passing", 3600)) {
|
||||
UserError.Log.e(TAG, "Allowing any value due to Allow Dead Sensor being enabled");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// passes by default!
|
||||
boolean state = true;
|
||||
|
||||
// checks for each type of data source
|
||||
|
||||
if (DexCollectionType.hasDexcomRaw(type)) {
|
||||
if (!BgReading.isRawMarkerValue(raw_value) || hard_check) {
|
||||
if (Pref.getBooleanDefaultFalse("using_g6")) {
|
||||
if (raw_value < DEXCOM_G6_MIN_RAW) state = false;
|
||||
else if (raw_value > DEXCOM_G6_MAX_RAW) state = false;
|
||||
} else {
|
||||
if (raw_value < DEXCOM_MIN_RAW) state = false;
|
||||
else if (raw_value > DEXCOM_MAX_RAW) state = false;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (DexCollectionType.hasLibre(type)) {
|
||||
if (raw_value < LIBRE_MIN_RAW) state = false;
|
||||
} else if (type == DexCollectionType.Medtrum) {
|
||||
if (raw_value < DEXCOM_MIN_RAW) state = false;
|
||||
else if (raw_value > DEXCOM_MAX_RAW) state = false;
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
if (JoH.ratelimit("sanity-failure", 20)) {
|
||||
final String msg = "Sensor Raw Data Sanity Failure: " + raw_value;
|
||||
UserError.Log.e(TAG, msg);
|
||||
JoH.static_toast_long(msg);
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
public static boolean allowTestingWithDeadSensor() {
|
||||
return Pref.getBooleanDefaultFalse("allow_testing_with_dead_sensor")
|
||||
&& Pref.getBooleanDefaultFalse("engineering_mode");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check Libre serial for unexpected changes. Stop xDrip sensor session if there is a mismatch.
|
||||
*
|
||||
* Same sensor session with different sensor serial number results in error response
|
||||
*
|
||||
* boolean return of true indicates problem
|
||||
*/
|
||||
|
||||
private static final String PREF_LIBRE_SN = "SensorSanity-LibreSN";
|
||||
private static final String PREF_LIBRE_SENSOR_UUID = "SensorSanity-LibreSensor";
|
||||
|
||||
public static boolean checkLibreSensorChangeIfEnabled(final String sn) {
|
||||
return Pref.getBoolean("detect_libre_sn_changes", true) && checkLibreSensorChange(sn);
|
||||
}
|
||||
|
||||
public synchronized static boolean checkLibreSensorChange(final String currentSerial) {
|
||||
if ((currentSerial == null) || currentSerial.length() < 4) return false;
|
||||
final String lastSn = PersistentStore.getString(PREF_LIBRE_SN);
|
||||
if (!currentSerial.equals(lastSn)) {
|
||||
final Sensor this_sensor = Sensor.currentSensor();
|
||||
if ((lastSn.length() > 3) && (this_sensor != null)) {
|
||||
|
||||
final String last_uuid = PersistentStore.getString(PREF_LIBRE_SENSOR_UUID);
|
||||
|
||||
if (last_uuid.equals(this_sensor.uuid)) {
|
||||
if (last_uuid.length() > 3) {
|
||||
UserError.Log.wtf(TAG, String.format("Different sensor serial number for same sensor uuid: %s :: %s vs %s", last_uuid, lastSn, currentSerial));
|
||||
Sensor.stopSensor();
|
||||
JoH.static_toast_long("Stopping sensor due to serial number change");
|
||||
Sensor.stopSensor();
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
PersistentStore.setString(PREF_LIBRE_SENSOR_UUID, this_sensor.uuid);
|
||||
}
|
||||
}
|
||||
PersistentStore.setString(PREF_LIBRE_SN, currentSerial);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
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.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.annotations.Expose;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by jamorham on 01/11/2016.
|
||||
*/
|
||||
|
||||
|
||||
@Table(name = "PebbleMovement", id = BaseColumns._ID)
|
||||
public class StepCounter extends Model {
|
||||
|
||||
private static boolean patched = false;
|
||||
private final static String TAG = "StepCounter";
|
||||
private final static boolean d = false;
|
||||
|
||||
@Expose
|
||||
@Column(name = "timestamp", unique = true, onUniqueConflicts = Column.ConflictAction.IGNORE)
|
||||
public long timestamp;
|
||||
|
||||
@Expose
|
||||
@Column(name = "metric")
|
||||
public int metric;
|
||||
|
||||
|
||||
// patches and saves
|
||||
public Long saveit() {
|
||||
try {
|
||||
fixUpTable();
|
||||
return save();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String toS() {
|
||||
final Gson gson = new GsonBuilder()
|
||||
.excludeFieldsWithoutExposeAnnotation()
|
||||
.create();
|
||||
return gson.toJson(this);
|
||||
}
|
||||
|
||||
|
||||
// static methods
|
||||
|
||||
public static StepCounter createEfficientRecord(long timestamp_ms, int data)
|
||||
{
|
||||
StepCounter pm = last();
|
||||
if ((pm == null) || (data < pm.metric) || ((timestamp_ms - pm.timestamp) > (1000 * 30 * 5))) {
|
||||
pm = new StepCounter();
|
||||
pm.timestamp = timestamp_ms;
|
||||
if (d) UserError.Log.d(TAG,"Creating new record for timestamp: "+JoH.dateTimeText(timestamp_ms));
|
||||
} else {
|
||||
if (d) UserError.Log.d(TAG,"Merging pebble movement record: "+JoH.dateTimeText(timestamp_ms)+" vs old "+JoH.dateTimeText(pm.timestamp));
|
||||
}
|
||||
|
||||
pm.metric = (int) (long) data;
|
||||
if(d) UserError.Log.d(TAG, "Saving Movement: " + pm.toS());
|
||||
pm.saveit();
|
||||
return pm;
|
||||
}
|
||||
|
||||
public static StepCounter last() {
|
||||
try {
|
||||
return new Select()
|
||||
.from(StepCounter.class)
|
||||
.orderBy("timestamp desc")
|
||||
.executeSingle();
|
||||
} catch (android.database.sqlite.SQLiteException e) {
|
||||
fixUpTable();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<StepCounter> latestForGraph(int number, double startTime) {
|
||||
return latestForGraph(number, (long) startTime, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static List<StepCounter> latestForGraph(int number, long startTime) {
|
||||
return latestForGraph(number, startTime, Long.MAX_VALUE);
|
||||
}
|
||||
|
||||
public static List<StepCounter> latestForGraph(int number, long startTime, long endTime) {
|
||||
try {
|
||||
return new Select()
|
||||
.from(StepCounter.class)
|
||||
.where("timestamp >= " + Math.max(startTime, 0))
|
||||
.where("timestamp <= " + endTime)
|
||||
.orderBy("timestamp asc") // warn asc!
|
||||
.limit(number)
|
||||
.execute();
|
||||
} catch (android.database.sqlite.SQLiteException e) {
|
||||
fixUpTable();
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
// expects pre-sorted in asc order?
|
||||
public static List<StepCounter> deltaListFromMovementList(List<StepCounter> mList) {
|
||||
int last_metric = -1;
|
||||
int temp_metric = -1;
|
||||
for (StepCounter pm : mList) {
|
||||
// first item in list
|
||||
if (last_metric == -1) {
|
||||
last_metric = pm.metric;
|
||||
pm.metric = 0;
|
||||
} else {
|
||||
// normal incrementing calculate delta
|
||||
if (pm.metric >= last_metric) {
|
||||
temp_metric = pm.metric - last_metric;
|
||||
last_metric = pm.metric;
|
||||
pm.metric = temp_metric;
|
||||
} else {
|
||||
last_metric = pm.metric;
|
||||
}
|
||||
}
|
||||
}
|
||||
return mList;
|
||||
}
|
||||
|
||||
public static List<StepCounter> cleanup(int retention_days) {
|
||||
return new Delete()
|
||||
.from(StepCounter.class)
|
||||
.where("timestamp < ?", JoH.tsl() - (retention_days * 86400000L))
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
||||
// create the table ourselves without worrying about model versioning and downgrading
|
||||
private static void fixUpTable() {
|
||||
if (patched) return;
|
||||
String[] patchup = {
|
||||
"CREATE TABLE PebbleMovement (_id INTEGER PRIMARY KEY AUTOINCREMENT);",
|
||||
"ALTER TABLE PebbleMovement ADD COLUMN timestamp INTEGER;",
|
||||
"ALTER TABLE PebbleMovement ADD COLUMN metric INTEGER;",
|
||||
"CREATE UNIQUE INDEX index_PebbleMovement_timestamp on PebbleMovement(timestamp);"};
|
||||
|
||||
for (String patch : patchup) {
|
||||
try {
|
||||
SQLiteUtils.execSql(patch);
|
||||
// UserError.Log.e(TAG, "Processed patch should not have succeeded!!: " + patch);
|
||||
} catch (Exception e) {
|
||||
// UserError.Log.d(TAG, "Patch: " + patch + " generated exception as it should: " + e.toString());
|
||||
}
|
||||
}
|
||||
patched = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
258
lib/nightscout/com/eveningoutpost/dexdrip/Models/Tomato.java
Normal file
258
lib/nightscout/com/eveningoutpost/dexdrip/Models/Tomato.java
Normal file
@@ -0,0 +1,258 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.util.HexDump;
|
||||
import com.eveningoutpost.dexdrip.Models.UserError.Log;
|
||||
import com.eveningoutpost.dexdrip.NFCReaderX;
|
||||
import com.eveningoutpost.dexdrip.R;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Blukon;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.BridgeResponse;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.LibreUtils;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import static com.eveningoutpost.dexdrip.xdrip.gs;
|
||||
/**
|
||||
* Created by Tzachi Dar on 7.3.2018.
|
||||
*/
|
||||
|
||||
public class Tomato {
|
||||
private static final String TAG = "DexCollectionService";//?????"Tomato";
|
||||
|
||||
private static final String CHECKSUM_FAILED = "checksum failed";
|
||||
private static final String SERIAL_FAILED = "serial failed";
|
||||
|
||||
private enum TOMATO_STATES {
|
||||
REQUEST_DATA_SENT,
|
||||
RECIEVING_DATA
|
||||
}
|
||||
private final static int TOMATO_HEADER_LENGTH = 18;
|
||||
private final static int TOMATO_PATCH_INFO = 6;
|
||||
|
||||
|
||||
static volatile TOMATO_STATES s_state;
|
||||
|
||||
private static volatile long s_lastReceiveTimestamp;
|
||||
private static volatile byte[] s_full_data = null;
|
||||
private static volatile int s_acumulatedSize = 0;
|
||||
private static volatile boolean s_recviedEnoughData;
|
||||
|
||||
|
||||
public static boolean isTomato() {
|
||||
final ActiveBluetoothDevice activeBluetoothDevice = ActiveBluetoothDevice.first();
|
||||
if (activeBluetoothDevice == null || activeBluetoothDevice.name == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return activeBluetoothDevice.name.startsWith("miaomiao")
|
||||
|| activeBluetoothDevice.name.toLowerCase().startsWith("watlaa");
|
||||
}
|
||||
|
||||
public static BridgeResponse decodeTomatoPacket(byte[] buffer, int len) {
|
||||
final BridgeResponse reply = new BridgeResponse();
|
||||
// Check time, probably need to start on sending
|
||||
long now = JoH.tsl();
|
||||
if(now - s_lastReceiveTimestamp > 3*1000) {
|
||||
// We did not receive data in 3 seconds, moving to init state again
|
||||
Log.e(TAG, "Recieved a buffer after " + (now - s_lastReceiveTimestamp) / 1000 + " seconds, starting again. "+
|
||||
"already acumulated " + s_acumulatedSize + " bytes.");
|
||||
s_state = TOMATO_STATES.REQUEST_DATA_SENT;
|
||||
}
|
||||
|
||||
s_lastReceiveTimestamp = now;
|
||||
if (buffer == null) {
|
||||
Log.e(TAG, "null buffer passed to decodeTomatoPacket");
|
||||
return reply;
|
||||
}
|
||||
if (s_state == TOMATO_STATES.REQUEST_DATA_SENT) {
|
||||
if(buffer.length == 1 && buffer[0] == 0x32) {
|
||||
Log.e(TAG, "returning allow sensor confirm");
|
||||
|
||||
ByteBuffer allowNewSensor = ByteBuffer.allocate(2);
|
||||
allowNewSensor.put(0, (byte) 0xD3);
|
||||
allowNewSensor.put(1, (byte) 0x01);
|
||||
reply.add(allowNewSensor);
|
||||
|
||||
// For debug, make it send data every minute (did not work...)
|
||||
ByteBuffer newFreqMessage = ByteBuffer.allocate(2);
|
||||
newFreqMessage.put(0, (byte) 0xD1);
|
||||
newFreqMessage.put(1, (byte) 0x05);
|
||||
reply.add(newFreqMessage);
|
||||
|
||||
//command to start reading
|
||||
ByteBuffer ackMessage = ByteBuffer.allocate(1);
|
||||
ackMessage.put(0, (byte) 0xF0);
|
||||
reply.add(ackMessage);
|
||||
return reply;
|
||||
}
|
||||
|
||||
if(buffer.length == 1 && buffer[0] == 0x34) {
|
||||
Log.e(TAG, "No sensor has been found");
|
||||
reply.setError_message(gs(R.string.no_sensor_found));
|
||||
return reply;
|
||||
}
|
||||
|
||||
// 18 is the expected header size
|
||||
if(buffer.length >= TOMATO_HEADER_LENGTH && buffer[0] == 0x28) {
|
||||
// We are starting to receive data, need to start accumulating
|
||||
|
||||
// &0xff is needed to convert to hex.
|
||||
int expectedSize = 256 * (int)(buffer[1] & 0xFF) + (int)(buffer[2] & 0xFF);
|
||||
Log.e(TAG, "Starting to acumulate data expectedSize = " + expectedSize);
|
||||
InitBuffer(expectedSize + TOMATO_PATCH_INFO);
|
||||
addData(buffer);
|
||||
s_state = TOMATO_STATES.RECIEVING_DATA;
|
||||
return reply;
|
||||
|
||||
} else {
|
||||
if (JoH.quietratelimit("unknown-initial-packet", 1)) {
|
||||
Log.d(TAG,"Unknown initial packet makeup received" + HexDump.dumpHexString(buffer));
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
}
|
||||
|
||||
if (s_state == TOMATO_STATES.RECIEVING_DATA) {
|
||||
//Log.e(TAG, "received more data s_acumulatedSize = " + s_acumulatedSize + " current buffer size " + buffer.length);
|
||||
try {
|
||||
addData(buffer);
|
||||
} catch (RuntimeException e) {
|
||||
// if the checksum failed lets ask for the data set again but not more than once per minute
|
||||
if (e.getMessage().equals(CHECKSUM_FAILED)) {
|
||||
if (JoH.ratelimit("tomato-full-retry",60)
|
||||
|| JoH.ratelimit("tomato-full-retry2",60)) {
|
||||
reply.getSend().clear();
|
||||
reply.getSend().addAll(Tomato.resetTomatoState());
|
||||
reply.setDelay(8000);
|
||||
reply.setError_message(gs(R.string.checksum_failed__retrying));
|
||||
Log.d(TAG,"Asking for retry of data");
|
||||
}
|
||||
} else if (e.getMessage().equals(SERIAL_FAILED)) {
|
||||
reply.setError_message("Sensor Serial Problem");
|
||||
} else throw e;
|
||||
}
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
Log.wtf(TAG, "Very strange, In an unexpected state " + s_state);
|
||||
|
||||
return reply;
|
||||
}
|
||||
|
||||
static void addData(byte[] buffer) {
|
||||
if(s_acumulatedSize + buffer.length > s_full_data.length) {
|
||||
Log.e(TAG, "Error recieving too much data. exiting. s_acumulatedSize = " + s_acumulatedSize +
|
||||
" buffer.length = " + buffer.length + " s_full_data.length " + s_full_data.length);
|
||||
//??? send something to start back??
|
||||
return;
|
||||
|
||||
}
|
||||
System.arraycopy(buffer, 0, s_full_data, s_acumulatedSize, buffer.length);
|
||||
s_acumulatedSize += buffer.length;
|
||||
AreWeDone();
|
||||
}
|
||||
|
||||
static void AreWeDone() {
|
||||
// Give both versions a chance to work.
|
||||
final int extended_length = 344 + TOMATO_HEADER_LENGTH + 1 + TOMATO_PATCH_INFO;
|
||||
if(s_recviedEnoughData && (s_acumulatedSize != extended_length)) {
|
||||
// This reading already ended
|
||||
Log.e(TAG,"Getting out, as s_recviedEnoughData and we have too much data already s_acumulatedSize = " + s_acumulatedSize);
|
||||
return;
|
||||
}
|
||||
|
||||
if(s_acumulatedSize < 344 + TOMATO_HEADER_LENGTH + 1 ) {
|
||||
//Log.e(TAG,"Getting out, since not enough data s_acumulatedSize = " + s_acumulatedSize);
|
||||
return;
|
||||
}
|
||||
byte[] data = Arrays.copyOfRange(s_full_data, TOMATO_HEADER_LENGTH, TOMATO_HEADER_LENGTH+344);
|
||||
s_recviedEnoughData = true;
|
||||
|
||||
long now = JoH.tsl();
|
||||
// Important note, the actual serial number is 8 bytes long and starts at addresses 5.
|
||||
final String SensorSn = LibreUtils.decodeSerialNumberKey(Arrays.copyOfRange(s_full_data, 5, 13));
|
||||
byte []patchUid = null;
|
||||
byte []patchInfo = null;
|
||||
if(s_acumulatedSize >= extended_length) {
|
||||
patchUid = Arrays.copyOfRange(s_full_data, 5, 13);
|
||||
patchInfo = Arrays.copyOfRange(s_full_data, TOMATO_HEADER_LENGTH+ 344 + 1 , TOMATO_HEADER_LENGTH + 344 + 1+ TOMATO_PATCH_INFO);
|
||||
}
|
||||
Log.d(TAG, "patchUid = " + HexDump.dumpHexString(patchUid));
|
||||
Log.d(TAG, "patchInfo = " + HexDump.dumpHexString(patchInfo));
|
||||
boolean checksum_ok = NFCReaderX.HandleGoodReading(SensorSn, data, now, true, patchUid, patchInfo);
|
||||
Log.e(TAG, "We have all the data that we need " + s_acumulatedSize + " checksum_ok = " + checksum_ok + HexDump.dumpHexString(data));
|
||||
|
||||
if(!checksum_ok) {
|
||||
throw new RuntimeException(CHECKSUM_FAILED);
|
||||
}
|
||||
|
||||
if (SensorSanity.checkLibreSensorChangeIfEnabled(SensorSn)) {
|
||||
Log.e(TAG,"Problem with Libre Serial Number - not processing");
|
||||
throw new RuntimeException(SERIAL_FAILED);
|
||||
}
|
||||
|
||||
PersistentStore.setString("Tomatobattery", Integer.toString(s_full_data[13]));
|
||||
Pref.setInt("bridge_battery", s_full_data[13]);
|
||||
PersistentStore.setString("TomatoHArdware",HexDump.toHexString(s_full_data,16,2));
|
||||
PersistentStore.setString("TomatoFirmware",HexDump.toHexString(s_full_data,14,2));
|
||||
PersistentStore.setString("LibreSN", SensorSn);
|
||||
|
||||
|
||||
}
|
||||
|
||||
// This is the function that we should have once we are able to read all data realiably.
|
||||
static void AreWeDoneMax() {
|
||||
|
||||
if(s_acumulatedSize == s_full_data.length) {
|
||||
Log.e(TAG, "We have a full packet");
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if(s_full_data[s_full_data.length -1] != 0x29) {
|
||||
Log.e(TAG, "recieved full data, but last byte is not 0x29. It is " + s_full_data[s_full_data.length -1]);
|
||||
return;
|
||||
}
|
||||
// We have all the data
|
||||
if(s_full_data.length < 344 + TOMATO_HEADER_LENGTH + 1) {
|
||||
Log.e(TAG, "We have all the data, but it is not enough... s_full_data.length = " + s_full_data.length );
|
||||
return;
|
||||
}
|
||||
Log.e(TAG, "We have a full packet");
|
||||
|
||||
}
|
||||
|
||||
|
||||
static void InitBuffer(int expectedSize) {
|
||||
s_full_data = new byte[expectedSize];
|
||||
s_acumulatedSize = 0;
|
||||
s_recviedEnoughData = false;
|
||||
|
||||
}
|
||||
|
||||
public static ArrayList<ByteBuffer> initialize() {
|
||||
Log.i(TAG, "initialize!");
|
||||
Pref.setInt("bridge_battery", 0); //force battery to no-value before first reading
|
||||
return resetTomatoState();
|
||||
}
|
||||
|
||||
private static ArrayList<ByteBuffer> resetTomatoState() {
|
||||
ArrayList<ByteBuffer> ret = new ArrayList<>();
|
||||
|
||||
s_state = TOMATO_STATES.REQUEST_DATA_SENT;
|
||||
|
||||
// Make tomato send data every 5 minutes
|
||||
ByteBuffer newFreqMessage = ByteBuffer.allocate(2);
|
||||
newFreqMessage.put(0, (byte) 0xD1);
|
||||
newFreqMessage.put(1, (byte) 0x05);
|
||||
ret.add(newFreqMessage);
|
||||
|
||||
//command to start reading
|
||||
ByteBuffer ackMessage = ByteBuffer.allocate(1);
|
||||
ackMessage.put(0, (byte) 0xF0);
|
||||
ret.add(ackMessage);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.activeandroid.Model;
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Select;
|
||||
import com.eveningoutpost.dexdrip.GcmActivity;
|
||||
import com.eveningoutpost.dexdrip.Home;
|
||||
import com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.util.HexDump;
|
||||
import com.eveningoutpost.dexdrip.Models.UserError.Log;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
|
||||
import com.eveningoutpost.dexdrip.utils.CheckBridgeBattery;
|
||||
import com.eveningoutpost.dexdrip.utils.DexCollectionType;
|
||||
import com.google.gson.annotations.Expose;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Created by Emma Black on 11/6/14.
|
||||
*/
|
||||
|
||||
@Table(name = "TransmitterData", id = BaseColumns._ID)
|
||||
public class TransmitterData extends Model {
|
||||
private final static String TAG = TransmitterData.class.getSimpleName();
|
||||
|
||||
@Expose
|
||||
@Column(name = "timestamp", index = true)
|
||||
public long timestamp;
|
||||
|
||||
// TODO these should be int or long surely
|
||||
@Expose
|
||||
@Column(name = "raw_data")
|
||||
public double raw_data;
|
||||
|
||||
@Expose
|
||||
@Column(name = "filtered_data")
|
||||
public double filtered_data;
|
||||
|
||||
@Expose
|
||||
@Column(name = "sensor_battery_level")
|
||||
public int sensor_battery_level;
|
||||
|
||||
@Expose
|
||||
@Column(name = "uuid", index = true)
|
||||
public String uuid;
|
||||
|
||||
public static synchronized TransmitterData create(byte[] buffer, int len, Long timestamp) {
|
||||
if (len < 6) {
|
||||
return null;
|
||||
}
|
||||
final TransmitterData transmitterData = new TransmitterData();
|
||||
try {
|
||||
if ((buffer[0] == 0x11 || buffer[0] == 0x15) && buffer[1] == 0x00) {
|
||||
//this is a dexbridge packet. Process accordingly.
|
||||
Log.i(TAG, "create Processing a Dexbridge packet");
|
||||
final ByteBuffer txData = ByteBuffer.allocate(len);
|
||||
txData.order(ByteOrder.LITTLE_ENDIAN);
|
||||
txData.put(buffer, 0, len);
|
||||
transmitterData.raw_data = txData.getInt(2);
|
||||
transmitterData.filtered_data = txData.getInt(6);
|
||||
// bitwise and with 0xff (1111....1) to avoid that the byte is treated as signed.
|
||||
transmitterData.sensor_battery_level = txData.get(10) & 0xff;
|
||||
if (buffer[0] == 0x15) {
|
||||
Log.i(TAG, "create Processing a Dexbridge packet includes delay information");
|
||||
transmitterData.timestamp = timestamp - txData.getInt(16);
|
||||
} else {
|
||||
transmitterData.timestamp = timestamp;
|
||||
}
|
||||
Log.i(TAG, "Created transmitterData record with Raw value of " + transmitterData.raw_data + " and Filtered value of " + transmitterData.filtered_data + " at " + timestamp + " with timestamp " + transmitterData.timestamp);
|
||||
} else { //this is NOT a dexbridge packet. Process accordingly.
|
||||
Log.i(TAG, "create Processing a BTWixel or IPWixel packet");
|
||||
StringBuilder data_string = new StringBuilder();
|
||||
for (int i = 0; i < len; ++i) {
|
||||
data_string.append((char) buffer[i]);
|
||||
}
|
||||
final String[] data = data_string.toString().split("\\s+");
|
||||
|
||||
if (data.length > 1) {
|
||||
transmitterData.sensor_battery_level = Integer.parseInt(data[1]);
|
||||
if (data.length > 2) {
|
||||
try {
|
||||
Pref.setInt("bridge_battery", Integer.parseInt(data[2]));
|
||||
if (Home.get_master()) {
|
||||
GcmActivity.sendBridgeBattery(Pref.getInt("bridge_battery", -1));
|
||||
}
|
||||
CheckBridgeBattery.checkBridgeBattery();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Got exception processing classic wixel or limitter battery value: " + e.toString());
|
||||
}
|
||||
if (data.length > 3) {
|
||||
if ((DexCollectionType.getDexCollectionType() == DexCollectionType.LimiTTer)
|
||||
&& (!Pref.getBooleanDefaultFalse("use_transmiter_pl_bluetooth"))) {
|
||||
try {
|
||||
// reported sensor age in minutes
|
||||
final Integer sensorAge = Integer.parseInt(data[3]);
|
||||
if ((sensorAge > 0) && (sensorAge < 200000))
|
||||
Pref.setInt("nfc_sensor_age", sensorAge);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Got exception processing field 4 in classic limitter protocol: " + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
transmitterData.raw_data = Integer.parseInt(data[0]);
|
||||
transmitterData.filtered_data = Integer.parseInt(data[0]);
|
||||
// TODO process does_have_filtered_here with extended protocol
|
||||
transmitterData.timestamp = timestamp;
|
||||
}
|
||||
|
||||
//Stop allowing readings that are older than the last one - or duplicate data, its bad! (from savek-cc)
|
||||
final TransmitterData lastTransmitterData = TransmitterData.last();
|
||||
if (lastTransmitterData != null && lastTransmitterData.timestamp >= transmitterData.timestamp) {
|
||||
Log.e(TAG, "Rejecting TransmitterData constraint: last: " + JoH.dateTimeText(lastTransmitterData.timestamp) + " >= this: " + JoH.dateTimeText(transmitterData.timestamp));
|
||||
return null;
|
||||
}
|
||||
if (lastTransmitterData != null && lastTransmitterData.raw_data == transmitterData.raw_data && Math.abs(lastTransmitterData.timestamp - transmitterData.timestamp) < (Constants.MINUTE_IN_MS * 2)) {
|
||||
Log.e(TAG, "Rejecting identical TransmitterData constraint: last: " + JoH.dateTimeText(lastTransmitterData.timestamp) + " due to 2 minute rule this: " + JoH.dateTimeText(transmitterData.timestamp));
|
||||
return null;
|
||||
}
|
||||
final Calibration lastCalibration = Calibration.lastValid();
|
||||
if (lastCalibration != null && lastCalibration.timestamp > transmitterData.timestamp) {
|
||||
Log.e(TAG, "Rejecting historical TransmitterData constraint: calib: " + JoH.dateTimeText(lastCalibration.timestamp) + " > this: " + JoH.dateTimeText(transmitterData.timestamp));
|
||||
return null;
|
||||
}
|
||||
|
||||
transmitterData.uuid = UUID.randomUUID().toString();
|
||||
transmitterData.save();
|
||||
return transmitterData;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Got exception processing fields in protocol: " + e + " " + HexDump.dumpHexString(buffer));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static synchronized TransmitterData create(int raw_data, int filtered_data, int sensor_battery_level, long timestamp) {
|
||||
TransmitterData lastTransmitterData = TransmitterData.last();
|
||||
if (lastTransmitterData != null && lastTransmitterData.raw_data == raw_data && Math.abs(lastTransmitterData.timestamp - new Date().getTime()) < (Constants.MINUTE_IN_MS * 2)) { //Stop allowing duplicate data, its bad!
|
||||
return null;
|
||||
}
|
||||
|
||||
TransmitterData transmitterData = new TransmitterData();
|
||||
transmitterData.sensor_battery_level = sensor_battery_level;
|
||||
transmitterData.raw_data = raw_data;
|
||||
transmitterData.filtered_data = filtered_data;
|
||||
transmitterData.timestamp = timestamp;
|
||||
transmitterData.uuid = UUID.randomUUID().toString();
|
||||
transmitterData.save();
|
||||
return transmitterData;
|
||||
}
|
||||
|
||||
public static synchronized TransmitterData create(int raw_data ,int sensor_battery_level, long timestamp) {
|
||||
TransmitterData lastTransmitterData = TransmitterData.last();
|
||||
if (lastTransmitterData != null && lastTransmitterData.raw_data == raw_data && Math.abs(lastTransmitterData.timestamp - new Date().getTime()) < (Constants.MINUTE_IN_MS * 2)) { //Stop allowing duplicate data, its bad!
|
||||
return null;
|
||||
}
|
||||
|
||||
TransmitterData transmitterData = new TransmitterData();
|
||||
transmitterData.sensor_battery_level = sensor_battery_level;
|
||||
transmitterData.raw_data = raw_data ;
|
||||
transmitterData.timestamp = timestamp;
|
||||
transmitterData.uuid = UUID.randomUUID().toString();
|
||||
transmitterData.save();
|
||||
return transmitterData;
|
||||
}
|
||||
|
||||
public static TransmitterData last() {
|
||||
return new Select()
|
||||
.from(TransmitterData.class)
|
||||
.orderBy("_ID desc")
|
||||
.executeSingle();
|
||||
}
|
||||
|
||||
public static List<TransmitterData> last(int count) {
|
||||
return new Select()
|
||||
.from(TransmitterData.class)
|
||||
.orderBy("_ID desc")
|
||||
.limit(count)
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static TransmitterData lastByTimestamp() {
|
||||
return new Select()
|
||||
.from(TransmitterData.class)
|
||||
.orderBy("timestamp desc")
|
||||
.executeSingle();
|
||||
}
|
||||
|
||||
public static TransmitterData getForTimestamp(double timestamp) {//KS
|
||||
try {
|
||||
Sensor sensor = Sensor.currentSensor();
|
||||
if (sensor != null) {
|
||||
TransmitterData bgReading = new Select()
|
||||
.from(TransmitterData.class)
|
||||
.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;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG,"getForTimestamp() Got exception on Select : "+e.toString());
|
||||
return null;
|
||||
}
|
||||
Log.d(TAG, "getForTimestamp: No luck finding a BG timestamp match");
|
||||
return null;
|
||||
}
|
||||
|
||||
public static TransmitterData findByUuid(String uuid) {//KS
|
||||
try {
|
||||
return new Select()
|
||||
.from(TransmitterData.class)
|
||||
.where("uuid = ?", uuid)
|
||||
.executeSingle();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG,"findByUuid() Got exception on Select : "+e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static TransmitterData byid(long id) {
|
||||
return new Select()
|
||||
.from(TransmitterData.class)
|
||||
.where("_ID = ?", id)
|
||||
.executeSingle();
|
||||
}
|
||||
|
||||
public static void updateTransmitterBatteryFromSync(final int battery_level) {
|
||||
try {
|
||||
TransmitterData td = TransmitterData.last();
|
||||
if ((td == null) || (td.raw_data!=0))
|
||||
{
|
||||
td=TransmitterData.create(0,battery_level,(long)JoH.ts());
|
||||
Log.d(TAG,"Created new fake transmitter data record for battery sync");
|
||||
if (td==null) return;
|
||||
}
|
||||
if ((battery_level != td.sensor_battery_level) || ((JoH.ts()-td.timestamp)>(1000*60*60))) {
|
||||
td.sensor_battery_level = battery_level;
|
||||
td.timestamp = (long)JoH.ts(); // freshen timestamp on this bogus record for system status
|
||||
Log.d(TAG,"Saving synced sensor battery, new level: "+battery_level);
|
||||
td.save();
|
||||
} else {
|
||||
Log.d(TAG,"Synced sensor battery level same as existing: "+battery_level);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG,"Got exception updating sensor battery from sync: "+e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static double roundRaw(TransmitterData td) {
|
||||
return JoH.roundDouble(td.raw_data,3);
|
||||
}
|
||||
private static double roundFiltered(TransmitterData td) {
|
||||
return JoH.roundDouble(td.filtered_data,3);
|
||||
}
|
||||
|
||||
public static boolean unchangedRaw() {
|
||||
final List<TransmitterData> items = last(3);
|
||||
if (items != null && items.size() == 3) {
|
||||
return (roundRaw(items.get(0)) == roundRaw(items.get(1))
|
||||
&& roundRaw(items.get(0)) == roundRaw(items.get(2))
|
||||
&& roundFiltered(items.get(0)) == roundFiltered(items.get(1))
|
||||
&& roundFiltered(items.get(0)) == roundFiltered(items.get(2)));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
1252
lib/nightscout/com/eveningoutpost/dexdrip/Models/Treatments.java
Normal file
1252
lib/nightscout/com/eveningoutpost/dexdrip/Models/Treatments.java
Normal file
File diff suppressed because it is too large
Load Diff
413
lib/nightscout/com/eveningoutpost/dexdrip/Models/UserError.java
Normal file
413
lib/nightscout/com/eveningoutpost/dexdrip/Models/UserError.java
Normal file
@@ -0,0 +1,413 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.activeandroid.Cache;
|
||||
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.eveningoutpost.dexdrip.UtilityModels.Constants;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
|
||||
import com.google.gson.annotations.Expose;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
|
||||
//import com.bugfender.sdk.Bugfender;
|
||||
|
||||
/**
|
||||
* Created by Emma Black on 8/3/15.
|
||||
*/
|
||||
|
||||
@Table(name = "UserErrors", id = BaseColumns._ID)
|
||||
public class UserError extends Model {
|
||||
|
||||
private final static String TAG = UserError.class.getSimpleName();
|
||||
|
||||
@Expose
|
||||
@Column(name = "shortError")
|
||||
public String shortError; // Short error message to be displayed on table
|
||||
|
||||
@Expose
|
||||
@Column(name = "message")
|
||||
public String message; // Additional text when error is expanded
|
||||
|
||||
@Expose
|
||||
@Column(name = "severity", index = true)
|
||||
public int severity; // int between 1 and 3, 3 being most severe
|
||||
|
||||
// 5 = internal lower level user events
|
||||
// 6 = higher granularity user events
|
||||
|
||||
@Expose
|
||||
@Column(name = "timestamp", index = true)
|
||||
public long timestamp; // Time the error was raised
|
||||
|
||||
//todo: rather than include multiples of the same error, should we have a "Count" and just increase that on duplicates?
|
||||
//or rather, perhaps we should group up the errors
|
||||
|
||||
public String toString()
|
||||
{
|
||||
return severity+" ^ "+JoH.dateTimeText((long)timestamp)+" ^ "+shortError+" ^ "+message;
|
||||
}
|
||||
|
||||
public UserError() {}
|
||||
|
||||
public UserError(int severity, String shortError, String message) {
|
||||
this.severity = severity;
|
||||
this.shortError = shortError;
|
||||
this.message = message;
|
||||
this.timestamp = new Date().getTime();
|
||||
this.save();
|
||||
/* if (xdrip.useBF) {
|
||||
switch (severity) {
|
||||
case 2:
|
||||
case 3:
|
||||
Bugfender.e(shortError, message);
|
||||
break;
|
||||
case 5:
|
||||
case 6:
|
||||
Bugfender.w(shortError, message);
|
||||
break;
|
||||
default:
|
||||
Bugfender.d(shortError, message);
|
||||
break;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
public UserError(String shortError, String message) {
|
||||
this(2, shortError, message);
|
||||
}
|
||||
|
||||
public static UserError UserErrorHigh(String shortError, String message) {
|
||||
return new UserError(3, shortError, message);
|
||||
}
|
||||
|
||||
public static UserError UserErrorLow(String shortError, String message) {
|
||||
return new UserError(1, shortError, message);
|
||||
}
|
||||
|
||||
public static UserError UserEventLow(String shortError, String message) {
|
||||
return new UserError(5, shortError, message);
|
||||
}
|
||||
|
||||
public static UserError UserEventHigh(String shortError, String message) {
|
||||
return new UserError(6, shortError, message);
|
||||
}
|
||||
|
||||
// TODO move time calc stuff to JOH, wrap it here with our timestamp
|
||||
public String bestTime() {
|
||||
final long since = JoH.msSince(timestamp);
|
||||
if (since < Constants.DAY_IN_MS) {
|
||||
return JoH.hourMinuteString(timestamp);
|
||||
} else {
|
||||
return JoH.dateTimeText(timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void cleanup() {
|
||||
new Cleanup().execute(deletable());
|
||||
}
|
||||
|
||||
// used in unit testing
|
||||
public static void cleanup(long timestamp) {
|
||||
List<UserError> userErrors = new Select()
|
||||
.from(UserError.class)
|
||||
.where("timestamp < ?", timestamp)
|
||||
.orderBy("timestamp desc")
|
||||
.execute();
|
||||
if (userErrors != null) Log.d(TAG, "cleanup UserError size=" + userErrors.size());
|
||||
new Cleanup().execute(userErrors);
|
||||
}
|
||||
|
||||
public static void cleanupByTimeAndClause(final long timestamp, final String clause) {
|
||||
new Delete().from(UserError.class)
|
||||
.where("timestamp < ?", timestamp)
|
||||
.where(clause)
|
||||
.execute();
|
||||
}
|
||||
|
||||
public synchronized static void cleanupRaw() {
|
||||
final long timestamp = JoH.tsl();
|
||||
cleanupByTimeAndClause(timestamp - Constants.DAY_IN_MS, "severity < 3");
|
||||
cleanupByTimeAndClause(timestamp - Constants.DAY_IN_MS * 3, "severity = 3");
|
||||
cleanupByTimeAndClause(timestamp - Constants.DAY_IN_MS * 7, "severity > 3");
|
||||
Cache.clear();
|
||||
}
|
||||
|
||||
|
||||
public static List<UserError> all() {
|
||||
return new Select()
|
||||
.from(UserError.class)
|
||||
.orderBy("timestamp desc")
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static List<UserError> deletable() {
|
||||
List<UserError> userErrors = new Select()
|
||||
.from(UserError.class)
|
||||
.where("severity < ?", 3)
|
||||
.where("timestamp < ?", (new Date().getTime() - 1000 * 60 * 60 * 24))
|
||||
.orderBy("timestamp desc")
|
||||
.execute();
|
||||
List<UserError> highErrors = new Select()
|
||||
.from(UserError.class)
|
||||
.where("severity = ?", 3)
|
||||
.where("timestamp < ?", (new Date().getTime() - 1000*60*60*24*3))
|
||||
.orderBy("timestamp desc")
|
||||
.execute();
|
||||
List<UserError> events = new Select()
|
||||
.from(UserError.class)
|
||||
.where("severity > ?", 3)
|
||||
.where("timestamp < ?", (new Date().getTime() - 1000*60*60*24*7))
|
||||
.orderBy("timestamp desc")
|
||||
.execute();
|
||||
userErrors.addAll(highErrors);
|
||||
userErrors.addAll(events);
|
||||
return userErrors;
|
||||
}
|
||||
|
||||
public static List<UserError> bySeverity(Integer[] levels) {
|
||||
String levelsString = " ";
|
||||
for (int level : levels) {
|
||||
levelsString += level + ",";
|
||||
}
|
||||
Log.d("UserError", "severity in ("+levelsString.substring(0,levelsString.length() - 1)+")");
|
||||
return new Select()
|
||||
.from(UserError.class)
|
||||
.where("severity in ("+levelsString.substring(0,levelsString.length() - 1)+")")
|
||||
.orderBy("timestamp desc")
|
||||
.limit(10000)//too many data can kill akp
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static List<UserError> bySeverityNewerThanID(long id, Integer[] levels, int limit) {
|
||||
String levelsString = " ";
|
||||
for (int level : levels) {
|
||||
levelsString += level + ",";
|
||||
}
|
||||
Log.d("UserError", "severity in (" + levelsString.substring(0, levelsString.length() - 1) + ")");
|
||||
return new Select()
|
||||
.from(UserError.class)
|
||||
.where("_ID > ?", id)
|
||||
.where("severity in (" + levelsString.substring(0, levelsString.length() - 1) + ")")
|
||||
.orderBy("timestamp desc")
|
||||
.limit(limit)
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static List<UserError> newerThanID(long id, int limit) {
|
||||
return new Select()
|
||||
.from(UserError.class)
|
||||
.where("_ID > ?", id)
|
||||
.orderBy("timestamp desc")
|
||||
.limit(limit)
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static List<UserError> olderThanID(long id, int limit) {
|
||||
return new Select()
|
||||
.from(UserError.class)
|
||||
.where("_ID < ?", id)
|
||||
.orderBy("timestamp desc")
|
||||
.limit(limit)
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static List<UserError> bySeverityOlderThanID(long id, Integer[] levels, int limit) {
|
||||
String levelsString = " ";
|
||||
for (int level : levels) {
|
||||
levelsString += level + ",";
|
||||
}
|
||||
Log.d("UserError", "severity in (" + levelsString.substring(0, levelsString.length() - 1) + ")");
|
||||
return new Select()
|
||||
.from(UserError.class)
|
||||
.where("_ID < ?", id)
|
||||
.where("severity in (" + levelsString.substring(0, levelsString.length() - 1) + ")")
|
||||
.orderBy("timestamp desc")
|
||||
.limit(limit)
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
||||
public static UserError getForTimestamp(UserError error) {
|
||||
try {
|
||||
return new Select()
|
||||
.from(UserError.class)
|
||||
.where("timestamp = ?", error.timestamp)
|
||||
.where("shortError = ?", error.shortError)
|
||||
.where("message = ?", error.message)
|
||||
.executeSingle();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG,"getForTimestamp() Got exception on Select : "+e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Cleanup extends AsyncTask<List<UserError>, Integer, Boolean> {
|
||||
@Override
|
||||
protected Boolean doInBackground(List<UserError>... errors) {
|
||||
try {
|
||||
for(UserError userError : errors[0]) {
|
||||
userError.delete();
|
||||
//userError.save();
|
||||
}
|
||||
return true;
|
||||
} catch(Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<UserError> bySeverity(int level) {
|
||||
return bySeverity(new Integer[]{level});
|
||||
}
|
||||
public static List<UserError> bySeverity(int level, int level2) {
|
||||
return bySeverity(new Integer[]{ level, level2 });
|
||||
}
|
||||
public static List<UserError> bySeverity(int level, int level2, int level3) {
|
||||
return bySeverity(new Integer[]{ level, level2, level3 });
|
||||
}
|
||||
|
||||
|
||||
public static class Log {
|
||||
public static void e(String a, String b){
|
||||
android.util.Log.e(a, b);
|
||||
new UserError(a, b);
|
||||
}
|
||||
|
||||
public static void e(String tag, String b, Exception e){
|
||||
android.util.Log.e(tag, b, e);
|
||||
new UserError(tag, b + "\n" + e.toString());
|
||||
}
|
||||
|
||||
public static void w(String tag, String b){
|
||||
android.util.Log.w(tag, b);
|
||||
UserError.UserErrorLow(tag, b);
|
||||
}
|
||||
public static void w(String tag, String b, Exception e){
|
||||
android.util.Log.w(tag, b, e);
|
||||
UserError.UserErrorLow(tag, b + "\n" + e.toString());
|
||||
}
|
||||
public static void wtf(String tag, String b){
|
||||
android.util.Log.wtf(tag, b);
|
||||
UserError.UserErrorHigh(tag, b);
|
||||
}
|
||||
public static void wtf(String tag, String b, Exception e){
|
||||
android.util.Log.wtf(tag, b, e);
|
||||
UserError.UserErrorHigh(tag, b + "\n" + e.toString());
|
||||
}
|
||||
public static void wtf(String tag, Exception e){
|
||||
android.util.Log.wtf(tag, e);
|
||||
UserError.UserErrorHigh(tag, e.toString());
|
||||
}
|
||||
|
||||
public static void uel(String tag, String b) {
|
||||
android.util.Log.i(tag, b);
|
||||
UserError.UserEventLow(tag, b);
|
||||
}
|
||||
|
||||
public static void ueh(String tag, String b) {
|
||||
android.util.Log.i(tag, b);
|
||||
UserError.UserEventHigh(tag, b);
|
||||
}
|
||||
|
||||
public static void d(String tag, String b){
|
||||
android.util.Log.d(tag, b);
|
||||
if(ExtraLogTags.shouldLogTag(tag, android.util.Log.DEBUG)) {
|
||||
UserErrorLow(tag, b);
|
||||
}
|
||||
}
|
||||
|
||||
public static void v(String tag, String b){
|
||||
android.util.Log.v(tag, b);
|
||||
if(ExtraLogTags.shouldLogTag(tag, android.util.Log.VERBOSE)) {
|
||||
UserErrorLow(tag, b);
|
||||
}
|
||||
}
|
||||
|
||||
public static void i(String tag, String b){
|
||||
android.util.Log.i(tag, b);
|
||||
if(ExtraLogTags.shouldLogTag(tag, android.util.Log.INFO)) {
|
||||
UserErrorLow(tag, b);
|
||||
}
|
||||
}
|
||||
|
||||
static ExtraLogTags extraLogTags = new ExtraLogTags();
|
||||
}
|
||||
|
||||
public static class ExtraLogTags {
|
||||
|
||||
static Hashtable <String, Integer> extraTags;
|
||||
ExtraLogTags () {
|
||||
extraTags = new Hashtable <String, Integer>();
|
||||
String extraLogs = Pref.getStringDefaultBlank("extra_tags_for_logging");
|
||||
readPreference(extraLogs);
|
||||
}
|
||||
|
||||
/*
|
||||
* This function reads a string representing tags that the user wants to log
|
||||
* Format of string is tag1:level1,tag2,level2
|
||||
* Example of string is Alerts:i,BG:W
|
||||
*
|
||||
*/
|
||||
public static void readPreference(String extraLogs) {
|
||||
extraLogs = extraLogs.trim();
|
||||
if (extraLogs.length() > 0) UserErrorLow(TAG, "called with string " + extraLogs);
|
||||
extraTags.clear();
|
||||
|
||||
// allow splitting to work with a single entry and no delimiter zzz
|
||||
if ((extraLogs.length() > 1) && (!extraLogs.contains(","))) {
|
||||
extraLogs += ",";
|
||||
}
|
||||
String[] tags = extraLogs.split(",");
|
||||
if (tags.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// go over all tags and parse them
|
||||
for(String tag : tags) {
|
||||
if (tag.length() > 0) parseTag(tag);
|
||||
}
|
||||
}
|
||||
|
||||
static void parseTag(String tag) {
|
||||
// Format is tag:level for example Alerts:i
|
||||
String[] tagAndLevel = tag.trim().split(":");
|
||||
if(tagAndLevel.length != 2) {
|
||||
Log.e(TAG, "Failed to parse " + tag);
|
||||
return;
|
||||
}
|
||||
String level = tagAndLevel[1];
|
||||
String tagName = tagAndLevel[0].toLowerCase();
|
||||
if (level.compareTo("d") == 0) {
|
||||
extraTags.put(tagName, android.util.Log.DEBUG);
|
||||
UserErrorLow(TAG, "Adding tag with DEBUG " + tagAndLevel[0] );
|
||||
return;
|
||||
}
|
||||
if (level.compareTo("v") == 0) {
|
||||
extraTags.put(tagName, android.util.Log.VERBOSE);
|
||||
UserErrorLow(TAG,"Adding tag with VERBOSE " + tagAndLevel[0] );
|
||||
return;
|
||||
}
|
||||
if (level.compareTo("i") == 0) {
|
||||
extraTags.put(tagName, android.util.Log.INFO);
|
||||
UserErrorLow(TAG, "Adding tag with info " + tagAndLevel[0] );
|
||||
return;
|
||||
}
|
||||
Log.e(TAG, "Unknown level for tag " + tag + " please use d v or i");
|
||||
}
|
||||
|
||||
static boolean shouldLogTag(final String tag, final int level) {
|
||||
final Integer levelForTag = extraTags.get(tag != null ? tag.toLowerCase() : "");
|
||||
return levelForTag != null && level >= levelForTag;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.provider.BaseColumns;
|
||||
|
||||
import com.activeandroid.Model;
|
||||
import com.activeandroid.annotation.Column;
|
||||
import com.activeandroid.annotation.Table;
|
||||
import com.activeandroid.query.Select;
|
||||
import com.eveningoutpost.dexdrip.Models.UserError.Log;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.AlertPlayer;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Created by Emma Black on 11/29/14.
|
||||
*/
|
||||
|
||||
@Table(name = "Notifications", id = BaseColumns._ID)
|
||||
public class UserNotification extends Model {
|
||||
|
||||
// For 'other alerts' this will be the time that the alert should be raised again.
|
||||
// For calibration alerts this is the time that the alert was played.
|
||||
@Column(name = "timestamp", index = true)
|
||||
public double timestamp;
|
||||
|
||||
@Column(name = "message")
|
||||
public String message;
|
||||
|
||||
@Column(name = "bg_alert")
|
||||
public boolean bg_alert;
|
||||
|
||||
@Column(name = "calibration_alert")
|
||||
public boolean calibration_alert;
|
||||
|
||||
@Column(name = "double_calibration_alert")
|
||||
public boolean double_calibration_alert;
|
||||
|
||||
@Column(name = "extra_calibration_alert")
|
||||
public boolean extra_calibration_alert;
|
||||
|
||||
@Column(name = "bg_unclear_readings_alert")
|
||||
public boolean bg_unclear_readings_alert;
|
||||
|
||||
@Column(name = "bg_missed_alerts")
|
||||
public boolean bg_missed_alerts;
|
||||
|
||||
@Column(name = "bg_rise_alert")
|
||||
public boolean bg_rise_alert;
|
||||
|
||||
@Column(name = "bg_fall_alert")
|
||||
public boolean bg_fall_alert;
|
||||
|
||||
private final static List<String> legacy_types = Arrays.asList(
|
||||
"bg_alert", "calibration_alert", "double_calibration_alert",
|
||||
"extra_calibration_alert", "bg_unclear_readings_alert",
|
||||
"bg_missed_alerts", "bg_rise_alert", "bg_fall_alert");
|
||||
private final static String TAG = AlertPlayer.class.getSimpleName();
|
||||
|
||||
|
||||
public static UserNotification lastBgAlert() {
|
||||
return new Select()
|
||||
.from(UserNotification.class)
|
||||
.where("bg_alert = ?", true)
|
||||
.orderBy("_ID desc")
|
||||
.executeSingle();
|
||||
}
|
||||
public static UserNotification lastCalibrationAlert() {
|
||||
return new Select()
|
||||
.from(UserNotification.class)
|
||||
.where("calibration_alert = ?", true)
|
||||
.orderBy("_ID desc")
|
||||
.executeSingle();
|
||||
}
|
||||
public static UserNotification lastDoubleCalibrationAlert() {
|
||||
return new Select()
|
||||
.from(UserNotification.class)
|
||||
.where("double_calibration_alert = ?", true)
|
||||
.orderBy("_ID desc")
|
||||
.executeSingle();
|
||||
}
|
||||
public static UserNotification lastExtraCalibrationAlert() {
|
||||
return new Select()
|
||||
.from(UserNotification.class)
|
||||
.where("extra_calibration_alert = ?", true)
|
||||
.orderBy("_ID desc")
|
||||
.executeSingle();
|
||||
}
|
||||
|
||||
// the UserNotifcation model is difficult to extend without adding more
|
||||
// booleans which will introduce a database incompatibility and prevent
|
||||
// downgrading. So instead we work around it with shared preferences until
|
||||
// such time as the booleans are just replaced with a string field or similar
|
||||
// improvement.
|
||||
|
||||
public static UserNotification GetNotificationByType(String type) {
|
||||
if (legacy_types.contains(type)) {
|
||||
type = type + " = ?";
|
||||
return new Select()
|
||||
.from(UserNotification.class)
|
||||
.where(type, true)
|
||||
.orderBy("_ID desc")
|
||||
.executeSingle();
|
||||
} else {
|
||||
final String timestamp = PersistentStore.getString("UserNotification:timestamp:" + type);
|
||||
if (timestamp.equals("")) return null;
|
||||
final String message = PersistentStore.getString("UserNotification:message:" + type);
|
||||
if (message.equals("")) return null;
|
||||
UserNotification userNotification = new UserNotification();
|
||||
userNotification.timestamp = JoH.tolerantParseDouble(timestamp, -1);
|
||||
if (userNotification.timestamp == -1) return null; // bad data
|
||||
userNotification.message = message;
|
||||
Log.d(TAG, "Workaround for: " + type + " " + userNotification.message + " timestamp: " + userNotification.timestamp);
|
||||
return userNotification;
|
||||
}
|
||||
}
|
||||
|
||||
public static void DeleteNotificationByType(String type) {
|
||||
if (legacy_types.contains(type)) {
|
||||
UserNotification userNotification = UserNotification.GetNotificationByType(type);
|
||||
if (userNotification != null) {
|
||||
userNotification.delete();
|
||||
}
|
||||
} else {
|
||||
PersistentStore.setString("UserNotification:timestamp:" + type, "");
|
||||
}
|
||||
}
|
||||
|
||||
public static void snoozeAlert(String type, long snoozeMinutes) {
|
||||
UserNotification userNotification = GetNotificationByType(type);
|
||||
if(userNotification == null) {
|
||||
Log.e(TAG, "Error snoozeAlert did not find an alert for type " + type);
|
||||
return;
|
||||
}
|
||||
userNotification.timestamp = new Date().getTime() + snoozeMinutes * 60000;
|
||||
userNotification.save();
|
||||
|
||||
}
|
||||
|
||||
public static UserNotification create(String message, String type, long timestamp) {
|
||||
UserNotification userNotification = new UserNotification();
|
||||
userNotification.timestamp = timestamp;
|
||||
userNotification.message = message;
|
||||
switch (type) {
|
||||
case "bg_alert":
|
||||
userNotification.bg_alert = true;
|
||||
break;
|
||||
case "calibration_alert":
|
||||
userNotification.calibration_alert = true;
|
||||
break;
|
||||
case "double_calibration_alert":
|
||||
userNotification.double_calibration_alert = true;
|
||||
break;
|
||||
case "extra_calibration_alert":
|
||||
userNotification.extra_calibration_alert = true;
|
||||
break;
|
||||
case "bg_unclear_readings_alert":
|
||||
userNotification.bg_unclear_readings_alert = true;
|
||||
break;
|
||||
case "bg_missed_alerts":
|
||||
userNotification.bg_missed_alerts = true;
|
||||
break;
|
||||
case "bg_rise_alert":
|
||||
userNotification.bg_rise_alert = true;
|
||||
break;
|
||||
case "bg_fall_alert":
|
||||
userNotification.bg_fall_alert = true;
|
||||
break;
|
||||
default:
|
||||
Log.d(TAG, "Saving workaround for: " + type + " " + message);
|
||||
PersistentStore.setString("UserNotification:timestamp:" + type, String.format(Locale.US, "%d", (long) timestamp));
|
||||
PersistentStore.setString("UserNotification:message:" + type, message);
|
||||
return null;
|
||||
}
|
||||
userNotification.save();
|
||||
return userNotification;
|
||||
|
||||
}
|
||||
}
|
||||
220
lib/nightscout/com/eveningoutpost/dexdrip/Models/blueReader.java
Normal file
220
lib/nightscout/com/eveningoutpost/dexdrip/Models/blueReader.java
Normal file
@@ -0,0 +1,220 @@
|
||||
package com.eveningoutpost.dexdrip.Models;
|
||||
|
||||
import android.text.format.DateFormat;
|
||||
import com.eveningoutpost.dexdrip.Home;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Date;
|
||||
import com.eveningoutpost.dexdrip.Services.DexCollectionService;
|
||||
import com.eveningoutpost.dexdrip.Models.UserError.Log;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Notifications;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore;
|
||||
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
|
||||
import com.eveningoutpost.dexdrip.xdrip;
|
||||
import com.eveningoutpost.dexdrip.R;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.regex.*;
|
||||
|
||||
import static com.eveningoutpost.dexdrip.utils.FileUtils.getExternalDir;
|
||||
import static com.eveningoutpost.dexdrip.utils.FileUtils.makeSureDirectoryExists;
|
||||
|
||||
|
||||
/**
|
||||
* Created by MasterPlexus on 11.12.2017.
|
||||
*/
|
||||
|
||||
public class blueReader {
|
||||
private static final String TAG = "blueReader";
|
||||
private static final String BatLog="/BatteryLog.csv";
|
||||
private static int counterHibernated = 0;
|
||||
private static Matcher tempVers;
|
||||
|
||||
private static final byte[] shutdown = new byte[]{0x6B}; // Char 'k'
|
||||
private static final byte[] requestValue = new byte[]{0x6C}; // Char 'l'
|
||||
private static final byte[] goHybernate = new byte[]{0x68}; // Char 'h'
|
||||
private static final byte[] restart = new byte[]{0x79}; // Char 'y'
|
||||
|
||||
|
||||
public static boolean isblueReader() {
|
||||
final ActiveBluetoothDevice activeBluetoothDevice = ActiveBluetoothDevice.first();
|
||||
try {
|
||||
return activeBluetoothDevice.name.contentEquals("blueReader");
|
||||
} catch (NullPointerException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* no real need at the moment 30.01.2018
|
||||
public static boolean isblueReaderPacket(byte[] buffer) {
|
||||
return !((buffer == null) || (new String(buffer).startsWith("IDR") ||
|
||||
new String(buffer).startsWith("TRANS_FAILED") ||
|
||||
new String(buffer).startsWith("HYBERNATE SUCCESS") ||
|
||||
new String(buffer).startsWith("not ready for") ||
|
||||
new String(buffer).startsWith("NFC_DISABLED") )
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
public static byte[] decodeblueReaderPacket(byte[] buffer, int len) {
|
||||
int cmdFound = 0;
|
||||
long timestamp = new Date().getTime();
|
||||
String bufferstring;
|
||||
//Log.w(TAG, "Packet: " + bufferstring);
|
||||
if (buffer == null) {
|
||||
Log.e(TAG, "null buffer passed to decodeblueReaderPacket");
|
||||
return null;
|
||||
} else {
|
||||
bufferstring=new String(buffer);
|
||||
}
|
||||
if (bufferstring.startsWith("not ready for") ) { //delete the trans_failed, because its normal only if the bluereader could not read the sensor.
|
||||
counterHibernated++;
|
||||
Log.e(TAG, "Found blueReader in a ugly State (" + counterHibernated + "/3), send hibernate to reset! If this does not help in the next 5 Minutes, then turn the bluereader manually off and on!");
|
||||
if (counterHibernated > 2) {
|
||||
Log.wtf(TAG, "Ugly state not resolveable. Bluereader will be shut down! Please restart it!");
|
||||
Home.toaststatic("BlueReader ugly state not resolveable, bluereader will be shut down. Please restart it!");
|
||||
if (!Pref.getBooleanDefaultFalse("blueReader_suppressuglystatemsg")) {
|
||||
Notifications.RiseDropAlert(xdrip.getAppContext(),true,"BlueReader Alarm", xdrip.getAppContext().getString(R.string.bluereaderuglystate),1);
|
||||
}
|
||||
return shutdown;
|
||||
} else {
|
||||
Home.toaststatic("Found blueReader in a ugly State, send hibernate to reset!");
|
||||
return goHybernate; //send hard hibernate, because blueReader is in a ugly state
|
||||
}
|
||||
} else if (bufferstring.startsWith("IDR")){
|
||||
Log.i(TAG, bufferstring);
|
||||
PersistentStore.setString("blueReaderFirmware", bufferstring );
|
||||
tempVers=Pattern.compile(".*\\|blue(.*)-.*").matcher(bufferstring);
|
||||
tempVers.find();
|
||||
PersistentStore.setDouble("blueReaderFirmwareValue",Double.parseDouble(tempVers.group(1)));
|
||||
Log.i(TAG, "bluereader-Firmware-Version: " + tempVers);
|
||||
if (BgReading.last() == null || BgReading.last().timestamp + (4 * 60 * 1000) < System.currentTimeMillis()) {
|
||||
return requestValue;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (bufferstring.startsWith("WAKE")) {
|
||||
Log.d (TAG, "blueReader was set to wakeup-mode manually...");
|
||||
return null;
|
||||
} else if (bufferstring.startsWith("ECHO")) {
|
||||
Log.d (TAG, "blueReader was set to Echo-Mode manually...");
|
||||
return null;
|
||||
} else if (bufferstring.startsWith("NFC READY")) {
|
||||
Log.d (TAG, "blueReader notice that NFC is active...");
|
||||
return null;
|
||||
} else if (bufferstring.startsWith("NFC_DISABLED")) {
|
||||
Log.d (TAG, "blueReader notice that NFC is now hibernated...");
|
||||
return null;
|
||||
} else if (bufferstring.startsWith("HYBERNATE SUCCESS")) {
|
||||
Log.i (TAG, "blueReader notice that NFC is now really hibernated...");
|
||||
if (counterHibernated > 0) {
|
||||
Log.w (TAG,"Found hibernation after wrong read. Resend read-command...");
|
||||
return requestValue;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (bufferstring.startsWith("-r 0:")) {
|
||||
Log.d (TAG, "blueReader sends an unknown reaction: '" + bufferstring + "'");
|
||||
return null;
|
||||
} else if (bufferstring.startsWith("TRANS_FAILED")) {
|
||||
Log.w (TAG, "Attention: check position of blueReader on the sensor, as it was not able to read!");
|
||||
Home.toaststatic(xdrip.getAppContext().getString(R.string.bluereader_position));
|
||||
return null;
|
||||
} else if (bufferstring.startsWith("battery: ")) {
|
||||
if (BgReading.last() == null || BgReading.last().timestamp + (4 * 60 * 1000) < System.currentTimeMillis()) {
|
||||
return requestValue;
|
||||
}
|
||||
} else {
|
||||
counterHibernated = 0;
|
||||
processNewTransmitterData(TransmitterData.create(buffer, len, timestamp), timestamp);
|
||||
// check for shutdown blueReader if Battery is too low
|
||||
if (Pref.getBooleanDefaultFalse("blueReader_turn_off")) {
|
||||
if (Pref.getInt("blueReader_turn_off_value",5) > Pref.getInt("bridge_battery",100)) {
|
||||
Log.w (TAG, "blueReader will be turn off, as the battery is lower then " + Pref.getInt("blueReader_turn_off_value",5) +"%");
|
||||
Home.toaststatic(xdrip.getAppContext().getString(R.string.bluereaderoff) + Pref.getInt("blueReader_turn_off_value",5) +"%");
|
||||
return shutdown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static synchronized void processNewTransmitterData(TransmitterData transmitterData, long timestamp) {
|
||||
if (transmitterData == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Sensor sensor = Sensor.currentSensor();
|
||||
if (sensor == null) {
|
||||
Log.i(TAG, "setSerialDataToTransmitterRawData: No Active Sensor, Data only stored in Transmitter Data");
|
||||
return;
|
||||
}
|
||||
|
||||
if(PersistentStore.getLong("blueReader_Full_Battery") <3000 )
|
||||
PersistentStore.setLong("blueReader_Full_Battery", 4100);
|
||||
|
||||
double blueReaderDays =0;
|
||||
if (transmitterData.sensor_battery_level > PersistentStore.getLong("blueReader_Full_Battery")) {
|
||||
PersistentStore.setLong("blueReader_Full_Battery", transmitterData.sensor_battery_level);
|
||||
Log.i(TAG, "blueReader_Full_Battery set to: " + transmitterData.sensor_battery_level) ;
|
||||
}
|
||||
int localBridgeBattery =((transmitterData.sensor_battery_level - 3300) * 100 / (((int) (long) PersistentStore.getLong("blueReader_Full_Battery"))-3300));
|
||||
Pref.setInt("bridge_battery", localBridgeBattery);
|
||||
sensor.latest_battery_level = localBridgeBattery;
|
||||
blueReaderDays = 6.129200670865791d / (1d + Math.pow(((double)transmitterData.sensor_battery_level/3763.700630306379d),(-61.04241888028577d))); //todo compare with test-formular, and new Data of batterylog
|
||||
if (transmitterData.sensor_battery_level < 3600) {
|
||||
blueReaderDays=blueReaderDays + 0.1d;
|
||||
}
|
||||
blueReaderDays = ((Math.round((blueReaderDays)*10d)/10d));
|
||||
|
||||
PersistentStore.setString("bridge_battery_days", String.valueOf(blueReaderDays));
|
||||
sensor.save();
|
||||
if (Pref.getBooleanDefaultFalse("blueReader_writebatterylog")) {
|
||||
final String dir = getExternalDir();
|
||||
makeSureDirectoryExists(dir);
|
||||
writeLog(dir + BatLog,
|
||||
DateFormat.format("yyyyMMdd-kkmmss", System.currentTimeMillis()).toString() + "|" +
|
||||
PersistentStore.getLong("blueReader_Full_Battery") + "|" +
|
||||
transmitterData.sensor_battery_level + "|" +
|
||||
sensor.latest_battery_level + "|" +
|
||||
blueReaderDays
|
||||
);
|
||||
}
|
||||
DexCollectionService.last_transmitter_Data = transmitterData;
|
||||
Log.d(TAG, "BgReading.create: new BG reading at " + timestamp + " with a timestamp of " + transmitterData.timestamp);
|
||||
BgReading.create(transmitterData.raw_data, transmitterData.filtered_data, xdrip.getAppContext(), transmitterData.timestamp);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static ByteBuffer initialize() {
|
||||
Log.i(TAG, "initialize blueReader!");
|
||||
Pref.setInt("bridge_battery", 0);
|
||||
PersistentStore.setDouble("blueReaderFirmwareValue", 0);
|
||||
|
||||
//command to get Firmware
|
||||
ByteBuffer ackMessage = ByteBuffer.allocate(3);
|
||||
ackMessage.put(0, (byte) 0x49);
|
||||
ackMessage.put(1, (byte) 0x44);
|
||||
ackMessage.put(2, (byte) 0x4E);
|
||||
return ackMessage;
|
||||
}
|
||||
|
||||
private static void writeLog(String logFile, String logLine) {
|
||||
PrintWriter pWriter = null;
|
||||
try {
|
||||
pWriter = new PrintWriter(new BufferedWriter(new FileWriter(logFile, true)));
|
||||
pWriter.println(logLine);
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "log write error: " + ioe.toString());
|
||||
} finally {
|
||||
if (pWriter != null){
|
||||
pWriter.flush();
|
||||
pWriter.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user