commit
5093b7b815
9 changed files with 297 additions and 95 deletions
|
@ -162,6 +162,10 @@ android {
|
|||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests.returnDefaultValues = true
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
|
|
|
@ -11,8 +11,11 @@ import org.json.JSONObject;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
import info.nightscout.androidaps.BuildConfig;
|
||||
import info.nightscout.androidaps.MainApp;
|
||||
|
@ -96,20 +99,39 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
|
|||
@NonNull
|
||||
private static final ComboPump pump = new ComboPump();
|
||||
|
||||
private volatile boolean bolusInProgress;
|
||||
/** This is used to determine when to pass a bolus cancel request to the scripter */
|
||||
private volatile boolean scripterIsBolusing;
|
||||
/** This is set to true to request a bolus cancellation. {@link #deliverBolus(DetailedBolusInfo)}
|
||||
* will reset this flag. */
|
||||
private volatile boolean cancelBolus;
|
||||
|
||||
/** Used to reject boluses with the same amount requested within two minutes.
|
||||
* Used solely by {@link #deliverBolus(DetailedBolusInfo)}. This is independent of the
|
||||
* pump history and is meant as a safety feature to block multiple requests due to an
|
||||
* application bug. Whether the requested bolus was delivered once is not taken into account. */
|
||||
private Bolus lastRequestedBolus;
|
||||
|
||||
/**
|
||||
* This is set whenever a connection to the pump is made and indicates if new history
|
||||
* records on the pump have been found. This effectively blocks high temps and boluses
|
||||
* till the queue is empty and the connection is shut down. The next reconnect will
|
||||
* then reset this flag. This might cause some grief when attempting to bolus again within
|
||||
* the 5s of idling it takes before the connecting is shut down.
|
||||
* This is set (in {@link #checkHistory()} whenever a connection to the pump is made and
|
||||
* indicates if new history records on the pump have been found. This effectively blocks
|
||||
* high temps ({@link #setTempBasalPercent(Integer, Integer)} and boluses
|
||||
* ({@link #deliverBolus(DetailedBolusInfo)} till the queue is empty and the connection
|
||||
* is shut down.
|
||||
* {@link #initializePump()} resets this since on startup the history is allowed to have
|
||||
* changed (and the user can't possible have already calculated anything with out of date IOB).
|
||||
* The next reconnect will then reset this flag. This might cause some grief when attempting
|
||||
* to bolus again within the 5s of idling it takes before the connecting is shut down. Or if
|
||||
* the queue is very large, giving the user more time to input boluses. I don't have a good
|
||||
* solution for this at the moment, but this is enough of an edge case - faulting in the right
|
||||
* direction - so that adding more complexity yields little benefit.
|
||||
*/
|
||||
private volatile boolean pumpHistoryChanged = false;
|
||||
private volatile long timestampOfLastKnownPumpBolusRecord;
|
||||
|
||||
/** Cache of the last <=2 boluses on the pump. Used to detect changes in pump history,
|
||||
* requiring reading pump more history. This is read/set in {@link #checkHistory()} when changed
|
||||
* pump history was detected and was read, as well as in {@link #deliverBolus(DetailedBolusInfo)}
|
||||
* after bolus delivery. */
|
||||
private volatile List<Bolus> recentBoluses = new ArrayList<>(0);
|
||||
|
||||
public static ComboPlugin getPlugin() {
|
||||
if (plugin == null)
|
||||
|
@ -118,7 +140,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
|
|||
}
|
||||
|
||||
private static final PumpEnactResult OPERATION_NOT_SUPPORTED = new PumpEnactResult()
|
||||
.success(false).enacted(false).comment(MainApp.sResources.getString(R.string.combo_pump_unsupported_operation));
|
||||
.success(false).enacted(false).comment(MainApp.gs(R.string.combo_pump_unsupported_operation));
|
||||
|
||||
private ComboPlugin() {
|
||||
ruffyScripter = new RuffyScripter(MainApp.instance().getApplicationContext());
|
||||
|
@ -370,7 +392,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
|
|||
}
|
||||
|
||||
// trigger a connect, which will update state and check history
|
||||
CommandResult stateResult = runCommand(null,1, ruffyScripter::readPumpState);
|
||||
CommandResult stateResult = runCommand(null, 1, ruffyScripter::readPumpState);
|
||||
if (!stateResult.success) {
|
||||
return;
|
||||
}
|
||||
|
@ -404,7 +426,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
|
|||
|
||||
// ComboFragment updates state fully only after the pump has initialized,
|
||||
// so force an update after initialization completed
|
||||
updateLocalData(runCommand(null, 1, ruffyScripter::readQuickInfo));
|
||||
MainApp.bus().post(new EventComboPumpUpdateGUI());
|
||||
}
|
||||
|
||||
/** Updates local cache with state (reservoir level, last bolus ...) returned from the pump */
|
||||
|
@ -489,42 +511,71 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
|
|||
|
||||
@NonNull
|
||||
private PumpEnactResult deliverBolus(final DetailedBolusInfo detailedBolusInfo) {
|
||||
// Guard against boluses issued multiple times within two minutes.
|
||||
// Two minutes, so that the resulting timestamp and bolus are different with the Combo
|
||||
// history records which only store with minute-precision
|
||||
if (lastRequestedBolus != null
|
||||
&& Math.abs(lastRequestedBolus.amount - detailedBolusInfo.insulin) < 0.01
|
||||
&& lastRequestedBolus.timestamp + 120 * 1000 > System.currentTimeMillis()) {
|
||||
log.error("Bolus request rejected, same bolus requested recently: " + lastRequestedBolus);
|
||||
return new PumpEnactResult().success(false).enacted(false)
|
||||
.comment(MainApp.gs(R.string.bolus_frequency_exceeded));
|
||||
}
|
||||
lastRequestedBolus = new Bolus(System.currentTimeMillis(), detailedBolusInfo.insulin, true);
|
||||
|
||||
// check pump is ready and all pump bolus records are known
|
||||
CommandResult stateResult = runCommand(null, 2, ruffyScripter::readQuickInfo);
|
||||
if (!stateResult.success) {
|
||||
return new PumpEnactResult().success(false).enacted(false)
|
||||
.comment(MainApp.gs(R.string.combo_error_no_connection_no_bolus_delivered));
|
||||
}
|
||||
if (stateResult.reservoirLevel != -1 && stateResult.reservoirLevel - 0.5 < detailedBolusInfo.insulin) {
|
||||
return new PumpEnactResult().success(false).enacted(false)
|
||||
.comment(MainApp.gs(R.string.combo_reservoir_level_insufficient_for_bolus));
|
||||
}
|
||||
// the commands above ensured a connection was made, which updated this field
|
||||
if (pumpHistoryChanged) {
|
||||
return new PumpEnactResult().success(false).enacted(false)
|
||||
.comment(MainApp.gs(R.string.combo_bolus_rejected_due_to_pump_history_change));
|
||||
}
|
||||
|
||||
Bolus previousBolus = stateResult.history != null && !stateResult.history.bolusHistory.isEmpty()
|
||||
? stateResult.history.bolusHistory.get(0)
|
||||
: new Bolus(0, 0, false);
|
||||
|
||||
try {
|
||||
pump.activity = MainApp.gs(R.string.combo_pump_action_bolusing, detailedBolusInfo.insulin);
|
||||
MainApp.bus().post(new EventComboPumpUpdateGUI());
|
||||
|
||||
// Guard against boluses issued multiple times within two minutes.
|
||||
// Two minutes, so that the resulting timestamp and bolus are different with the Combo
|
||||
// history records which only store with minute-precision
|
||||
if (lastRequestedBolus != null
|
||||
&& Math.abs(lastRequestedBolus.amount - detailedBolusInfo.insulin) < 0.01
|
||||
&& lastRequestedBolus.timestamp + 120 * 1000 > System.currentTimeMillis()) {
|
||||
log.error("Bolus request rejected, same bolus requested recently: " + lastRequestedBolus);
|
||||
return new PumpEnactResult().success(false).enacted(false)
|
||||
.comment(MainApp.gs(R.string.bolus_frequency_exceeded));
|
||||
}
|
||||
lastRequestedBolus = new Bolus(System.currentTimeMillis(), detailedBolusInfo.insulin, true);
|
||||
|
||||
// check pump is ready and all pump bolus records are known
|
||||
CommandResult stateResult = runCommand(null, 2, () -> ruffyScripter.readQuickInfo(1));
|
||||
if (!stateResult.success) {
|
||||
return new PumpEnactResult().success(false).enacted(false)
|
||||
.comment(MainApp.gs(R.string.combo_error_no_connection_no_bolus_delivered));
|
||||
}
|
||||
if (stateResult.reservoirLevel != -1 && stateResult.reservoirLevel - 0.5 < detailedBolusInfo.insulin) {
|
||||
return new PumpEnactResult().success(false).enacted(false)
|
||||
.comment(MainApp.gs(R.string.combo_reservoir_level_insufficient_for_bolus));
|
||||
}
|
||||
// the commands above ensured a connection was made, which updated this field
|
||||
if (pumpHistoryChanged) {
|
||||
return new PumpEnactResult().success(false).enacted(false)
|
||||
.comment(MainApp.gs(R.string.combo_bolus_rejected_due_to_pump_history_change));
|
||||
}
|
||||
|
||||
Bolus previousBolus = stateResult.history != null && !stateResult.history.bolusHistory.isEmpty()
|
||||
? stateResult.history.bolusHistory.get(0)
|
||||
: new Bolus(0, 0, false);
|
||||
|
||||
// if the last bolus was given in the current minute, wait till the pump clock moves
|
||||
// to the next minute to ensure timestamps are unique and can be imported
|
||||
CommandResult timeCheckResult = stateResult;
|
||||
long waitStartTime = System.currentTimeMillis();
|
||||
long maxWaitTimeout = waitStartTime + 65 * 1000;
|
||||
int waitLoops = 0;
|
||||
while (previousBolus.timestamp == timeCheckResult.state.pumpTime
|
||||
&& maxWaitTimeout > System.currentTimeMillis()) {
|
||||
if (cancelBolus) {
|
||||
return new PumpEnactResult().success(true).enacted(false);
|
||||
}
|
||||
if (!timeCheckResult.success) {
|
||||
return new PumpEnactResult().success(false).enacted(false)
|
||||
.comment(MainApp.gs(R.string.combo_error_no_connection_no_bolus_delivered));
|
||||
}
|
||||
log.debug("Waiting for pump clock to advance for the next unused bolus record timestamp");
|
||||
SystemClock.sleep(2000);
|
||||
timeCheckResult = runCommand(null, 0, ruffyScripter::readPumpState);
|
||||
waitLoops++;
|
||||
}
|
||||
if (waitLoops > 0) {
|
||||
long waitDuration = (System.currentTimeMillis() - waitStartTime) / 1000;
|
||||
Answers.getInstance().logCustom(new CustomEvent("ComboBolusTimestampWait")
|
||||
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
|
||||
.putCustomAttribute("version", BuildConfig.VERSION)
|
||||
.putCustomAttribute("waitTimeSecs", String.valueOf(waitDuration)));
|
||||
log.debug("Waited " + waitDuration + "s for pump to switch to a fresh minute before bolusing");
|
||||
}
|
||||
|
||||
if (cancelBolus) {
|
||||
return new PumpEnactResult().success(true).enacted(false);
|
||||
}
|
||||
|
@ -532,17 +583,18 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
|
|||
BolusProgressReporter progressReporter = detailedBolusInfo.isSMB ? nullBolusProgressReporter : bolusProgressReporter;
|
||||
|
||||
// start bolus delivery
|
||||
bolusInProgress = true;
|
||||
scripterIsBolusing = true;
|
||||
runCommand(null, 0,
|
||||
() -> ruffyScripter.deliverBolus(detailedBolusInfo.insulin, progressReporter));
|
||||
bolusInProgress = false;
|
||||
scripterIsBolusing = false;
|
||||
|
||||
// Note that the result of the issued bolus command is not checked. If there was
|
||||
// a connection problem, ruffyscripter tried to recover and we can just check the
|
||||
// history below to see what was actually delivered
|
||||
|
||||
// get last bolus from pump history for verification
|
||||
CommandResult postBolusStateResult = runCommand(null, 3, ruffyScripter::readQuickInfo);
|
||||
// (reads 2 records to update `recentBoluses` further down)
|
||||
CommandResult postBolusStateResult = runCommand(null, 3, () -> ruffyScripter.readQuickInfo(2));
|
||||
if (!postBolusStateResult.success) {
|
||||
return new PumpEnactResult().success(false).enacted(false)
|
||||
.comment(MainApp.gs(R.string.combo_error_bolus_verification_failed));
|
||||
|
@ -552,7 +604,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
|
|||
: null;
|
||||
|
||||
// no bolus delivered?
|
||||
if (lastPumpBolus == null || lastPumpBolus.equals(previousBolus) ) {
|
||||
if (lastPumpBolus == null || lastPumpBolus.equals(previousBolus)) {
|
||||
if (cancelBolus) {
|
||||
return new PumpEnactResult().success(true).enacted(false);
|
||||
} else {
|
||||
|
@ -568,7 +620,9 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
|
|||
return new PumpEnactResult().success(false).enacted(true)
|
||||
.comment(MainApp.gs(R.string.combo_error_updating_treatment_record));
|
||||
|
||||
timestampOfLastKnownPumpBolusRecord = lastPumpBolus.timestamp;
|
||||
// update `recentBoluses` so the bolus was just delivered won't be detected as a new
|
||||
// bolus that has been delivered on the pump
|
||||
recentBoluses = postBolusStateResult.history.bolusHistory;
|
||||
|
||||
// only a partial bolus was delivered
|
||||
if (Math.abs(lastPumpBolus.amount - detailedBolusInfo.insulin) > 0.01) {
|
||||
|
@ -602,7 +656,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
|
|||
private boolean addBolusToTreatments(DetailedBolusInfo detailedBolusInfo, Bolus lastPumpBolus) {
|
||||
DetailedBolusInfo dbi = detailedBolusInfo.copy();
|
||||
dbi.date = calculateFakeBolusDate(lastPumpBolus);
|
||||
dbi.pumpId = calculateFakeBolusDate(lastPumpBolus);
|
||||
dbi.pumpId = dbi.date;
|
||||
dbi.source = Source.PUMP;
|
||||
dbi.insulin = lastPumpBolus.amount;
|
||||
try {
|
||||
|
@ -610,23 +664,25 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
|
|||
if (!treatmentCreated) {
|
||||
log.error("Adding treatment record overrode an existing record: " + dbi);
|
||||
if (dbi.isSMB) {
|
||||
Notification notification = new Notification(Notification.COMBO_PUMP_ALARM, MainApp.sResources.getString(R.string.combo_error_updating_treatment_record), Notification.URGENT);
|
||||
Notification notification = new Notification(Notification.COMBO_PUMP_ALARM, MainApp.gs(R.string.combo_error_updating_treatment_record), Notification.URGENT);
|
||||
MainApp.bus().post(new EventNewNotification(notification));
|
||||
}
|
||||
Answers.getInstance().logCustom(new CustomEvent("ComboBolusToDbError")
|
||||
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
|
||||
.putCustomAttribute("version", BuildConfig.VERSION)
|
||||
.putCustomAttribute("bolus", String.valueOf(lastPumpBolus.amount))
|
||||
.putCustomAttribute("issue", "record with same timestamp existed and was overridden"));
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Adding treatment record failed", e);
|
||||
if (dbi.isSMB) {
|
||||
Notification notification = new Notification(Notification.COMBO_PUMP_ALARM, MainApp.sResources.getString(R.string.combo_error_updating_treatment_record), Notification.URGENT);
|
||||
Notification notification = new Notification(Notification.COMBO_PUMP_ALARM, MainApp.gs(R.string.combo_error_updating_treatment_record), Notification.URGENT);
|
||||
MainApp.bus().post(new EventNewNotification(notification));
|
||||
Answers.getInstance().logCustom(new CustomEvent("ComboBolusToDbError")
|
||||
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
|
||||
.putCustomAttribute("version", BuildConfig.VERSION)
|
||||
.putCustomAttribute("bolus", String.valueOf(lastPumpBolus.amount))
|
||||
.putCustomAttribute("issue", "adding record caused exception"));
|
||||
}
|
||||
return false;
|
||||
|
@ -636,7 +692,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
|
|||
|
||||
@Override
|
||||
public void stopBolusDelivering() {
|
||||
if (bolusInProgress) {
|
||||
if (scripterIsBolusing) {
|
||||
ruffyScripter.cancelBolus();
|
||||
}
|
||||
cancelBolus = true;
|
||||
|
@ -780,9 +836,11 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
|
|||
CommandResult commandResult;
|
||||
try {
|
||||
if (!ruffyScripter.isConnected()) {
|
||||
String originalActivity = pump.activity;
|
||||
pump.activity = MainApp.gs(R.string.combo_activity_checking_pump_state);
|
||||
MainApp.bus().post(new EventComboPumpUpdateGUI());
|
||||
CommandResult preCheckError = runOnConnectChecks();
|
||||
pump.activity = originalActivity;
|
||||
if (preCheckError != null) {
|
||||
updateLocalData(preCheckError);
|
||||
return preCheckError;
|
||||
|
@ -855,7 +913,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
|
|||
// turn benign warnings into notifications
|
||||
notifyAboutPumpWarning(activeAlert);
|
||||
ruffyScripter.confirmAlert(activeAlert.warningCode);
|
||||
} else if (activeAlert.errorCode != null){
|
||||
} else if (activeAlert.errorCode != null) {
|
||||
Notification notification = new Notification();
|
||||
notification.date = new Date();
|
||||
notification.id = Notification.COMBO_PUMP_ALARM;
|
||||
|
@ -1055,7 +1113,7 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
|
|||
for (Bolus pumpBolus : history.bolusHistory) {
|
||||
DetailedBolusInfo dbi = new DetailedBolusInfo();
|
||||
dbi.date = calculateFakeBolusDate(pumpBolus);
|
||||
dbi.pumpId = calculateFakeBolusDate(pumpBolus);
|
||||
dbi.pumpId = dbi.date;
|
||||
dbi.source = Source.PUMP;
|
||||
dbi.insulin = pumpBolus.amount;
|
||||
dbi.eventType = CareportalEvent.CORRECTIONBOLUS;
|
||||
|
@ -1067,13 +1125,15 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
|
|||
}
|
||||
|
||||
/** Adds the bolus to the timestamp to be able to differentiate multiple boluses in the same
|
||||
* minute. Best effort, since this covers only boluses up to 5.9 U and relies on other code
|
||||
* minute. Best effort, since this covers only boluses up to 6.0 U and relies on other code
|
||||
* to prevent a boluses with the same amount to be delivered within the same minute.
|
||||
* Should be good enough, even with command mode, it's a challenge to create that situation
|
||||
* and most time clashes will be around SMBs which are covered.
|
||||
*/
|
||||
private long calculateFakeBolusDate(Bolus pumpBolus) {
|
||||
return pumpBolus.timestamp + (Math.min((int) (pumpBolus.amount - 0.1) * 10 * 1000, 59 * 1000));
|
||||
long calculateFakeBolusDate(Bolus pumpBolus) {
|
||||
double bolus = pumpBolus.amount - 0.1;
|
||||
int secondsFromBolus = (int) (bolus * 10 * 1000);
|
||||
return pumpBolus.timestamp + Math.min(secondsFromBolus, 59 * 1000);
|
||||
}
|
||||
|
||||
// TODO use queue once ready
|
||||
|
@ -1126,26 +1186,65 @@ public class ComboPlugin implements PluginBase, PumpInterface, ConstraintsInterf
|
|||
* @return null on success or the failed command result
|
||||
*/
|
||||
private CommandResult checkHistory() {
|
||||
CommandResult quickInfoResult = runCommand(MainApp.gs(R.string.combo_activity_checking_for_history_changes), 3, ruffyScripter::readQuickInfo);
|
||||
if (quickInfoResult.history != null && !quickInfoResult.history.bolusHistory.isEmpty()
|
||||
&& quickInfoResult.history.bolusHistory.get(0).timestamp == timestampOfLastKnownPumpBolusRecord) {
|
||||
CommandResult quickInfoResult = runCommand(MainApp.gs(R.string.combo_activity_checking_for_history_changes), 3,
|
||||
() -> ruffyScripter.readQuickInfo(2));
|
||||
|
||||
// no history, nothing to check or complain about
|
||||
if (quickInfoResult.history == null || quickInfoResult.history.bolusHistory.isEmpty()) {
|
||||
log.debug("Setting 'pumpHistoryChanged' false");
|
||||
pumpHistoryChanged = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
// OPTIMIZE this reads the entire history on start, so this could be optimized by persisting
|
||||
// `timestampOfLastKnownPumpBolusRecord`, though this should be thought through, to make sure
|
||||
// all scenarios are covered
|
||||
// compare recent records
|
||||
List<Bolus> initialPumpBolusHistory = quickInfoResult.history.bolusHistory;
|
||||
if (recentBoluses.size() == 1 && initialPumpBolusHistory.size() >= 1
|
||||
&& recentBoluses.get(0).equals(quickInfoResult.history.bolusHistory.get(0))) {
|
||||
log.debug("Setting 'pumpHistoryChanged' false");
|
||||
pumpHistoryChanged = false;
|
||||
return null;
|
||||
} else if (recentBoluses.size() == 2 && initialPumpBolusHistory.size() >= 2
|
||||
&& recentBoluses.get(0).equals(quickInfoResult.history.bolusHistory.get(0))
|
||||
&& recentBoluses.get(1).equals(quickInfoResult.history.bolusHistory.get(1))) {
|
||||
log.debug("Setting 'pumpHistoryChanged' false");
|
||||
pumpHistoryChanged = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
// fetch new records
|
||||
long lastKnownPumpRecordTimestamp = recentBoluses.isEmpty() ? 0 : recentBoluses.get(0).timestamp;
|
||||
CommandResult historyResult = runCommand(MainApp.gs(R.string.combo_activity_reading_pump_history), 3, () ->
|
||||
ruffyScripter.readHistory(new PumpHistoryRequest()
|
||||
.bolusHistory(timestampOfLastKnownPumpBolusRecord)));
|
||||
ruffyScripter.readHistory(new PumpHistoryRequest().bolusHistory(lastKnownPumpRecordTimestamp)));
|
||||
if (!historyResult.success) {
|
||||
pumpHistoryChanged = true;
|
||||
return historyResult;
|
||||
}
|
||||
|
||||
pumpHistoryChanged = updateDbFromPumpHistory(historyResult.history);
|
||||
// Check edge of multiple boluses with the same amount in the same minute being imported.
|
||||
// This is about as edgy-casey as it can get. I'd be surprised of this one actually ever
|
||||
// triggers. It might, so at least give a warning, since a delivered bolus isn't accounted
|
||||
// for.
|
||||
HashSet<Bolus> bolusSet = new HashSet<>(historyResult.history.bolusHistory);
|
||||
if (bolusSet.size() != historyResult.history.bolusHistory.size()) {
|
||||
log.debug("Bolus with same amount within the same minute imported. Only one will make it to the DB.");
|
||||
Answers.getInstance().logCustom(new CustomEvent("ComboBolusToDbError")
|
||||
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
|
||||
.putCustomAttribute("version", BuildConfig.VERSION)
|
||||
.putCustomAttribute("bolus", "")
|
||||
.putCustomAttribute("issue", "multiple pump history records with the same time and amount"));
|
||||
Notification notification = new Notification(Notification.COMBO_PUMP_ALARM, MainApp.gs(R.string.
|
||||
combo_error_multiple_boluses_with_identical_timestamp), Notification.URGENT);
|
||||
MainApp.bus().post(new EventNewNotification(notification));
|
||||
}
|
||||
|
||||
if (!historyResult.history.bolusHistory.isEmpty()) {
|
||||
timestampOfLastKnownPumpBolusRecord = historyResult.history.bolusHistory.get(0).timestamp;
|
||||
pumpHistoryChanged = updateDbFromPumpHistory(historyResult.history);
|
||||
if (pumpHistoryChanged) {
|
||||
log.debug("Setting 'pumpHistoryChanged' true");
|
||||
}
|
||||
|
||||
List<Bolus> updatedPumpBolusHistory = historyResult.history.bolusHistory;
|
||||
if (!updatedPumpBolusHistory.isEmpty()) {
|
||||
recentBoluses = updatedPumpBolusHistory.subList(0, Math.min(updatedPumpBolusHistory.size(), 2));
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -32,7 +32,7 @@ public interface RuffyCommands {
|
|||
CommandResult readPumpState();
|
||||
|
||||
/** Read reservoir level and last bolus via Quick Info */
|
||||
CommandResult readQuickInfo();
|
||||
CommandResult readQuickInfo(int numberOfBolusRecordsToRetrieve);
|
||||
|
||||
/** Reads pump history via the My Data menu. The {@link PumpHistoryRequest} specifies
|
||||
* what types of data and how far back data is returned. */
|
||||
|
|
|
@ -48,8 +48,6 @@ import info.nightscout.androidaps.BuildConfig;
|
|||
* operations and are cleanly separated from the thread management, connection management etc
|
||||
*/
|
||||
public class RuffyScripter implements RuffyCommands {
|
||||
private final boolean readQuickInfo = true;
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(RuffyScripter.class);
|
||||
|
||||
private IRuffyService ruffyService;
|
||||
|
@ -225,17 +223,11 @@ public class RuffyScripter implements RuffyCommands {
|
|||
}
|
||||
|
||||
@Override
|
||||
public CommandResult readQuickInfo() {
|
||||
if (readQuickInfo) {
|
||||
Answers.getInstance().logCustom(new CustomEvent("ComboReadQuickInfoCmd")
|
||||
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
|
||||
.putCustomAttribute("version", BuildConfig.VERSION));
|
||||
return runCommand(new ReadQuickInfoCommand());
|
||||
}
|
||||
Answers.getInstance().logCustom(new CustomEvent("ComboReadHistoryCmd")
|
||||
public CommandResult readQuickInfo(int numberOfBolusRecordsToRetrieve) {
|
||||
Answers.getInstance().logCustom(new CustomEvent("ComboReadQuickInfoCmd")
|
||||
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
|
||||
.putCustomAttribute("version", BuildConfig.VERSION));
|
||||
return runCommand(new ReadHistoryCommand(new PumpHistoryRequest().bolusHistory(PumpHistoryRequest.LAST)));
|
||||
return runCommand(new ReadQuickInfoCommand(numberOfBolusRecordsToRetrieve));
|
||||
}
|
||||
|
||||
public void returnToRootMenu() {
|
||||
|
@ -436,7 +428,7 @@ public class RuffyScripter implements RuffyCommands {
|
|||
Answers.getInstance().logCustom(new CustomEvent("ComboRecoveryFromConnectionLoss")
|
||||
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
|
||||
.putCustomAttribute("version", BuildConfig.VERSION)
|
||||
.putCustomAttribute("activeCommand", "" + activeCmd)
|
||||
.putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : ""))
|
||||
.putCustomAttribute("success", connected ? "true" : "else"));
|
||||
return connected;
|
||||
}
|
||||
|
@ -451,7 +443,8 @@ public class RuffyScripter implements RuffyCommands {
|
|||
Answers.getInstance().logCustom(new CustomEvent("ComboRecoveryFromCommandFailure")
|
||||
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
|
||||
.putCustomAttribute("version", BuildConfig.VERSION)
|
||||
.putCustomAttribute("activeCommand", "" + activeCmd)
|
||||
.putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : ""))
|
||||
.putCustomAttribute("exit", "1")
|
||||
.putCustomAttribute("success", "false"));
|
||||
return new PumpState();
|
||||
}
|
||||
|
@ -469,15 +462,18 @@ public class RuffyScripter implements RuffyCommands {
|
|||
Answers.getInstance().logCustom(new CustomEvent("ComboRecoveryFromCommandFailure")
|
||||
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
|
||||
.putCustomAttribute("version", BuildConfig.VERSION)
|
||||
.putCustomAttribute("activeCommand", "" + activeCmd)
|
||||
.putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : ""))
|
||||
.putCustomAttribute("exit", "2")
|
||||
.putCustomAttribute("success", "true"));
|
||||
return pumpState;
|
||||
} catch (Exception e) {
|
||||
Answers.getInstance().logCustom(new CustomEvent("ComboRecoveryFromCommandFailure")
|
||||
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
|
||||
.putCustomAttribute("version", BuildConfig.VERSION)
|
||||
.putCustomAttribute("activeCommand", "" + activeCmd)
|
||||
.putCustomAttribute("exit", "3")
|
||||
.putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : ""))
|
||||
.putCustomAttribute("success", "false"));
|
||||
|
||||
log.debug("Reading pump state during recovery failed", e);
|
||||
return new PumpState();
|
||||
}
|
||||
|
@ -502,7 +498,7 @@ public class RuffyScripter implements RuffyCommands {
|
|||
Answers.getInstance().logCustom(new CustomEvent("ComboConnectTimeout")
|
||||
.putCustomAttribute("buildversion", BuildConfig.BUILDVERSION)
|
||||
.putCustomAttribute("version", BuildConfig.VERSION)
|
||||
.putCustomAttribute("activeCommand", "" + activeCmd)
|
||||
.putCustomAttribute("activeCommand", "" + (activeCmd != null ? activeCmd.getClass().getSimpleName() : ""))
|
||||
.putCustomAttribute("previousCommand", previousCommand));
|
||||
throw new CommandException("Timeout connecting to pump");
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import android.support.annotation.NonNull;
|
|||
|
||||
import org.monkey.d.ruffy.ruffy.driver.display.MenuAttribute;
|
||||
import org.monkey.d.ruffy.ruffy.driver.display.MenuType;
|
||||
import org.monkey.d.ruffy.ruffy.driver.display.menu.BolusType;
|
||||
import org.monkey.d.ruffy.ruffy.driver.display.menu.MenuDate;
|
||||
import org.monkey.d.ruffy.ruffy.driver.display.menu.MenuTime;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -145,7 +144,7 @@ public class ReadHistoryCommand extends BaseCommand {
|
|||
int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD);
|
||||
while (true) {
|
||||
Tdd tdd = readTddRecord();
|
||||
if (requestedTime != PumpHistoryRequest.FULL && tdd.timestamp <= requestedTime) {
|
||||
if (requestedTime != PumpHistoryRequest.FULL && tdd.timestamp < requestedTime) {
|
||||
break;
|
||||
}
|
||||
log.debug("Read TDD record #" + record + "/" + totalRecords);
|
||||
|
@ -183,7 +182,7 @@ public class ReadHistoryCommand extends BaseCommand {
|
|||
int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD);
|
||||
while (true) {
|
||||
Tbr tbr = readTbrRecord();
|
||||
if (requestedTime != PumpHistoryRequest.FULL && tbr.timestamp <= requestedTime) {
|
||||
if (requestedTime != PumpHistoryRequest.FULL && tbr.timestamp < requestedTime) {
|
||||
break;
|
||||
}
|
||||
log.debug("Read TBR record #" + record + "/" + totalRecords);
|
||||
|
@ -215,7 +214,7 @@ public class ReadHistoryCommand extends BaseCommand {
|
|||
int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD);
|
||||
while (true) {
|
||||
Bolus bolus = readBolusRecord();
|
||||
if (requestedTime != PumpHistoryRequest.FULL && bolus.timestamp <= requestedTime) {
|
||||
if (requestedTime != PumpHistoryRequest.FULL && bolus.timestamp < requestedTime) {
|
||||
break;
|
||||
}
|
||||
log.debug("Read bolus record #" + record + "/" + totalRecords);
|
||||
|
@ -237,7 +236,7 @@ public class ReadHistoryCommand extends BaseCommand {
|
|||
int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD);
|
||||
while (true) {
|
||||
PumpAlert error = readAlertRecord();
|
||||
if (requestedTime != PumpHistoryRequest.FULL && error.timestamp <= requestedTime) {
|
||||
if (requestedTime != PumpHistoryRequest.FULL && error.timestamp < requestedTime) {
|
||||
break;
|
||||
}
|
||||
log.debug("Read alert record #" + record + "/" + totalRecords);
|
||||
|
|
|
@ -2,26 +2,64 @@ package info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.commands;
|
|||
|
||||
import org.monkey.d.ruffy.ruffy.driver.display.MenuAttribute;
|
||||
import org.monkey.d.ruffy.ruffy.driver.display.MenuType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.Bolus;
|
||||
import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.PumpHistory;
|
||||
|
||||
public class ReadQuickInfoCommand extends BaseCommand {
|
||||
private static final Logger log = LoggerFactory.getLogger(ReadQuickInfoCommand.class);
|
||||
|
||||
private final int numberOfBolusRecordsToRetrieve;
|
||||
|
||||
public ReadQuickInfoCommand(int numberOfBolusRecordsToRetrieve) {
|
||||
this.numberOfBolusRecordsToRetrieve = numberOfBolusRecordsToRetrieve;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute() {
|
||||
scripter.verifyRootMenuIsDisplayed();
|
||||
// navigate to reservoir menu
|
||||
scripter.pressCheckKey();
|
||||
scripter.waitForMenuToBeLeft(MenuType.MAIN_MENU);
|
||||
scripter.waitForMenuToBeLeft(MenuType.STOP);
|
||||
scripter.verifyMenuIsDisplayed(MenuType.QUICK_INFO);
|
||||
result.reservoirLevel = ((Double) scripter.getCurrentMenu().getAttribute(MenuAttribute.REMAINING_INSULIN)).intValue();
|
||||
scripter.pressCheckKey();
|
||||
List<Bolus> bolusHistory = new ArrayList<>(1);
|
||||
bolusHistory.add(readBolusRecord());
|
||||
result.history = new PumpHistory().bolusHistory(bolusHistory);
|
||||
if (numberOfBolusRecordsToRetrieve > 0) {
|
||||
// navigate to bolus data menu
|
||||
scripter.pressCheckKey();
|
||||
scripter.verifyMenuIsDisplayed(MenuType.BOLUS_DATA);
|
||||
List<Bolus> bolusHistory = new ArrayList<>(numberOfBolusRecordsToRetrieve);
|
||||
result.history = new PumpHistory().bolusHistory(bolusHistory);
|
||||
// read bolus records
|
||||
int totalRecords = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.TOTAL_RECORD);
|
||||
int record = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD);
|
||||
while (true) {
|
||||
bolusHistory.add(readBolusRecord());
|
||||
if (bolusHistory.size() == numberOfBolusRecordsToRetrieve || record == totalRecords) {
|
||||
break;
|
||||
}
|
||||
// advance to next record
|
||||
scripter.pressDownKey();
|
||||
while (record == (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD)) {
|
||||
scripter.waitForScreenUpdate();
|
||||
}
|
||||
record = (int) scripter.getCurrentMenu().getAttribute(MenuAttribute.CURRENT_RECORD);
|
||||
}
|
||||
if (log.isDebugEnabled()) {
|
||||
if (!result.history.bolusHistory.isEmpty()) {
|
||||
log.debug("Read bolus history (" + result.history.bolusHistory.size() + "):");
|
||||
for (Bolus bolus : result.history.bolusHistory) {
|
||||
log.debug(new Date(bolus.timestamp) + ": " + bolus.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
scripter.returnToRootMenu();
|
||||
result.success = true;
|
||||
}
|
||||
|
|
|
@ -5,8 +5,10 @@ import java.util.Date;
|
|||
/** What data a 'read history' request should return. */
|
||||
public class PumpHistoryRequest {
|
||||
/* History to read:
|
||||
Either the timestamp of the last known record to fetch all newer records,
|
||||
or one of the constants to read no history or all of it.
|
||||
Either the timestamp of the last known record or one of the constants to read no history
|
||||
or all of it. When a timestamp is provided all newer records and records matching the
|
||||
timestamp are returned. Returning all records equal to the timestamp ensures a record
|
||||
with a duplicate timestamp is also detected as a new record.
|
||||
*/
|
||||
public static final long LAST = -2;
|
||||
public static final long SKIP = -1;
|
||||
|
|
|
@ -874,5 +874,6 @@
|
|||
<string name="combo_warning_pump_basal_rate_changed">The basal rate on the pump has changed and will be updated soon</string>
|
||||
<string name="combo_error_failure_reading_changed_basal_rate">Basal rate changed on pump, but reading it failed</string>
|
||||
<string name="combo_activity_checking_for_history_changes">Checking for history changes</string>
|
||||
<string name="combo_error_multiple_boluses_with_identical_timestamp">Multiple boluses with the same amount within the same minute were just imported. Only one record could be added to treatments. Please check the pump and manually add a bolus record using the Careportal tab. Make sure to create a bolus with a time no other bolus uses.</string>
|
||||
</resources>
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package info.nightscout.androidaps.plugins.PumpCombo;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.squareup.otto.Bus;
|
||||
import com.squareup.otto.ThreadEnforcer;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.powermock.api.mockito.PowerMockito;
|
||||
import org.powermock.core.classloader.annotations.PrepareForTest;
|
||||
import org.powermock.modules.junit4.PowerMockRunner;
|
||||
|
||||
import info.nightscout.androidaps.MainApp;
|
||||
import info.nightscout.androidaps.plugins.ConfigBuilder.ConfigBuilderPlugin;
|
||||
import info.nightscout.androidaps.plugins.PumpCombo.ruffyscripter.history.Bolus;
|
||||
import info.nightscout.utils.ToastUtils;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(PowerMockRunner.class)
|
||||
@PrepareForTest({MainApp.class, ConfigBuilderPlugin.class, ConfigBuilderPlugin.class, ToastUtils.class, Context.class})
|
||||
public class ComboPluginTest {
|
||||
|
||||
@Before
|
||||
public void prepareMocks() throws Exception {
|
||||
ConfigBuilderPlugin configBuilderPlugin = mock(ConfigBuilderPlugin.class);
|
||||
PowerMockito.mockStatic(ConfigBuilderPlugin.class);
|
||||
|
||||
PowerMockito.mockStatic(MainApp.class);
|
||||
MainApp mainApp = mock(MainApp.class);
|
||||
when(MainApp.getConfigBuilder()).thenReturn(configBuilderPlugin);
|
||||
when(MainApp.instance()).thenReturn(mainApp);
|
||||
Bus bus = new Bus(ThreadEnforcer.ANY);
|
||||
when(MainApp.bus()).thenReturn(bus);
|
||||
when(MainApp.gs(0)).thenReturn("");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void calculateFakePumpTimestamp() throws Exception {
|
||||
ComboPlugin plugin = ComboPlugin.getPlugin();
|
||||
long now = System.currentTimeMillis();
|
||||
long pumpTimestamp = now - now % 1000;
|
||||
// same timestamp, different bolus leads to different fake timestamp
|
||||
Assert.assertNotEquals(
|
||||
plugin.calculateFakeBolusDate(new Bolus(pumpTimestamp, 0.1, true)),
|
||||
plugin.calculateFakeBolusDate(new Bolus(pumpTimestamp, 0.3, true))
|
||||
);
|
||||
// different timestamp, same bolus leads to different fake timestamp
|
||||
Assert.assertNotEquals(
|
||||
plugin.calculateFakeBolusDate(new Bolus(pumpTimestamp, 0.3, true)),
|
||||
plugin.calculateFakeBolusDate(new Bolus(pumpTimestamp + 60 * 1000, 0.3, true))
|
||||
);
|
||||
// generated timestamp has second-precision
|
||||
Bolus bolus = new Bolus(pumpTimestamp, 0.2, true);
|
||||
long calculatedTimestamp = plugin.calculateFakeBolusDate(bolus);
|
||||
assertEquals(calculatedTimestamp, calculatedTimestamp - calculatedTimestamp % 1000);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue