package org.nightscout.lasso; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.IBinder; import android.provider.Settings; import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import com.nightscout.core.dexcom.records.CalRecord; import com.nightscout.core.dexcom.records.EGVRecord; import com.nightscout.core.dexcom.records.InsertionRecord; import com.nightscout.core.dexcom.records.MeterRecord; import com.nightscout.core.dexcom.records.SensorRecord; import com.nightscout.core.events.EventSeverity; import com.nightscout.core.events.EventType; import com.nightscout.core.model.CalibrationEntry; import com.nightscout.core.model.Download; import com.nightscout.core.model.GlucoseUnit; import com.nightscout.core.model.InsertionEntry; import com.nightscout.core.model.MeterEntry; import com.nightscout.core.model.SensorEntry; import com.nightscout.core.model.SensorGlucoseValueEntry; import com.nightscout.core.mqtt.MqttEventMgr; import com.nightscout.core.mqtt.MqttMgrObserver; import com.nightscout.core.mqtt.MqttPinger; import com.nightscout.core.mqtt.MqttTimer; import com.nightscout.core.preferences.NightscoutPreferences; import com.nightscout.core.utils.GlucoseReading; import com.nightscout.core.utils.IsigReading; import com.nightscout.core.utils.RestUriUtils; import com.squareup.wire.Wire; import net.tribe7.common.base.Optional; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import org.eclipse.paho.client.mqttv3.MqttClient; import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; import org.joda.time.DateTime; import org.joda.time.Minutes; import org.joda.time.Seconds; import org.joda.time.format.ISODateTimeFormat; import org.json.JSONException; import org.json.JSONObject; import org.nightscout.lasso.alarm.Alarm; import org.nightscout.lasso.events.AndroidEventReporter; import org.nightscout.lasso.model.CalibrationDbEntry; import org.nightscout.lasso.model.InsertionDbEntry; import org.nightscout.lasso.model.MeterDbEntry; import org.nightscout.lasso.model.SensorDbEntry; import org.nightscout.lasso.model.SgvDbEntry; import org.nightscout.lasso.mqtt.AndroidMqttPinger; import org.nightscout.lasso.mqtt.AndroidMqttTimer; import org.nightscout.lasso.preferences.AndroidPreferences; import java.io.IOException; import java.net.URI; import java.text.DecimalFormat; import java.util.List; public class NightscoutMonitor extends Service implements MqttMgrObserver { public static final String RECEIVER_STATE_INTENT = "org.nightscout.scout.RECEIVER_STATE"; public static final String SNOOZE_INTENT = "org.nightscout.scout.SNOOZE"; public static final String MQTT_QUERY_STATUS_INTENT = "org.nightscout.scout.MQTT_QUERY_STATUS"; public static final String MQTT_RESPONSE_STATUS_INTENT = "org.nightscout.scout.MQTT_RESPONSE_STATUS"; public static final String NEW_READING_ACTION = "org.nightscout.NEW_READING"; public static final String MQTT_STATUS_EXTRA_FIELD = "MqttConnected"; private String TAG = this.getClass().getSimpleName(); private AndroidEventReporter reporter; private AndroidPreferences preferences; private MqttEventMgr mqttManager; private Alarm alarm; private Optional<Integer> uploaderBattery = Optional.absent(); private int messageCount = 0; private BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(SNOOZE_INTENT)) { if (preferences.getAlarmStrategy() == 0) { for (String url : preferences.getRestApiBaseUris()) { ackAlarm(url); } } else { Log.w(TAG, "Snoozing per request"); alarm.alarmSnooze(Minutes.minutes(30).toStandardDuration().getMillis()); } } else if (intent.getAction().equals(MQTT_QUERY_STATUS_INTENT)) { Intent responseIntent = new Intent(MQTT_RESPONSE_STATUS_INTENT); responseIntent.putExtra(MQTT_STATUS_EXTRA_FIELD, mqttManager.isConnected()); LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(responseIntent); } } }; public NightscoutMonitor() { } public void ackAlarm(final String uriStr) { new Thread(new Runnable() { @Override public void run() { Integer level = alarm.getAlarmResults().severity.ordinal() - 3; URI uri = URI.create(uriStr + "/notifications/ack?level=" + level); Log.w(TAG, "Sending snooze command to " + uri.toString() + "/notifications/ack?level=" + level + "&time=" + 15000); String url = RestUriUtils.removeToken(uri).toString(); Log.w(TAG, "URL: " + url); String secret = RestUriUtils.generateSecret(uri.getUserInfo()); HttpGet httpGet = new HttpGet(url); httpGet.addHeader("Content-Type", "application/json"); httpGet.addHeader("Accept", "application/json"); Log.d(TAG, "Secret: " + secret); httpGet.setHeader("api-secret", secret); HttpClient client = new DefaultHttpClient(); HttpResponse response = null; try { response = client.execute(httpGet); Log.d(TAG, "Response code: " + response.getStatusLine().getStatusCode()); Log.d(TAG, "Response: " + response.getEntity().getContent().read()); } catch (IOException e) { e.printStackTrace(); } } }).start(); } @Override public void onCreate() { super.onCreate(); Log.d(TAG, "service created"); preferences = new AndroidPreferences(getApplicationContext()); reporter = AndroidEventReporter.getReporter(getApplicationContext()); preferences.setMqttUploadEnabled(true); setupMqtt(); // FIXME - hack to get the app to start up if (mqttManager != null) { mqttManager.registerObserver(this); mqttManager.subscribe(2, "/downloads/protobuf"); if (preferences.getAlarmStrategy() == 0) { Log.d(TAG, "Subscribing to notifications"); mqttManager.subscribe(2, "/notifications/json"); } } getApplicationContext().registerReceiver(broadcastReceiver, new IntentFilter(SNOOZE_INTENT)); alarm = new Alarm(getApplicationContext()); } // TODO handle mqtt configuration changes @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.d(TAG, "service started"); return START_STICKY; } @Override public IBinder onBind(Intent intent) { // TODO: Return the communication channel to the service. throw new UnsupportedOperationException("Not yet implemented"); } public void setupMqtt() { if (preferences.isMqttEnabled()) { mqttManager = setupMqttConnection(preferences.getMqttUser(), preferences.getMqttPass(), preferences.getMqttEndpoint()); if (mqttManager != null) { Log.d(TAG, "Attempt to connect to MQTT"); mqttManager.setShouldReconnect(true); mqttManager.connect(); } else { Log.d(TAG, "MQTT is NULL"); } } } public MqttEventMgr setupMqttConnection(String user, String pass, String endpoint) { if (user.equals("") || pass.equals("") || endpoint.equals("")) { reporter.report(EventType.UPLOADER, EventSeverity.ERROR, "Unable to setup MQTT. Please check settings"); return null; } try { MqttConnectOptions mqttOptions = new MqttConnectOptions(); mqttOptions.setCleanSession(false); mqttOptions.setKeepAliveInterval(150000); mqttOptions.setUserName(user); mqttOptions.setPassword(pass.toCharArray()); String androidId = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID); MemoryPersistence dataStore = new MemoryPersistence(); MqttClient client = new MqttClient(endpoint, androidId, dataStore); MqttPinger pinger = new AndroidMqttPinger(getApplicationContext(), 0, client, 150000); MqttTimer timer = new AndroidMqttTimer(getApplicationContext(), 0); return new MqttEventMgr(client, mqttOptions, pinger, timer, reporter); } catch (MqttException e) { reporter.report(EventType.UPLOADER, EventSeverity.ERROR, "Unable to setup MQTT. Please check settings"); return null; } } @Override public void onDestroy() { super.onDestroy(); Log.d(TAG, "service destroyed"); } @Override public void onMessage(String topic, MqttMessage message) { if (topic.equals("/notifications/json")) { JSONObject reader = null; try { reader = new JSONObject(new String(message.getPayload())); if (reader.has("clear") && reader.getBoolean("clear")) { alarm.clear(); } if (reader.has("level") && reader.has("title") && reader.has("message")) { alarm.generateAlarm(reader); } } catch (JSONException e) { e.printStackTrace(); } } else if (topic.equals("/downloads/protobuf")) { messageCount += 1; Wire wire = new Wire(); try { Download download = wire.parseFrom(message.getPayload(), Download.class); if (download.receiver_state != null) { Intent intent = new Intent(RECEIVER_STATE_INTENT); intent.putExtra("state", download.receiver_state.event.name()); LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent); } Log.d(TAG, "Msg #" + messageCount); saveToDb(download); NightscoutPreferences preferences = new AndroidPreferences(getApplicationContext()); List<EGVRecord> egvRecords = SgvDbEntry.getLastEgvRecords(new DateTime().minus(Minutes.minutes(90))); Log.d(TAG, "Analyzing message #" + messageCount); DateTime dlTimestamp = ISODateTimeFormat.dateTime().parseDateTime(download.download_timestamp); alarm.analyze(egvRecords, preferences.getPreferredUnits(), Optional.fromNullable(download.uploader_battery), dlTimestamp); alarm.alarm(); Log.e(TAG, "New reading came in - EGV: " + egvRecords.get(egvRecords.size() - 1).getBgMgdl() + ". Reading taken at: " + egvRecords.get(egvRecords.size() - 1).getWallTime()); String delta = "None"; if (egvRecords.size() > 2) { GlucoseReading glucoseDelta = egvRecords.get(egvRecords.size() - 1).getReading().subtract(egvRecords.get(egvRecords.size() - 2).getReading()); DecimalFormat fmt; if (preferences.getPreferredUnits() == GlucoseUnit.MGDL) { fmt = new DecimalFormat("+#,##0;-#"); } else { fmt = new DecimalFormat("+#,##0.0;-#"); } if (egvRecords.get(egvRecords.size() - 2).getReading().asMgdl() > 38 && egvRecords.get(egvRecords.size() - 2).getReading().asMgdl() > 38) { delta = fmt.format(glucoseDelta.as(preferences.getPreferredUnits())); } } Optional<CalRecord> lastCal = CalibrationDbEntry.getLastCal(); Optional<SensorRecord> lastSensor = SensorDbEntry.getLastSensor(); EGVRecord lastEgv = egvRecords.get(egvRecords.size() - 1); IsigReading isigReading = new IsigReading(); if (lastCal.isPresent() && lastSensor.isPresent() && Math.abs(lastEgv.getSystemTime().getMillis() - lastSensor.get().getSystemTime().getMillis()) < Seconds.seconds(10).toStandardDuration().getMillis()) { isigReading = new IsigReading(lastSensor.get(), lastCal.get(), egvRecords.get(egvRecords.size() - 1)); Log.e("isig", "iSig reading: " + isigReading.asMgdlStr()); } else { Log.w("isig", "Problem matching sensor to egv for isig calculation"); Log.w("isig", "Last egv: " + lastEgv.getSystemTime().getMillis()); Log.w("isig", "Last sensor: " + lastSensor.get().getSystemTime().getMillis()); } WatchMaker.sendReadings(egvRecords.get(egvRecords.size() - 1), egvRecords.get(egvRecords.size() - 1).getNoiseMode(), isigReading, preferences.getPreferredUnits(), delta, uploaderBattery.or(-1), Optional.fromNullable(download.receiver_battery).or(-1), getApplicationContext()); Intent intent = new Intent(NEW_READING_ACTION); Log.d(TAG, "Msg #" + messageCount + " - Battery: " + download.uploader_battery); intent.putExtra("uploaderBattery", download.uploader_battery); Log.d(TAG, "Msg #" + messageCount + " - Receiver: " + download.receiver_battery); LocalBroadcastManager.getInstance(this).sendBroadcast(intent); } catch (IOException e) { e.printStackTrace(); } } } private void saveToDb(Download download) { long dlTimestampMs = ISODateTimeFormat.dateTime().parseDateTime(download.download_timestamp).getMillis(); EGVRecord egvRecord = null; SensorRecord sensorRecord = null; CalRecord calRecord = null; InsertionRecord insertionRecord = null; MeterRecord meterRecord = null; for (SensorGlucoseValueEntry sensorGlucoseValueEntry : download.sgv) { egvRecord = new EGVRecord(sensorGlucoseValueEntry, download.receiver_system_time_sec, dlTimestampMs); SgvDbEntry sgvEntry = new SgvDbEntry(egvRecord, download.receiver_id, download.transmitter_id); Log.d(TAG, "SGV: " + egvRecord.getBgMgdl() + " Trend: " + egvRecord.getTrend().friendlyTrendName()); sgvEntry.save(); } for (CalibrationEntry calibrationEntry : download.cal) { calRecord = new CalRecord(calibrationEntry, download.receiver_system_time_sec, dlTimestampMs); CalibrationDbEntry calEntry = new CalibrationDbEntry(calRecord, download.receiver_id, download.transmitter_id); calEntry.save(); } for (SensorEntry sensorEntry : download.sensor) { sensorRecord = new SensorRecord(sensorEntry, download.receiver_system_time_sec, dlTimestampMs); SensorDbEntry sensorDbEntry = new SensorDbEntry(sensorRecord, download.receiver_id, download.transmitter_id); sensorDbEntry.save(); } for (InsertionEntry insertionEntry : download.insert) { insertionRecord = new InsertionRecord(insertionEntry, download.receiver_system_time_sec, dlTimestampMs); InsertionDbEntry insertionDbEntry = new InsertionDbEntry(insertionRecord, download.receiver_id, download.transmitter_id); insertionDbEntry.save(); } for (MeterEntry meterEntry : download.meter) { meterRecord = new MeterRecord(meterEntry, download.receiver_system_time_sec, dlTimestampMs); MeterDbEntry meterDbEntry = new MeterDbEntry(meterRecord, download.receiver_id, download.transmitter_id); meterDbEntry.save(); } } @Override public void onDisconnect() { Intent intent = new Intent(MQTT_RESPONSE_STATUS_INTENT); intent.putExtra(MQTT_STATUS_EXTRA_FIELD, false); LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent); } @Override public void onConnect() { Intent intent = new Intent(MQTT_RESPONSE_STATUS_INTENT); intent.putExtra(MQTT_STATUS_EXTRA_FIELD, true); LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent); } }