diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/ChooseTriggerDialog.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/ChooseTriggerDialog.java index cd251add31..956d802be0 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/ChooseTriggerDialog.java +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/dialogs/ChooseTriggerDialog.java @@ -19,6 +19,7 @@ import butterknife.Unbinder; import info.nightscout.androidaps.R; import info.nightscout.androidaps.plugins.general.automation.triggers.Trigger; import info.nightscout.androidaps.plugins.general.automation.triggers.TriggerBg; +import info.nightscout.androidaps.plugins.general.automation.triggers.TriggerDelta; import info.nightscout.androidaps.plugins.general.automation.triggers.TriggerIob; import info.nightscout.androidaps.plugins.general.automation.triggers.TriggerLocation; import info.nightscout.androidaps.plugins.general.automation.triggers.TriggerProfilePercent; @@ -37,6 +38,7 @@ public class ChooseTriggerDialog extends DialogFragment { add(new TriggerTime()); add(new TriggerRecurringTime()); add(new TriggerBg()); + add(new TriggerDelta()); add(new TriggerIob()); add(new TriggerProfilePercent()); add(new TriggerTempTarget()); diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputDelta.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputDelta.java new file mode 100644 index 0000000000..d75d73e495 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/elements/InputDelta.java @@ -0,0 +1,132 @@ +package info.nightscout.androidaps.plugins.general.automation.elements; + +import android.text.Editable; +import android.text.TextWatcher; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.LinearLayout; +import android.widget.Spinner; + +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.List; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.utils.NumberPicker; + +public class InputDelta extends Element { + private Comparator.Compare compare = Comparator.Compare.IS_EQUAL; + + final TextWatcher textWatcher = new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + }; + + private double value; + double minValue; + double maxValue; + private double step; + private DecimalFormat decimalFormat; + private int deltaType; + + NumberPicker numberPicker; + + public InputDelta() { + super(); + } + + public InputDelta(double value, double minValue, double maxValue, double step, DecimalFormat decimalFormat, int deltaType) { + super(); + this.value = value; + this.minValue = minValue; + this.maxValue = maxValue; + this.step = step; + this.decimalFormat = decimalFormat; + this.deltaType = deltaType; + } + + public InputDelta(InputDelta another) { + super(); + value = another.getValue(); + minValue = another.minValue; + maxValue = another.maxValue; + step = another.step; + decimalFormat = another.decimalFormat; + deltaType = another.deltaType; + } + + + @Override + public void addToLayout(LinearLayout root) { + Spinner spinner = new Spinner(root.getContext()); + ArrayAdapter spinnerArrayAdapter = new ArrayAdapter<>(root.getContext(), android.R.layout.simple_spinner_item, InputDelta.labels()); + spinnerArrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(spinnerArrayAdapter); + LinearLayout.LayoutParams spinnerParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + ); + spinnerParams.setMargins(0, MainApp.dpToPx(4), 0, MainApp.dpToPx(4)); + spinner.setLayoutParams(spinnerParams); + spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int position, long id) { + deltaType = spinner.getSelectedItemPosition(); + } + + @Override + public void onNothingSelected(AdapterView parent) { + } + }); + spinner.setSelection(this.deltaType); +// root.addView(spinner); + numberPicker = new NumberPicker(root.getContext(), null); + numberPicker.setParams(value, minValue, maxValue, step, decimalFormat, true, textWatcher); + numberPicker.setOnValueChangedListener(value -> this.value = value); + LinearLayout l = new LinearLayout(root.getContext()); + l.setOrientation(LinearLayout.VERTICAL); + l.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + l.addView(spinner); + l.addView(numberPicker); + root.addView(l); + + } + + public InputDelta setValue(double value, int type) { + this.value = value; + this.deltaType = type; + if (numberPicker != null) + numberPicker.setValue(value); + return this; + } + + public double getValue() { + return value; + } + + public int getDeltaType() { + return deltaType; + } + + public static List labels() { + List list = new ArrayList<>(); + list.add(MainApp.gs(R.string.delta)); + list.add(MainApp.gs(R.string.short_avgdelta)); + list.add(MainApp.gs(R.string.long_avgdelta)); + return list; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerDelta.java b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerDelta.java new file mode 100644 index 0000000000..a9f1a7d2ae --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerDelta.java @@ -0,0 +1,220 @@ +package info.nightscout.androidaps.plugins.general.automation.triggers; + + +import android.support.v4.app.FragmentManager; +import android.widget.LinearLayout; + +import com.google.common.base.Optional; + +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.DecimalFormat; + +import info.nightscout.androidaps.Constants; +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.data.Profile; +import info.nightscout.androidaps.logging.L; +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctions; +import info.nightscout.androidaps.plugins.general.automation.elements.Comparator; +import info.nightscout.androidaps.plugins.general.automation.elements.InputDelta; +import info.nightscout.androidaps.plugins.general.automation.elements.LabelWithElement; +import info.nightscout.androidaps.plugins.general.automation.elements.LayoutBuilder; +import info.nightscout.androidaps.plugins.general.automation.elements.StaticLabel; +import info.nightscout.androidaps.plugins.iob.iobCobCalculator.GlucoseStatus; +import info.nightscout.androidaps.utils.DateUtil; +import info.nightscout.androidaps.utils.JsonHelper; +import info.nightscout.androidaps.utils.T; + +public class TriggerDelta extends Trigger { + private static Logger log = LoggerFactory.getLogger(L.AUTOMATION); + private double minValue = 0d; + private double maxValue = 1d; + private double step = 1; + private DecimalFormat decimalFormat = new DecimalFormat("1"); + private String units = ProfileFunctions.getInstance().getProfileUnits(); + private int deltaType = 0; // 0 is delta, 1 is short average delta, 2 is long average delta + + private InputDelta value = new InputDelta( (double) minValue,(double) minValue, (double) maxValue, step, decimalFormat, deltaType); + private Comparator comparator = new Comparator(); + + public TriggerDelta() { + super(); + setUnits(); + } + + private TriggerDelta(TriggerDelta triggerDelta) { + super(); + setUnits(); + value = triggerDelta.value; + lastRun = triggerDelta.lastRun; + } + + public double getValue() { + return value.getValue(); + } + + public double getType() { return value.getDeltaType(); } + + public String getUnits() { + return this.units; + } + + public Comparator getComparator() { + return comparator; + } + + @Override + public synchronized boolean shouldRun() { + GlucoseStatus glucoseStatus = GlucoseStatus.getGlucoseStatusData(); + if (glucoseStatus == null) + if (comparator.getValue() == Comparator.Compare.IS_NOT_AVAILABLE) + return true; + else + return false; + + // Setting type of delta + double delta; + + if (deltaType == 1) + delta = glucoseStatus.short_avgdelta; + else if (deltaType == 2) + delta = glucoseStatus.long_avgdelta; + else + delta = glucoseStatus.delta; + + if (lastRun > DateUtil.now() - T.mins(5).msecs()) + return false; + + if (glucoseStatus == null && comparator.getValue().equals(Comparator.Compare.IS_NOT_AVAILABLE)) { + if (L.isEnabled(L.AUTOMATION)) + log.debug("Ready for execution: delta is " + delta + friendlyDescription()); + return true; + } + + boolean doRun = comparator.getValue().check(delta, Profile.toMgdl(value.getValue(), this.units)); + if (doRun) { + if (L.isEnabled(L.AUTOMATION)) + log.debug("Ready for execution: delta is " + delta + " " + friendlyDescription()); + return true; + } + return false; + } + + @Override + public synchronized String toJSON() { + JSONObject o = new JSONObject(); + try { + o.put("type", TriggerDelta.class.getName()); + JSONObject data = new JSONObject(); + data.put("value", getValue()); + data.put("units", units); + data.put("lastRun", lastRun); + data.put("type", getType()); + data.put("comparator", comparator.getValue().toString()); + o.put("data", data); + } catch (JSONException e) { + e.printStackTrace(); + } + return o.toString(); + } + + @Override + Trigger fromJSON(String data) { + try { + JSONObject d = new JSONObject(data); + units = JsonHelper.safeGetString(d, "units"); + deltaType = JsonHelper.safeGetInt(d, "type"); + value.setValue(JsonHelper.safeGetDouble(d, "value"), deltaType); + lastRun = JsonHelper.safeGetLong(d, "lastRun"); + comparator.setValue(Comparator.Compare.valueOf(JsonHelper.safeGetString(d, "comparator"))); + } catch (Exception e) { + e.printStackTrace(); + } + return this; + } + + @Override + public int friendlyName() { + return R.string.deltalabel; + } + + @Override + public String friendlyDescription() { + return MainApp.gs(R.string.deltacompared, MainApp.gs(comparator.getValue().getStringRes()), getValue(), typeToString(deltaType)); + } + + @Override + public Optional icon() { + return Optional.of(R.drawable.as); // TODO: Icon for delta + } + + @Override + public Trigger duplicate() { + return new TriggerDelta(this); + } + + TriggerDelta setValue(double requestedValue) { + this.value.setValue(requestedValue, deltaType); + return this; + } + + TriggerDelta setUnits(String units) { + this.units = units; + return this; + } + + void setUnits(){ + if (this.units.equals(Constants.MMOL)) { + this.maxValue = 4d; + this.minValue = 0.1d; + this.step = 0.1d; + this.decimalFormat = new DecimalFormat("0.1"); + this.deltaType = 0; + } else { + this.maxValue = 72d; + this.minValue = 2d; + this.step = 1d; + this.deltaType = 0; + } + value = new InputDelta( (double) minValue,(double) minValue, (double) maxValue, step, decimalFormat, deltaType); + } + + + TriggerDelta lastRun(long lastRun) { + this.lastRun = lastRun; + return this; + } + + TriggerDelta comparator(Comparator.Compare compare) { + this.comparator = new Comparator().setValue(compare); + return this; + } + + @Override + public void generateDialog(LinearLayout root, FragmentManager fragmentManager) { + + new LayoutBuilder() + .add(new StaticLabel(R.string.deltalabel)) + .add(comparator) + .add(new LabelWithElement(MainApp.gs(R.string.deltalabel) + ": ", "", value)) + .build(root); + } + + public String typeToString( int type ) { + switch (type) { + case 0: + return MainApp.gs(R.string.delta); + case 1: + return MainApp.gs(R.string.short_avgdelta); + case 2: + return MainApp.gs(R.string.long_avgdelta); + default: + return MainApp.gs(R.string.delta); + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/as.png b/app/src/main/res/drawable/as.png new file mode 100644 index 0000000000..a37e4c3465 Binary files /dev/null and b/app/src/main/res/drawable/as.png differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dee0a621f8..f258c3ef9d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1400,6 +1400,8 @@ not exists Temp target %1$s WiFi SSID %1$s %2$s + BG %3$s %1$s %2$s + BG difference Current Location Location Lat: @@ -1420,4 +1422,4 @@ %1$d minute %1$d minutes - + \ No newline at end of file diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerDeltaTest.java b/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerDeltaTest.java new file mode 100644 index 0000000000..fc86f9adb3 --- /dev/null +++ b/app/src/test/java/info/nightscout/androidaps/plugins/general/automation/triggers/TriggerDeltaTest.java @@ -0,0 +1,151 @@ +package info.nightscout.androidaps.plugins.general.automation.triggers; + +import com.google.common.base.Optional; +import com.squareup.otto.Bus; + +import org.json.JSONException; +import org.json.JSONObject; +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 java.util.ArrayList; +import java.util.List; + +import info.AAPSMocker; +import info.nightscout.androidaps.Constants; +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.db.BgReading; +import info.nightscout.androidaps.plugins.configBuilder.ProfileFunctions; +import info.nightscout.androidaps.plugins.general.automation.elements.Comparator; +import info.nightscout.androidaps.plugins.general.nsclient.data.NSSgv; +import info.nightscout.androidaps.plugins.iob.iobCobCalculator.GlucoseStatus; +import info.nightscout.androidaps.plugins.iob.iobCobCalculator.IobCobCalculatorPlugin; +import info.nightscout.androidaps.utils.DateUtil; + +import static org.powermock.api.mockito.PowerMockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareForTest({MainApp.class, Bus.class, ProfileFunctions.class, DateUtil.class, IobCobCalculatorPlugin.class, GlucoseStatus.class}) +public class TriggerDeltaTest { + + long now = 1514766900000L; + + @Test + public void shouldRunTest() { + when(IobCobCalculatorPlugin.getPlugin().getBgReadings()).thenReturn(generateValidBgData()); + + TriggerDelta t = new TriggerDelta().setUnits(Constants.MGDL).setValue(73d).comparator(Comparator.Compare.IS_EQUAL); + Assert.assertFalse(t.shouldRun()); + t = new TriggerDelta().setUnits(Constants.MGDL).setValue(-2d).comparator(Comparator.Compare.IS_EQUAL); + Assert.assertTrue(t.shouldRun()); + t = new TriggerDelta().setUnits(Constants.MGDL).setValue(-3d).comparator(Comparator.Compare.IS_EQUAL_OR_GREATER); + Assert.assertTrue(t.shouldRun()); + t = new TriggerDelta().setUnits(Constants.MGDL).setValue(2d).comparator(Comparator.Compare.IS_EQUAL_OR_LESSER); + Assert.assertTrue(t.shouldRun()); + t = new TriggerDelta().setUnits(Constants.MGDL).setValue(2d).comparator(Comparator.Compare.IS_EQUAL); + Assert.assertFalse(t.shouldRun()); + t = new TriggerDelta().setUnits(Constants.MMOL).setValue(0.3d).comparator(Comparator.Compare.IS_EQUAL_OR_LESSER); + Assert.assertTrue(t.shouldRun()); + t = new TriggerDelta().setUnits(Constants.MMOL).setValue(0.1d).comparator(Comparator.Compare.IS_EQUAL_OR_GREATER); + Assert.assertFalse(t.shouldRun()); + t = new TriggerDelta().setUnits(Constants.MMOL).setValue(-0.5d).comparator(Comparator.Compare.IS_EQUAL_OR_GREATER); + Assert.assertTrue(t.shouldRun()); + t = new TriggerDelta().setUnits(Constants.MMOL).setValue(-0.2d).comparator(Comparator.Compare.IS_EQUAL_OR_LESSER); + Assert.assertFalse(t.shouldRun()); + + when(IobCobCalculatorPlugin.getPlugin().getBgReadings()).thenReturn(new ArrayList<>()); + t = new TriggerDelta().setUnits(Constants.MGDL).setValue(213).comparator(Comparator.Compare.IS_EQUAL_OR_LESSER); + Assert.assertFalse(t.shouldRun()); + t = new TriggerDelta().comparator(Comparator.Compare.IS_NOT_AVAILABLE); + Assert.assertTrue(t.shouldRun()); + + t = new TriggerDelta().setUnits(Constants.MGDL).setValue(214).comparator(Comparator.Compare.IS_EQUAL).lastRun(now - 1); + Assert.assertFalse(t.shouldRun()); + + } + + @Test + public void copyConstructorTest() { + TriggerDelta t = new TriggerDelta().setUnits(Constants.MGDL).setValue(213).comparator(Comparator.Compare.IS_EQUAL_OR_LESSER); + TriggerDelta t1 = (TriggerDelta) t.duplicate(); + Assert.assertEquals(213d, t1.getValue(), 0.01d); + Assert.assertEquals(Constants.MGDL, t1.getUnits()); + Assert.assertEquals(Comparator.Compare.IS_EQUAL_OR_LESSER, t.getComparator().getValue()); + } + + @Test + public void executeTest() { + TriggerDelta t = new TriggerDelta().setUnits(Constants.MGDL).setValue(213).comparator(Comparator.Compare.IS_EQUAL_OR_LESSER); + t.executed(1); + Assert.assertEquals(1l, t.getLastRun()); + } + + String deltaJson = "{\"data\":{\"comparator\":\"IS_EQUAL\",\"lastRun\":0,\"units\":\"mmol\",\"type\":0,\"value\":4.1},\"type\":\"info.nightscout.androidaps.plugins.general.automation.triggers.TriggerDelta\"}"; + + @Test + public void toJSONTest() { + TriggerDelta t = new TriggerDelta().setUnits(Constants.MMOL).setValue(4.1d).comparator(Comparator.Compare.IS_EQUAL); + Assert.assertEquals(deltaJson, t.toJSON()); + } + + @Test + public void fromJSONTest() throws JSONException { + TriggerDelta t = new TriggerDelta().setUnits(Constants.MMOL).setValue(4.1d).comparator(Comparator.Compare.IS_EQUAL); + + TriggerDelta t2 = (TriggerDelta) Trigger.instantiate(new JSONObject(t.toJSON())); + Assert.assertEquals(Comparator.Compare.IS_EQUAL, t2.getComparator().getValue()); + Assert.assertEquals(4.1d, t2.getValue(), 0.01d); + Assert.assertEquals(Constants.MMOL, t2.getUnits()); + Assert.assertEquals(0d, t2.getType(), 0.01d); + } + + @Test + public void iconTest() { + Assert.assertEquals(Optional.of(R.drawable.as), new TriggerDelta().icon()); + } + + @Test + public void typeToStringTest() { + TriggerDelta t = new TriggerDelta(); + Assert.assertEquals(MainApp.gs(R.string.delta), t.typeToString(0)); + Assert.assertEquals(MainApp.gs(R.string.short_avgdelta), t.typeToString(1)); + Assert.assertEquals(MainApp.gs(R.string.long_avgdelta), t.typeToString(2)); + } + + + @Before + public void mock() { + AAPSMocker.mockMainApp(); + AAPSMocker.mockBus(); + AAPSMocker.mockIobCobCalculatorPlugin(); + AAPSMocker.mockProfileFunctions(); + AAPSMocker.mockApplicationContext(); + + PowerMockito.mockStatic(DateUtil.class); + when(DateUtil.now()).thenReturn(now); + + } + + List generateValidBgData() { + List list = new ArrayList<>(); + try { + list.add(new BgReading(new NSSgv(new JSONObject("{\"mgdl\":214,\"mills\":1514766900000,\"direction\":\"Flat\"}")))); + list.add(new BgReading(new NSSgv(new JSONObject("{\"mgdl\":216,\"mills\":1514766600000,\"direction\":\"Flat\"}")))); // +2 + list.add(new BgReading(new NSSgv(new JSONObject("{\"mgdl\":219,\"mills\":1514766300000,\"direction\":\"Flat\"}")))); // +3 + list.add(new BgReading(new NSSgv(new JSONObject("{\"mgdl\":223,\"mills\":1514766000000,\"direction\":\"Flat\"}")))); // +4 + list.add(new BgReading(new NSSgv(new JSONObject("{\"mgdl\":222,\"mills\":1514765700000,\"direction\":\"Flat\"}")))); + list.add(new BgReading(new NSSgv(new JSONObject("{\"mgdl\":224,\"mills\":1514765400000,\"direction\":\"Flat\"}")))); + list.add(new BgReading(new NSSgv(new JSONObject("{\"mgdl\":226,\"mills\":1514765100000,\"direction\":\"Flat\"}")))); + list.add(new BgReading(new NSSgv(new JSONObject("{\"mgdl\":228,\"mills\":1514764800000,\"direction\":\"Flat\"}")))); + } catch (JSONException e) { + e.printStackTrace(); + } + return list; + } +}