package info.nightscout.androidaps.plugins.Wear.wearintegration;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.BatteryManager;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.wearable.DataMap;
import com.google.android.gms.wearable.MessageEvent;
import com.google.android.gms.wearable.PutDataMapRequest;
import com.google.android.gms.wearable.PutDataRequest;
import com.google.android.gms.wearable.Wearable;
import com.google.android.gms.wearable.WearableListenerService;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import info.nightscout.androidaps.Constants;
import info.nightscout.androidaps.MainApp;
import info.nightscout.androidaps.R;
import info.nightscout.androidaps.data.GlucoseStatus;
import info.nightscout.androidaps.db.BgReading;
import info.nightscout.androidaps.db.TempBasal;
import info.nightscout.androidaps.interfaces.PluginBase;
import info.nightscout.androidaps.interfaces.PumpInterface;
import info.nightscout.androidaps.data.IobTotal;
import info.nightscout.androidaps.plugins.Loop.LoopPlugin;
import info.nightscout.androidaps.plugins.Overview.OverviewPlugin;
import info.nightscout.androidaps.plugins.Wear.ActionStringHandler;
import info.nightscout.androidaps.plugins.Wear.WearPlugin;
import info.nightscout.androidaps.plugins.NSClientInternal.data.NSProfile;
import info.nightscout.utils.DecimalFormatter;
import info.nightscout.utils.SafeParse;
import info.nightscout.utils.ToastUtils;
public class WatchUpdaterService extends WearableListenerService implements
GoogleApiClient.ConnectionCallbacks,
GoogleApiClient.OnConnectionFailedListener {
public static final String ACTION_RESEND = WatchUpdaterService.class.getName().concat(".Resend");
public static final String ACTION_OPEN_SETTINGS = WatchUpdaterService.class.getName().concat(".OpenSettings");
public static final String ACTION_SEND_STATUS = WatchUpdaterService.class.getName().concat(".SendStatus");
public static final String ACTION_SEND_BASALS = WatchUpdaterService.class.getName().concat(".SendBasals");
public static final String ACTION_SEND_BOLUSPROGRESS = WatchUpdaterService.class.getName().concat(".BolusProgress");
public static final String ACTION_SEND_ACTIONCONFIRMATIONREQUEST = WatchUpdaterService.class.getName().concat(".ActionConfirmationRequest");
private GoogleApiClient googleApiClient;
public static final String WEARABLE_DATA_PATH = "/nightscout_watch_data";
public static final String WEARABLE_RESEND_PATH = "/nightscout_watch_data_resend";
private static final String WEARABLE_CANCELBOLUS_PATH = "/nightscout_watch_cancel_bolus";
public static final String WEARABLE_CONFIRM_ACTIONSTRING_PATH = "/nightscout_watch_confirmactionstring";
public static final String WEARABLE_INITIATE_ACTIONSTRING_PATH = "/nightscout_watch_initiateactionstring";
private static final String OPEN_SETTINGS_PATH = "/openwearsettings";
private static final String NEW_STATUS_PATH = "/sendstatustowear";
public static final String BASAL_DATA_PATH = "/nightscout_watch_basal";
public static final String BOLUS_PROGRESS_PATH = "/nightscout_watch_bolusprogress";
public static final String ACTION_CONFIRMATION_REQUEST_PATH = "/nightscout_watch_actionconfirmationrequest";
boolean wear_integration = false;
SharedPreferences mPrefs;
private static boolean lastLoopStatus;
@Override
public void onCreate() {
mPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
listenForChangeInSettings();
setSettings();
if (wear_integration) {
googleApiConnect();
}
}
public void listenForChangeInSettings() {
WearPlugin.registerWatchUpdaterService(this);
}
public void setSettings() {
wear_integration = WearPlugin.isEnabled();
if (wear_integration) {
googleApiConnect();
}
}
public void googleApiConnect() {
if(googleApiClient != null && (googleApiClient.isConnected() || googleApiClient.isConnecting())) { googleApiClient.disconnect(); }
googleApiClient = new GoogleApiClient.Builder(this)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.addApi(Wearable.API)
.build();
Wearable.MessageApi.addListener(googleApiClient, this);
if (googleApiClient.isConnected()) {
Log.d("WatchUpdater", "API client is connected");
} else {
googleApiClient.connect();
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
double timestamp = 0;
if (intent != null) {
timestamp = intent.getDoubleExtra("timestamp", 0);
}
String action = null;
if (intent != null) {
action = intent.getAction();
}
if (wear_integration) {
if (googleApiClient.isConnected()) {
if (ACTION_RESEND.equals(action)) {
resendData();
} else if (ACTION_OPEN_SETTINGS.equals(action)) {
sendNotification();
} else if (ACTION_SEND_STATUS.equals(action)) {
sendStatus();
} else if (ACTION_SEND_BASALS.equals(action)) {
sendBasals();
} else if (ACTION_SEND_BOLUSPROGRESS.equals(action)){
sendBolusProgress(intent.getIntExtra("progresspercent", 0), intent.hasExtra("progressstatus")?intent.getStringExtra("progressstatus"):"");
} else if (ACTION_SEND_ACTIONCONFIRMATIONREQUEST.equals(action)){
String title = intent.getStringExtra("title");
String message = intent.getStringExtra("message");
String actionstring = intent.getStringExtra("actionstring");
sendActionConfirmationRequest(title, message, actionstring);
}
else {
sendData();
}
} else {
googleApiClient.connect();
}
}
return START_STICKY;
}
@Override
public void onConnected(Bundle connectionHint) {
sendData();
}
@Override
public void onMessageReceived(MessageEvent event) {
if (wear_integration) {
if (event != null && event.getPath().equals(WEARABLE_RESEND_PATH)) {
resendData();
}
if (event != null && event.getPath().equals(WEARABLE_CANCELBOLUS_PATH)) {
cancelBolus();
}
if (event != null && event.getPath().equals(WEARABLE_INITIATE_ACTIONSTRING_PATH)) {
String actionstring = new String(event.getData());
ToastUtils.showToastInUiThread(this, "Wear: " + actionstring);
ActionStringHandler.handleInitiate(actionstring);
}
if (event != null && event.getPath().equals(WEARABLE_CONFIRM_ACTIONSTRING_PATH)) {
String actionstring = new String(event.getData());
ToastUtils.showToastInUiThread(this, "Wear Confirm: " + actionstring);
ActionStringHandler.handleConfirmation(actionstring);
}
}
}
private void cancelBolus() {
PumpInterface pump = MainApp.getConfigBuilder();
pump.stopBolusDelivering();
}
private void sendData() {
BgReading lastBG = GlucoseStatus.lastBg();
if (lastBG != null) {
GlucoseStatus glucoseStatus = GlucoseStatus.getGlucoseStatusData();
if(googleApiClient != null && !googleApiClient.isConnected() && !googleApiClient.isConnecting()) { googleApiConnect(); }
if (wear_integration) {
final DataMap dataMap = dataMapSingleBG(lastBG, glucoseStatus);
if(dataMap==null) {
ToastUtils.showToastInUiThread(this, getString(R.string.noprofile));
return;
}
new SendToDataLayerThread(WEARABLE_DATA_PATH, googleApiClient).execute(dataMap);
}
}
}
private DataMap dataMapSingleBG(BgReading lastBG, GlucoseStatus glucoseStatus) {
NSProfile profile = MainApp.getConfigBuilder().getActiveProfile().getProfile();
if(profile == null) return null;
Double lowLine = SafeParse.stringToDouble(mPrefs.getString("low_mark", "0"));
Double highLine = SafeParse.stringToDouble(mPrefs.getString("high_mark", "0"));
//convert to mg/dl
if (! profile.getUnits().equals(Constants.MGDL)){
lowLine *= Constants.MMOLL_TO_MGDL;
highLine *= Constants.MMOLL_TO_MGDL;
}
if (lowLine < 1){
lowLine = OverviewPlugin.bgTargetLow;
}
if(highLine < 1){
highLine = OverviewPlugin.bgTargetHigh;
}
long sgvLevel = 0l;
if (lastBG.value > highLine) {
sgvLevel = 1;
} else if (lastBG.value < lowLine) {
sgvLevel = -1;
}
DataMap dataMap = new DataMap();
int battery = getBatteryLevel(getApplicationContext());
dataMap.putString("sgvString", lastBG.valueToUnitsToString(profile.getUnits()));
dataMap.putDouble("timestamp", lastBG.getTimeIndex());
if(glucoseStatus == null) {
dataMap.putString("slopeArrow", "" );
dataMap.putString("delta", "");
dataMap.putString("avgDelta", "");
} else {
dataMap.putString("slopeArrow", slopeArrow(glucoseStatus.delta));
dataMap.putString("delta", deltastring(glucoseStatus.delta, glucoseStatus.delta * Constants.MGDL_TO_MMOLL, profile.getUnits()));
dataMap.putString("avgDelta", deltastring(glucoseStatus.avgdelta, glucoseStatus.avgdelta * Constants.MGDL_TO_MMOLL, profile.getUnits()));
}
dataMap.putString("battery", "" + battery);
dataMap.putLong("sgvLevel", sgvLevel);
dataMap.putInt("batteryLevel", (battery>=30)?1:0);
dataMap.putDouble("sgvDouble", lastBG.value);
dataMap.putDouble("high", highLine);
dataMap.putDouble("low", lowLine);
return dataMap;
}
private String deltastring(double deltaMGDL, double deltaMMOL, String units) {
String deltastring = "";
if (deltaMGDL >=0){
deltastring += "+";
} else{
deltastring += "-";
}
if (units.equals(Constants.MGDL)){
deltastring += DecimalFormatter.to1Decimal(Math.abs(deltaMGDL));
}
else {
deltastring += DecimalFormatter.to1Decimal(Math.abs(deltaMMOL));
}
return deltastring;
}
private String slopeArrow(double delta) {
if (delta <= (-3.5*5)) {
return "\u21ca";
} else if (delta <= (-2*5)) {
return "\u2193";
} else if (delta <= (-1*5)) {
return "\u2198";
} else if (delta <= (1*5)) {
return "\u2192";
} else if (delta <= (2*5)) {
return "\u2197";
} else if (delta <= (3.5*5)) {
return "\u2191";
} else {
return "\u21c8";
}
}
private void resendData() {
if(googleApiClient != null && !googleApiClient.isConnected() && !googleApiClient.isConnecting()) { googleApiConnect(); }
long startTime = System.currentTimeMillis() - (long)(60000 * 60 * 5.5);
BgReading last_bg = GlucoseStatus.lastBg();
if (last_bg == null) return;
List<BgReading> graph_bgs = MainApp.getDbHelper().getBgreadingsDataFromTime(startTime, true);
GlucoseStatus glucoseStatus = GlucoseStatus.getGlucoseStatusData();
if (!graph_bgs.isEmpty()) {
DataMap entries = dataMapSingleBG(last_bg, glucoseStatus);
if(entries==null) {
ToastUtils.showToastInUiThread(this, getString(R.string.noprofile));
return;
}
final ArrayList<DataMap> dataMaps = new ArrayList<>(graph_bgs.size());
for (BgReading bg : graph_bgs) {
DataMap dataMap = dataMapSingleBG(bg, glucoseStatus);
if(dataMap != null) {
dataMaps.add(dataMap);
}
}
entries.putDataMapArrayList("entries", dataMaps);
new SendToDataLayerThread(WEARABLE_DATA_PATH, googleApiClient).execute(entries);
}
sendBasals();
sendStatus();
}
private void sendBasals() {
if(googleApiClient != null && !googleApiClient.isConnected() && !googleApiClient.isConnecting()) { googleApiConnect(); }
long now = System.currentTimeMillis();
long startTimeWindow = now - (long)(60000 * 60 * 5.5);
ArrayList<DataMap> basals = new ArrayList<>();
ArrayList<DataMap> temps = new ArrayList<>();
NSProfile profile = MainApp.getConfigBuilder().getActiveProfile().getProfile();
if(profile==null) {
return;
}
long beginBasalSegmentTime = startTimeWindow;
long runningTime = startTimeWindow;
double beginBasalValue = profile.getBasal(NSProfile.secondsFromMidnight(new Date(beginBasalSegmentTime)));
double endBasalValue = beginBasalValue;
TempBasal tb1 = MainApp.getConfigBuilder().getTempBasal(new Date(runningTime));
TempBasal tb2 = MainApp.getConfigBuilder().getTempBasal(new Date(runningTime));
double tb_before = beginBasalValue;
double tb_amount = beginBasalValue;
long tb_start = runningTime;
if(tb1 != null){
tb_before = beginBasalValue;
tb_amount = tb1.tempBasalConvertedToAbsolute(new Date(runningTime));
tb_start = runningTime;
}
for(;runningTime<now;runningTime+= 5*60*1000){
//basal rate
endBasalValue = profile.getBasal(NSProfile.secondsFromMidnight(new Date(runningTime)));
if(endBasalValue != beginBasalValue){
//push the segment we recently left
basals.add(basalMap(beginBasalSegmentTime, runningTime, beginBasalValue));
//begin new Basal segment
beginBasalSegmentTime = runningTime;
beginBasalValue = endBasalValue;
}
//temps
tb2 = MainApp.getConfigBuilder().getTempBasal(new Date(runningTime));
if (tb1 == null && tb2 == null) {
//no temp stays no temp
} else if (tb1 != null && tb2 == null) {
//temp is over -> push it
temps.add(tempDatamap(tb_start, tb_before, runningTime, endBasalValue, tb_amount));
tb1 = null;
} else if (tb1 == null && tb2 != null) {
//temp begins
tb1 = tb2;
tb_start = runningTime;
tb_before = endBasalValue;
tb_amount = tb1.tempBasalConvertedToAbsolute(new Date(runningTime));
} else if (tb1 != null && tb2 != null) {
double currentAmount = tb2.tempBasalConvertedToAbsolute(new Date(runningTime));
if(currentAmount != tb_amount){
temps.add(tempDatamap(tb_start, tb_before, runningTime, currentAmount, tb_amount));
tb_start = runningTime;
tb_before = tb_amount;
tb_amount = currentAmount;
tb1 = tb2;
}
}
}
if(beginBasalSegmentTime != runningTime){
//push the remaining segment
basals.add(basalMap(beginBasalSegmentTime, runningTime, beginBasalValue));
}
if(tb1 != null){
tb2 = MainApp.getConfigBuilder().getTempBasal(new Date(now)); //use "now" to express current situation
if(tb2 == null) {
//express the cancelled temp by painting it down one minute early
temps.add(tempDatamap(tb_start, tb_before, now - 1 * 60 * 1000, endBasalValue, tb_amount));
} else {
//express currently running temp by painting it a bit into the future
double currentAmount = tb2.tempBasalConvertedToAbsolute(new Date(now));
if(currentAmount != tb_amount){
temps.add(tempDatamap(tb_start, tb_before, now, tb_amount, tb_amount));
temps.add(tempDatamap(now, tb_amount, runningTime + 5 * 60 * 1000, currentAmount, currentAmount));
} else {
temps.add(tempDatamap(tb_start, tb_before, runningTime + 5 * 60 * 1000, tb_amount, tb_amount));
}
}
} else {
tb2 = MainApp.getConfigBuilder().getTempBasal(new Date(now)); //use "now" to express current situation
if(tb2 != null) {
//onset at the end
double currentAmount = tb2.tempBasalConvertedToAbsolute(new Date(runningTime));
temps.add(tempDatamap(now - 1 * 60 * 1000, endBasalValue, runningTime + 5 * 60 * 1000, currentAmount, currentAmount));
}
}
DataMap dm = new DataMap();
dm.putDataMapArrayList("basals", basals);
dm.putDataMapArrayList("temps", temps);
new SendToDataLayerThread(BASAL_DATA_PATH, googleApiClient).execute(dm);
}
private DataMap tempDatamap(long startTime, double startBasal, long to, double toBasal, double amount) {
DataMap dm = new DataMap();
dm.putLong("starttime", startTime);
dm.putDouble("startBasal", startBasal);
dm.putLong("endtime", to);
dm.putDouble("endbasal", toBasal);
dm.putDouble("amount", amount);
return dm;
}
private DataMap basalMap(long startTime, long endTime, double amount) {
DataMap dm = new DataMap();
dm.putLong("starttime", startTime);
dm.putLong("endtime", endTime);
dm.putDouble("amount", amount);
return dm;
}
private void sendNotification() {
if (googleApiClient.isConnected()) {
PutDataMapRequest dataMapRequest = PutDataMapRequest.create(OPEN_SETTINGS_PATH);
//unique content
dataMapRequest.getDataMap().putDouble("timestamp", System.currentTimeMillis());
dataMapRequest.getDataMap().putString("openSettings", "openSettings");
PutDataRequest putDataRequest = dataMapRequest.asPutDataRequest();
Wearable.DataApi.putDataItem(googleApiClient, putDataRequest);
} else {
Log.e("OpenSettings", "No connection to wearable available!");
}
}
private void sendBolusProgress(int progresspercent, String status) {
if (googleApiClient.isConnected()) {
PutDataMapRequest dataMapRequest = PutDataMapRequest.create(BOLUS_PROGRESS_PATH);
//unique content
dataMapRequest.getDataMap().putDouble("timestamp", System.currentTimeMillis());
dataMapRequest.getDataMap().putString("bolusProgress", "bolusProgress");
dataMapRequest.getDataMap().putString("progressstatus", status);
dataMapRequest.getDataMap().putInt("progresspercent", progresspercent);
PutDataRequest putDataRequest = dataMapRequest.asPutDataRequest();
Wearable.DataApi.putDataItem(googleApiClient, putDataRequest);
} else {
Log.e("BolusProgress", "No connection to wearable available!");
}
}
private void sendActionConfirmationRequest(String title, String message, String actionstring) {
if (googleApiClient.isConnected()) {
PutDataMapRequest dataMapRequest = PutDataMapRequest.create(ACTION_CONFIRMATION_REQUEST_PATH);
//unique content
dataMapRequest.getDataMap().putDouble("timestamp", System.currentTimeMillis());
dataMapRequest.getDataMap().putString("actionConfirmationRequest", "actionConfirmationRequest");
dataMapRequest.getDataMap().putString("title", title);
dataMapRequest.getDataMap().putString("message", message);
dataMapRequest.getDataMap().putString("actionstring", actionstring);
ToastUtils.showToastInUiThread(this, "Requesting confirmation from wear: " + actionstring);
PutDataRequest putDataRequest = dataMapRequest.asPutDataRequest();
Wearable.DataApi.putDataItem(googleApiClient, putDataRequest);
} else {
Log.e("confirmationRequest", "No connection to wearable available!");
}
}
private void sendStatus() {
if (googleApiClient.isConnected()) {
String status = "";
boolean shortString = true;
LoopPlugin activeloop = MainApp.getConfigBuilder().getActiveLoop();
if (activeloop != null && !activeloop.isEnabled(PluginBase.LOOP)) {
status += getString(R.string.disabledloop) + "\n";
lastLoopStatus = false;
} else if (activeloop != null && activeloop.isEnabled(PluginBase.LOOP)) {
lastLoopStatus = true;
}
//Temp basal
PumpInterface pump = MainApp.getConfigBuilder();
if (pump.isTempBasalInProgress()) {
TempBasal activeTemp = pump.getTempBasal();
if (shortString) {
status += activeTemp.toStringShort();
} else {
status += activeTemp.toStringMedium();
}
}
//IOB
MainApp.getConfigBuilder().getActiveTreatments().updateTotalIOB();
IobTotal bolusIob = MainApp.getConfigBuilder().getActiveTreatments().getLastCalculation().round();
MainApp.getConfigBuilder().getActiveTempBasals().updateTotalIOB();
IobTotal basalIob = MainApp.getConfigBuilder().getActiveTempBasals().getLastCalculation().round();
status += (shortString?"":(getString(R.string.treatments_iob_label_string) + " ")) + DecimalFormatter.to2Decimal(bolusIob.iob + basalIob.basaliob);
if (mPrefs.getBoolean("wear_detailediob", true)) {
status += "("
+ DecimalFormatter.to2Decimal(bolusIob.iob) + "|"
+ DecimalFormatter.to2Decimal(basalIob.basaliob) + ")";
}
PutDataMapRequest dataMapRequest = PutDataMapRequest.create(NEW_STATUS_PATH);
//unique content
dataMapRequest.getDataMap().putDouble("timestamp", System.currentTimeMillis());
dataMapRequest.getDataMap().putString("externalStatusString", status);
PutDataRequest putDataRequest = dataMapRequest.asPutDataRequest();
Wearable.DataApi.putDataItem(googleApiClient, putDataRequest);
} else {
Log.e("SendStatus", "No connection to wearable available!");
}
}
@Override
public void onDestroy() {
if (googleApiClient != null && googleApiClient.isConnected()) {
googleApiClient.disconnect();
}
WearPlugin.unRegisterWatchUpdaterService();
}
@Override
public void onConnectionSuspended(int cause) {
}
@Override
public void onConnectionFailed(ConnectionResult connectionResult) {
}
public static boolean shouldReportLoopStatus(boolean enabled){
return (lastLoopStatus != enabled);
}
public static int getBatteryLevel(Context context) {
Intent batteryIntent = context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
int level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
if(level == -1 || scale == -1) {
return 50;
}
return (int)(((float)level / (float)scale) * 100.0f);
}
}