From 3386e86158a50effc9793c16e4bb201f8058e442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Mon, 22 Apr 2024 21:02:06 +0100 Subject: [PATCH] Garmin: Add support for http weather requests --- .../devices/garmin/GarminPreferences.java | 5 + .../garmin/venu3/GarminVenu3Coordinator.java | 6 - .../service/devices/garmin/GarminSupport.java | 18 +- .../devices/garmin/ProtocolBufferHandler.java | 9 + .../garmin/communicator/CobsCoDec.java | 2 +- .../devices/garmin/http/HttpHandler.java | 124 ++++++ .../devices/garmin/http/WeatherHandler.java | 375 ++++++++++++++++++ .../garmin/messages/ConfigurationMessage.java | 21 +- .../messages/DeviceInformationMessage.java | 8 +- .../messages/FindMyPhoneCancelMessage.java | 7 +- .../messages/FindMyPhoneRequestMessage.java | 7 +- .../devices/garmin/messages/GFDIMessage.java | 6 +- .../garmin/messages/MusicControlMessage.java | 8 +- .../messages/NotificationControlMessage.java | 6 +- .../NotificationSubscriptionMessage.java | 7 +- .../garmin/messages/WeatherMessage.java | 8 +- .../SupportedFileTypesStatusMessage.java | 6 +- .../zeppos/services/ZeppOsHttpService.java | 22 +- .../gadgetbridge/util/HttpUtils.java | 42 ++ .../garmin_vivomovehr/gdi_http_service.proto | 46 +++ .../garmin_vivomovehr/gdi_smart_proto.proto | 2 + 21 files changed, 687 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminPreferences.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/HttpHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/WeatherHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/HttpUtils.java create mode 100644 app/src/main/proto/garmin_vivomovehr/gdi_http_service.proto diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminPreferences.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminPreferences.java new file mode 100644 index 000000000..f1d5c3159 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/GarminPreferences.java @@ -0,0 +1,5 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.garmin; + +public class GarminPreferences { + public static final String PREF_GARMIN_CAPABILITIES = "garmin_capabilities"; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/venu3/GarminVenu3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/venu3/GarminVenu3Coordinator.java index fb0210daf..6c378a03d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/venu3/GarminVenu3Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/garmin/venu3/GarminVenu3Coordinator.java @@ -15,10 +15,4 @@ public class GarminVenu3Coordinator extends GarminCoordinator { public int getDeviceNameResource() { return R.string.devicetype_garmin_vivomove_style; } - - @Override - public boolean supportsWeather() { - // FIXME: It's not working - return false; - } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java index 5856ff325..62460a40c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/GarminSupport.java @@ -10,6 +10,7 @@ import java.io.File; import java.io.IOException; import java.text.DecimalFormat; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; @@ -23,6 +24,8 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences; +import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; @@ -191,7 +194,10 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni } } - evaluateGBDeviceEvent(parsedMessage.getGBDeviceEvent()); + final List events = parsedMessage.getGBDeviceEvent(); + for (final GBDeviceEvent event : events) { + evaluateGBDeviceEvent(event); + } communicator.sendMessage(parsedMessage.getAckBytestream()); //send status message @@ -272,7 +278,17 @@ public class GarminSupport extends AbstractBTLEDeviceSupport implements ICommuni communicator.sendMessage(message.getOutgoingMessage()); } + private boolean supports(final GarminCapability capability) { + return getDevicePrefs().getStringSet(GarminPreferences.PREF_GARMIN_CAPABILITIES, Collections.emptySet()) + .contains(capability.name()); + } + private void sendWeatherConditions(WeatherSpec weather) { + if (!supports(GarminCapability.WEATHER_CONDITIONS)) { + // Device does not support sending weather as fit + return; + } + List weatherData = new ArrayList<>(); List weatherDefinitions = new ArrayList<>(3); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java index e58da9935..46b96faac 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/ProtocolBufferHandler.java @@ -19,8 +19,10 @@ import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiCalendarService; import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiCore; import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiDeviceStatus; import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiFindMyWatch; +import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiHttpService; import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiSmartProto; import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiSmsNotification; +import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http.HttpHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.GFDIMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.ProtobufMessage; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.ProtobufStatusMessage; @@ -82,6 +84,13 @@ public class ProtocolBufferHandler implements MessageHandler { if (smart.hasSmsNotificationService()) { return prepareProtobufResponse(processProtobufSmsNotificationMessage(smart.getSmsNotificationService()), message.getRequestId()); } + if (smart.hasHttpService()) { + final GdiHttpService.HttpService response = HttpHandler.handle(smart.getHttpService()); + if (response == null) { + return null; + } + return prepareProtobufResponse(GdiSmartProto.Smart.newBuilder().setHttpService(response).build(), message.getRequestId()); + } if (smart.hasDeviceStatusService()) { processed = true; processProtobufDeviceStatusResponse(smart.getDeviceStatusService()); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/communicator/CobsCoDec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/communicator/CobsCoDec.java index 96da30cad..6633907d1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/communicator/CobsCoDec.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/communicator/CobsCoDec.java @@ -4,7 +4,7 @@ import java.nio.ByteBuffer; public class CobsCoDec { private static final long BUFFER_TIMEOUT = 1500L; // turn this value up while debugging - private final ByteBuffer byteBuffer = ByteBuffer.allocate(1000); + private final ByteBuffer byteBuffer = ByteBuffer.allocate(10_000); private long lastUpdate; private byte[] cobsDecodedMessage; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/HttpHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/HttpHandler.java new file mode 100644 index 000000000..bc88cf929 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/HttpHandler.java @@ -0,0 +1,124 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.protobuf.ByteString; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.zip.GZIPOutputStream; + +import nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr.GdiHttpService; +import nodomain.freeyourgadget.gadgetbridge.util.HttpUtils; + +public class HttpHandler { + private static final Logger LOG = LoggerFactory.getLogger(HttpHandler.class); + + private static final Gson GSON = new GsonBuilder() + //.serializeNulls() + .create(); + + public static GdiHttpService.HttpService handle(final GdiHttpService.HttpService httpService) { + if (httpService.hasRawRequest()) { + final GdiHttpService.HttpService.RawResponse rawResponse = handleRawRequest(httpService.getRawRequest()); + if (rawResponse != null) { + return GdiHttpService.HttpService.newBuilder() + .setRawResponse(rawResponse) + .build(); + } + return null; + } + + LOG.warn("Unsupported http service request {}", httpService); + + return null; + } + + public static GdiHttpService.HttpService.RawResponse handleRawRequest(final GdiHttpService.HttpService.RawRequest rawRequest) { + final String urlString = rawRequest.getUrl(); + LOG.debug("Got rawRequest: {} - {}", rawRequest.getMethod(), urlString); + + final URL url; + try { + url = new URL(urlString); + } catch (final MalformedURLException e) { + LOG.error("Failed to parse url", e); + return null; + } + + final String path = url.getPath(); + final Map query = HttpUtils.urlQueryParameters(url); + final Map requestHeaders = headersToMap(rawRequest.getHeaderList()); + + final byte[] responseBody; + final List responseHeaders = new ArrayList<>(); + if (path.startsWith("/weather/")) { + LOG.debug("Got weather request for {}", path); + final Object obj = WeatherHandler.handleWeatherRequest(path, query); + if (obj == null) { + return null; + } + final String json = GSON.toJson(obj); + LOG.debug("Weather response: {}", json); + + final byte[] stringBytes = json.getBytes(StandardCharsets.UTF_8); + + if ("gzip".equals(requestHeaders.get("accept-encoding"))) { + responseHeaders.add( + GdiHttpService.HttpService.Header.newBuilder() + .setKey("Content-Encoding") + .setValue("gzip") + .build() + ); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (GZIPOutputStream gzos = new GZIPOutputStream(baos)) { + gzos.write(stringBytes); + gzos.finish(); + gzos.flush(); + responseBody = baos.toByteArray(); + } catch (final Exception e) { + LOG.error("Failed to compress response", e); + return null; + } + } else { + responseBody = stringBytes; + } + + responseHeaders.add( + GdiHttpService.HttpService.Header.newBuilder() + .setKey("Content-Type") + .setValue("application/json") + .build() + ); + } else { + LOG.warn("Unhandled path {}", urlString); + return null; + } + + return GdiHttpService.HttpService.RawResponse.newBuilder() + .setStatus(GdiHttpService.HttpService.Status.OK) + .setHttpStatus(200) + .setBody(ByteString.copyFrom(responseBody)) + .addAllHeader(responseHeaders) + .build(); + } + + private static Map headersToMap(final List headers) { + final Map ret = new HashMap<>(); + for (final GdiHttpService.HttpService.Header header : headers) { + ret.put(header.getKey().toLowerCase(Locale.ROOT), header.getValue()); + } + return ret; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/WeatherHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/WeatherHandler.java new file mode 100644 index 000000000..d5909be00 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/http/WeatherHandler.java @@ -0,0 +1,375 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.http; + +import android.location.Location; + +import net.e175.klaus.solarpositioning.DeltaT; +import net.e175.klaus.solarpositioning.SPA; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Map; + +import nodomain.freeyourgadget.gadgetbridge.model.Weather; +import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.CurrentPosition; + +public class WeatherHandler { + private static final Logger LOG = LoggerFactory.getLogger(WeatherHandler.class); + + // These get requested on connection at most every 5 minutes + public static Object handleWeatherRequest(final String path, final Map query) { + final WeatherSpec weatherSpec = Weather.getInstance().getWeatherSpec(); + + if (weatherSpec == null) { + LOG.warn("No weather in weather instance"); + return null; + } + + switch (path) { + case "/weather/v2/forecast/day": { + final int lat = getQueryNum(query, "lat", 0); + final int lon = getQueryNum(query, "lon", 0); + final int duration = getQueryNum(query, "duration", 5); + final String tempUnit = getQueryString(query, "tempUnit", "CELSIUS"); + final String provider = getQueryString(query, "provider", "dci"); + final List ret = new ArrayList<>(duration); + final GregorianCalendar date = new GregorianCalendar(); + date.setTime(new Date(weatherSpec.timestamp * 1000L)); + for (int i = 0; i < Math.min(duration, weatherSpec.forecasts.size()); i++) { + date.add(Calendar.DAY_OF_MONTH, 1); + ret.add(new WeatherForecastDay(date, weatherSpec.forecasts.get(i))); + } + return ret; + } + case "/weather/v2/forecast/hour": { + final int lat = getQueryNum(query, "lat", 0); + final int lon = getQueryNum(query, "lon", 0); + final int duration = getQueryNum(query, "duration", 13); + final String speedUnit = getQueryString(query, "speedUnit", "METERS_PER_SECOND"); + final String tempUnit = getQueryString(query, "tempUnit", "CELSIUS"); + final String provider = getQueryString(query, "provider", "dci"); + final String timesOfInterest = getQueryString(query, "timesOfInterest", ""); + final List ret = new ArrayList<>(duration); + for (int i = 0; i < Math.min(duration, weatherSpec.hourly.size()); i++) { + ret.add(new WeatherForecastHour(weatherSpec.hourly.get(i))); + } + return ret; + } + case "/weather/v2/current": { + final int lat = getQueryNum(query, "lat", 0); + final int lon = getQueryNum(query, "lon", 0); + final String tempUnit = getQueryString(query, "tempUnit", "CELSIUS"); + final String speedUnit = getQueryString(query, "speedUnit", "METERS_PER_SECOND"); + final String provider = getQueryString(query, "provider", "dci"); + return new WeatherForecastCurrent(weatherSpec); + } + } + + LOG.warn("Unknown weather path {}", path); + + return null; + } + + private static int getQueryNum(final Map query, final String key, final int defaultValue) { + final String str = query.get(key); + if (str != null) { + return Integer.parseInt(str); + } else { + return defaultValue; + } + } + + private static String getQueryString(final Map query, final String key, final String defaultValue) { + final String str = query.get(key); + if (str != null) { + return str; + } else { + return defaultValue; + } + } + + public static class WeatherForecastDay { + public int dayOfWeek; // 1 monday .. 7 sunday + public String description; + public String summary; + public WeatherValue high; + public WeatherValue low; + public Integer precipProb; + public Integer icon; + public Integer epochSunrise; + public Integer epochSunset; + public Wind wind; + public Integer humidity; + + public WeatherForecastDay(final GregorianCalendar date, final WeatherSpec.Daily dailyForecast) { + dayOfWeek = BLETypeConversions.dayOfWeekToRawBytes(date); + description = "Unknown"; // TODO from conditionCode + summary = "Unknown"; // TODO from conditionCode + high = new WeatherValue(dailyForecast.maxTemp - 273f, "CELSIUS"); + low = new WeatherValue(dailyForecast.minTemp - 273f, "CELSIUS"); + precipProb = dailyForecast.precipProbability; + icon = mapToCmfCondition(dailyForecast.conditionCode); + + if (dailyForecast.sunRise != 0 && dailyForecast.sunSet != 0) { + epochSunrise = dailyForecast.sunRise; + epochSunset = dailyForecast.sunSet; + } else { + final Location lastKnownLocation = new CurrentPosition().getLastKnownLocation(); + + final GregorianCalendar[] sunriseTransitSet = SPA.calculateSunriseTransitSet( + date, + lastKnownLocation.getLatitude(), + lastKnownLocation.getLongitude(), + DeltaT.estimate(date) + ); + + epochSunrise = (int) (sunriseTransitSet[0].getTime().getTime() / 1000); + epochSunset = (int) (sunriseTransitSet[2].getTime().getTime() / 1000); + } + + wind = new Wind(new WeatherValue(dailyForecast.windSpeed * 3.6, "METERS_PER_SECOND"), dailyForecast.windDirection); + humidity = dailyForecast.humidity; + } + } + + public static class WeatherForecastHour { + public int epochSeconds; + public String description; + public WeatherValue temp; + public Integer precipProb; + public Wind wind; + public Integer icon; + public WeatherValue dewPoint; + public Float uvIndex; + public Integer relativeHumidity; + public WeatherValue feelsLikeTemperature; + public WeatherValue visibility; + public WeatherValue pressure; + public Object airQuality; + public Integer cloudCover; + + public WeatherForecastHour(final WeatherSpec.Hourly hourlyForecast) { + epochSeconds = hourlyForecast.timestamp; + description = "Unknown"; // TODO from conditionCode + temp = new WeatherValue(hourlyForecast.temp - 273f, "CELSIUS"); + precipProb = hourlyForecast.precipProbability; + wind = new Wind(new WeatherValue(hourlyForecast.windSpeed * 3.6, "METERS_PER_SECOND"), hourlyForecast.windDirection); + icon = mapToCmfCondition(hourlyForecast.conditionCode); + //dewPoint = new WeatherValue(hourlyForecast.temp - 273f, "CELSIUS"); // TODO dewPoint + uvIndex = hourlyForecast.uvIndex; + relativeHumidity = hourlyForecast.humidity; + //feelsLikeTemperature = new WeatherValue(hourlyForecast.temp - 273f, "CELSIUS"); // TODO feelsLikeTemperature + //visibility = new WeatherValue(0, "METER"); // TODO visibility + //pressure = new WeatherValue(0f, "INCHES_OF_MERCURY"); // TODO pressure + //airQuality = null; // TODO airQuality + //cloudCover = 0; // TODO cloudCover + } + } + + public static class WeatherForecastCurrent { + public Integer epochSeconds; + public WeatherValue temperature; + public String description; + public Integer icon; + public WeatherValue feelsLikeTemperature; + public WeatherValue dewPoint; + public Integer relativeHumidity; + public Wind wind; + public String locationName; + public WeatherValue visibility; + public WeatherValue pressure; + public WeatherValue pressureChange; + + public WeatherForecastCurrent(final WeatherSpec weatherSpec) { + epochSeconds = weatherSpec.timestamp; + temperature = new WeatherValue(weatherSpec.currentTemp - 273f, "CELSIUS"); + description = weatherSpec.currentCondition; + icon = mapToCmfCondition(weatherSpec.currentConditionCode); + feelsLikeTemperature = new WeatherValue(weatherSpec.currentTemp - 273f, "CELSIUS"); + dewPoint = new WeatherValue(weatherSpec.dewPoint - 273f, "CELSIUS"); + relativeHumidity = weatherSpec.currentHumidity; + wind = new Wind(new WeatherValue(weatherSpec.windSpeed * 3.6, "METERS_PER_SECOND"), weatherSpec.windDirection); + locationName = weatherSpec.location; + visibility = new WeatherValue(weatherSpec.visibility, "METER"); + pressure = new WeatherValue(weatherSpec.pressure * 0.02953, "INCHES_OF_MERCURY"); + pressureChange = new WeatherValue(0f, "INCHES_OF_MERCURY"); + } + } + + public static class WeatherValue { + public Number value; + public String units; + + public WeatherValue(final Number value, final String units) { + this.value = value; + this.units = units; + } + } + + public static class Wind { + public WeatherValue speed; + public String directionString; // NW + public Integer direction; + + public Wind(final WeatherValue speed, final int direction) { + this.speed = speed; + this.direction = direction; + } + } + + public static int mapToCmfCondition(int openWeatherMapCondition) { + // Icons mapped from a Venu 3: + // 0 1 2 unk + // 3 4 5 6 sunny + // 7 8 9 10 sun cloudy + // 11 12 cloudy with dashes below + // 13 14 sun cloud 2 clouds + // 15 16 clouds + // 17 rain + // 18 19 20 21 rain with sun (or night at night?) + // 22 rain + // 23 24 unk + // 25 26 thunder with rain and sun behind + // 27 thunder with rain + // 28 29 rain + // 30 31 32 33 34 snow with clouds + // 35 36 37 snowflake + // 38 snow with clouds, with big flake + // 39 snow with rain + // 40 41 snow with rain + // 42 43 44 rain with snow + // 45 rain with snow + // 46 wind + // 47 48 foggy (dashes?) + // 49 50 51 unk + + switch (openWeatherMapCondition) { + //Group 2xx: Thunderstorm + case 210: //light thunderstorm:: //11d + case 200: //thunderstorm with light rain: //11d + case 201: //thunderstorm with rain: //11d + case 202: //thunderstorm with heavy rain: //11d + case 230: //thunderstorm with light drizzle: //11d + case 231: //thunderstorm with drizzle: //11d + case 232: //thunderstorm with heavy drizzle: //11d + case 211: //thunderstorm: //11d + case 212: //heavy thunderstorm: //11d + case 221: //ragged thunderstorm: //11d + return 27; + + //Group 90x: Extreme + case 901: //tropical storm + //Group 7xx: Atmosphere + case 781: //tornado: //[[file:50d.png]] + //Group 90x: Extreme + case 900: //tornado + // Group 7xx: Atmosphere + case 771: //squalls: //[[file:50d.png]] + //Group 9xx: Additional + case 960: //storm + case 961: //violent storm + case 902: //hurricane + case 962: //hurricane + return 46; + + //Group 3xx: Drizzle + case 300: //light intensity drizzle: //09d + case 301: //drizzle: //09d + case 302: //heavy intensity drizzle: //09d + case 310: //light intensity drizzle rain: //09d + case 311: //drizzle rain: //09d + case 312: //heavy intensity drizzle rain: //09d + case 313: //shower rain and drizzle: //09d + case 314: //heavy shower rain and drizzle: //09d + case 321: //shower drizzle: //09d + //Group 5xx: Rain + case 500: //light rain: //10d + case 501: //moderate rain: //10d + case 502: //heavy intensity rain: //10d + case 503: //very heavy rain: //10d + case 504: //extreme rain: //10d + case 520: //light intensity shower rain: //09d + case 521: //shower rain: //09d + case 522: //heavy intensity shower rain: //09d + case 531: //ragged shower rain: //09d + return 17; + + //Group 90x: Extreme + case 906: //hail + case 615: //light rain and snow: //[[file:13d.png]] + case 616: //rain and snow: //[[file:13d.png]] + case 511: //freezing rain: //13d + return 40; + + //Group 6xx: Snow + case 611: //sleet: //[[file:13d.png]] + case 612: //shower sleet: //[[file:13d.png]] + //Group 6xx: Snow + case 600: //light snow: //[[file:13d.png]] + case 601: //snow: //[[file:13d.png]] + //Group 6xx: Snow + case 602: //heavy snow: //[[file:13d.png]] + //Group 6xx: Snow + case 620: //light shower snow: //[[file:13d.png]] + case 621: //shower snow: //[[file:13d.png]] + case 622: //heavy shower snow: //[[file:13d.png]] + return 38; + + + //Group 7xx: Atmosphere + case 701: //mist: //[[file:50d.png]] + case 711: //smoke: //[[file:50d.png]] + case 721: //haze: //[[file:50d.png]] + case 731: //sandcase dust whirls: //[[file:50d.png]] + case 741: //fog: //[[file:50d.png]] + case 751: //sand: //[[file:50d.png]] + case 761: //dust: //[[file:50d.png]] + case 762: //volcanic ash: //[[file:50d.png]] + return 47; + + //Group 800: Clear + case 800: //clear sky: //[[file:01d.png]] [[file:01n.png]] + return 5; + + //Group 90x: Extreme + case 904: //hot + return 5; + + //Group 80x: Clouds + case 801: //few clouds: //[[file:02d.png]] [[file:02n.png]] + case 802: //scattered clouds: //[[file:03d.png]] [[file:03d.png]] + return 8; + case 803: //broken clouds: //[[file:04d.png]] [[file:03d.png]] + return 15; + + //Group 80x: Clouds + case 804: //overcast clouds: //[[file:04d.png]] [[file:04d.png]] + return 15; + + //Group 9xx: Additional + case 905: //windy + case 951: //calm + case 952: //light breeze + case 953: //gentle breeze + case 954: //moderate breeze + case 955: //fresh breeze + case 956: //strong breeze + case 957: //high windcase near gale + case 958: //gale + case 959: //severe gale + return 46; + + default: + //Group 90x: Extreme + case 903: //cold + return 35; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/ConfigurationMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/ConfigurationMessage.java index aac34cde0..415fb19ca 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/ConfigurationMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/ConfigurationMessage.java @@ -1,13 +1,20 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; import java.util.Set; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; +import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminPreferences; import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.GarminCapability; public class ConfigurationMessage extends GFDIMessage { public final Set OUR_CAPABILITIES = GarminCapability.ALL_CAPABILITIES; private final byte[] incomingConfigurationPayload; + private final Set capabilities; private final byte[] ourConfigurationPayload = GarminCapability.setToBinary(OUR_CAPABILITIES); public ConfigurationMessage(GarminMessage garminMessage, byte[] configurationPayload) { @@ -15,8 +22,7 @@ public class ConfigurationMessage extends GFDIMessage { if (configurationPayload.length > 255) throw new IllegalArgumentException("Too long payload"); this.incomingConfigurationPayload = configurationPayload; - - Set capabilities = GarminCapability.setFromBinary(configurationPayload); + this.capabilities = GarminCapability.setFromBinary(configurationPayload); LOG.info("Received configuration message; capabilities: {}", GarminCapability.setToString(capabilities)); this.statusMessage = this.getStatusMessage(); @@ -28,6 +34,17 @@ public class ConfigurationMessage extends GFDIMessage { return configurationMessage; } + @Override + public List getGBDeviceEvent() { + final Set capabilitiesPref = new HashSet<>(); + for (final GarminCapability capability : capabilities) { + capabilitiesPref.add(capability.name()); + } + return Collections.singletonList( + new GBDeviceEventUpdatePreferences(GarminPreferences.PREF_GARMIN_CAPABILITIES, capabilitiesPref) + ); + } + @Override protected boolean generateOutgoing() { final MessageWriter writer = new MessageWriter(response); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/DeviceInformationMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/DeviceInformationMessage.java index 986bd31cc..ab40605be 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/DeviceInformationMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/DeviceInformationMessage.java @@ -4,8 +4,11 @@ import android.annotation.SuppressLint; import android.bluetooth.BluetoothAdapter; import android.os.Build; +import java.util.Collections; +import java.util.List; import java.util.Locale; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; public class DeviceInformationMessage extends GFDIMessage { @@ -82,7 +85,8 @@ public class DeviceInformationMessage extends GFDIMessage { return true; } - public GBDeviceEventVersionInfo getGBDeviceEvent() { + @Override + public List getGBDeviceEvent() { LOG.info( "Received device information: protocol {}, product {}, unit {}, SW {}, max packet {}, BT name {}, device name {}, device model {}", incomingProtocolVersion, @@ -98,7 +102,7 @@ public class DeviceInformationMessage extends GFDIMessage { GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); versionCmd.fwVersion = getSoftwareVersionStr(); versionCmd.hwVersion = deviceModel; - return versionCmd; + return Collections.singletonList(versionCmd); } private String getSoftwareVersionStr() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FindMyPhoneCancelMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FindMyPhoneCancelMessage.java index 9ddc504b6..32ef56704 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FindMyPhoneCancelMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FindMyPhoneCancelMessage.java @@ -1,6 +1,9 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages; +import java.util.Collections; +import java.util.List; + import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; @@ -16,10 +19,10 @@ public class FindMyPhoneCancelMessage extends GFDIMessage { } @Override - public GBDeviceEvent getGBDeviceEvent() { + public List getGBDeviceEvent() { final GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone(); findPhoneEvent.event = GBDeviceEventFindPhone.Event.STOP; - return findPhoneEvent; + return Collections.singletonList(findPhoneEvent); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FindMyPhoneRequestMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FindMyPhoneRequestMessage.java index af7337066..1b9a05768 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FindMyPhoneRequestMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/FindMyPhoneRequestMessage.java @@ -1,5 +1,8 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages; +import java.util.Collections; +import java.util.List; + import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; @@ -20,10 +23,10 @@ public class FindMyPhoneRequestMessage extends GFDIMessage { } @Override - public GBDeviceEvent getGBDeviceEvent() { + public List getGBDeviceEvent() { final GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone(); findPhoneEvent.event = GBDeviceEventFindPhone.Event.START; - return findPhoneEvent; + return Collections.singletonList(findPhoneEvent); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/GFDIMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/GFDIMessage.java index 69baa214a..82e0c511f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/GFDIMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/GFDIMessage.java @@ -8,6 +8,8 @@ import org.slf4j.LoggerFactory; import java.lang.reflect.Method; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.util.Collections; +import java.util.List; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.ChecksumCalculator; @@ -81,8 +83,8 @@ public abstract class GFDIMessage { return new GenericStatusMessage(garminMessage, Status.ACK); } - public GBDeviceEvent getGBDeviceEvent() { - return null; + public List getGBDeviceEvent() { + return Collections.emptyList(); } public byte[] getAckBytestream() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/MusicControlMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/MusicControlMessage.java index 47fb256ee..508027d16 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/MusicControlMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/MusicControlMessage.java @@ -1,6 +1,10 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages; +import java.util.Collections; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; public class MusicControlMessage extends GFDIMessage { @@ -38,8 +42,8 @@ public class MusicControlMessage extends GFDIMessage { return new MusicControlMessage(garminMessage, command); } - public GBDeviceEventMusicControl getGBDeviceEvent() { - return event; + public List getGBDeviceEvent() { + return Collections.singletonList(event); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationControlMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationControlMessage.java index b02e837ee..1655200d2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationControlMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationControlMessage.java @@ -1,6 +1,8 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; @@ -111,8 +113,8 @@ public class NotificationControlMessage extends GFDIMessage { } @Override - public GBDeviceEvent getGBDeviceEvent() { - return deviceEvent; + public List getGBDeviceEvent() { + return Collections.singletonList(deviceEvent); } public NotificationsHandler.LegacyNotificationAction getLegacyNotificationAction() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationSubscriptionMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationSubscriptionMessage.java index d0b2f1a8b..ddc173e2a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationSubscriptionMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/NotificationSubscriptionMessage.java @@ -1,5 +1,8 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages; +import java.util.Collections; +import java.util.List; + import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.NotificationSubscriptionDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.status.NotificationSubscriptionStatusMessage; @@ -25,10 +28,10 @@ public class NotificationSubscriptionMessage extends GFDIMessage { } @Override - public GBDeviceEvent getGBDeviceEvent() { + public List getGBDeviceEvent() { NotificationSubscriptionDeviceEvent notificationSubscriptionDeviceEvent = new NotificationSubscriptionDeviceEvent(); notificationSubscriptionDeviceEvent.enable = this.enable; - return notificationSubscriptionDeviceEvent; + return Collections.singletonList(notificationSubscriptionDeviceEvent); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/WeatherMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/WeatherMessage.java index 93cc0c1b5..f7b9cf54b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/WeatherMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/WeatherMessage.java @@ -1,5 +1,9 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages; +import java.util.Collections; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.WeatherRequestDeviceEvent; public class WeatherMessage extends GFDIMessage { @@ -21,8 +25,8 @@ public class WeatherMessage extends GFDIMessage { return new WeatherMessage(format, latitude, longitude, hoursOfForecast, garminMessage); } - public WeatherRequestDeviceEvent getGBDeviceEvent() { - return weatherRequestDeviceEvent; + public List getGBDeviceEvent() { + return Collections.singletonList(weatherRequestDeviceEvent); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/SupportedFileTypesStatusMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/SupportedFileTypesStatusMessage.java index e5316568d..5f43f370d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/SupportedFileTypesStatusMessage.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/garmin/messages/status/SupportedFileTypesStatusMessage.java @@ -2,8 +2,10 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages.sta import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType; import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.deviceevents.SupportedFileTypesDeviceEvent; @@ -41,7 +43,7 @@ public class SupportedFileTypesStatusMessage extends GFDIStatusMessage { } @Override - public SupportedFileTypesDeviceEvent getGBDeviceEvent() { - return new SupportedFileTypesDeviceEvent(fileTypeInfoList); + public List getGBDeviceEvent() { + return Collections.singletonList(new SupportedFileTypesDeviceEvent(fileTypeInfoList)); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsHttpService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsHttpService.java index 39ee9ffaa..96581fb04 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsHttpService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/services/ZeppOsHttpService.java @@ -31,6 +31,7 @@ import java.util.Map; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsWeather; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService; +import nodomain.freeyourgadget.gadgetbridge.util.HttpUtils; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; public class ZeppOsHttpService extends AbstractZeppOsService { @@ -99,7 +100,7 @@ public class ZeppOsHttpService extends AbstractZeppOsService { } final String path = url.getPath(); - final Map query = urlQueryParameters(url); + final Map query = HttpUtils.urlQueryParameters(url); if (path.startsWith("/weather/")) { final ZeppOsWeather.Response response = ZeppOsWeather.handleHttpRequest(path, query); @@ -111,25 +112,6 @@ public class ZeppOsHttpService extends AbstractZeppOsService { replyHttpNoInternet(requestId); } - private Map urlQueryParameters(final URL url) { - final Map queryParameters = new HashMap<>(); - final String[] pairs = url.getQuery().split("&"); - for (final String pair : pairs) { - final String[] parts = pair.split("=", 2); - try { - final String key = URLDecoder.decode(parts[0], "UTF-8"); - if (parts.length == 2) { - queryParameters.put(key, URLDecoder.decode(parts[1], "UTF-8")); - } else { - queryParameters.put(key, ""); - } - } catch (final Exception e) { - LOG.error("Failed to decode query", e); - } - } - return queryParameters; - } - private void replyHttpNoInternet(final byte requestId) { LOG.info("Replying with no internet to http request {}", requestId); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/HttpUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/HttpUtils.java new file mode 100644 index 000000000..67dbf3be0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/HttpUtils.java @@ -0,0 +1,42 @@ +package nodomain.freeyourgadget.gadgetbridge.util; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URL; +import java.net.URLDecoder; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class HttpUtils { + private static final Logger LOG = LoggerFactory.getLogger(HttpUtils.class); + + private HttpUtils() { + // utility class + } + + public static Map urlQueryParameters(final URL url) { + final String query = url.getQuery(); + if (StringUtils.isBlank(query)) { + return Collections.emptyMap(); + } + final Map queryParameters = new HashMap<>(); + final String[] pairs = query.split("&"); + for (final String pair : pairs) { + final String[] parts = pair.split("=", 2); + try { + final String key = URLDecoder.decode(parts[0], "UTF-8"); + if (parts.length == 2) { + queryParameters.put(key, URLDecoder.decode(parts[1], "UTF-8")); + } else { + queryParameters.put(key, ""); + } + } catch (final Exception e) { + LOG.error("Failed to decode query", e); + } + } + return queryParameters; + } +} diff --git a/app/src/main/proto/garmin_vivomovehr/gdi_http_service.proto b/app/src/main/proto/garmin_vivomovehr/gdi_http_service.proto new file mode 100644 index 000000000..932e2ed03 --- /dev/null +++ b/app/src/main/proto/garmin_vivomovehr/gdi_http_service.proto @@ -0,0 +1,46 @@ +syntax = "proto2"; + +package garmin_vivomovehr; + +option java_package = "nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr"; + +message HttpService { + enum Method { + UNKNOWN_METHOD = 0; + GET = 1; + PUT = 2; + POST = 3; + DELETE = 4; + PATCH = 5; + HEAD = 6; + } + + enum Status { + UNKNOWN_STATUS = 0; + OK = 100; + NETWORK_REQUEST_TIMED_OUT = 200; + FILE_TOO_LARGE = 300; + DATA_TRANSFER_ITEM_FAILURE = 400; + } + + optional RawRequest rawRequest = 5; + optional RawResponse rawResponse = 6; + + message RawRequest { + required string url = 1; + optional Method method = 3; + repeated Header header = 5; + } + + message RawResponse { + optional Status status = 1; + optional uint32 httpStatus = 2; + optional bytes body = 3; + repeated Header header = 5; + } + + message Header { + required string key = 1; + required string value = 2; + } +} diff --git a/app/src/main/proto/garmin_vivomovehr/gdi_smart_proto.proto b/app/src/main/proto/garmin_vivomovehr/gdi_smart_proto.proto index 11484a2a5..cea626dd5 100644 --- a/app/src/main/proto/garmin_vivomovehr/gdi_smart_proto.proto +++ b/app/src/main/proto/garmin_vivomovehr/gdi_smart_proto.proto @@ -7,12 +7,14 @@ option java_package = "nodomain.freeyourgadget.gadgetbridge.proto.vivomovehr"; import "garmin_vivomovehr/gdi_device_status.proto"; import "garmin_vivomovehr/gdi_find_my_watch.proto"; import "garmin_vivomovehr/gdi_core.proto"; +import "garmin_vivomovehr/gdi_http_service.proto"; import "garmin_vivomovehr/gdi_sms_notification.proto"; import "garmin_vivomovehr/gdi_calendar_service.proto"; import "garmin_vivomovehr/gdi_settings_service.proto"; message Smart { optional CalendarService calendar_service = 1; + optional HttpService http_service = 2; optional DeviceStatusService device_status_service = 8; optional FindMyWatchService find_my_watch_service = 12; optional CoreService core_service = 13;