mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-12-25 18:15:49 +01:00
Garmin: Add support for http weather requests
This commit is contained in:
parent
01d48cde91
commit
d8dcc57813
@ -0,0 +1,5 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
|
||||
|
||||
public class GarminPreferences {
|
||||
public static final String PREF_GARMIN_CAPABILITIES = "garmin_capabilities";
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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<GBDeviceEvent> 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<RecordData> weatherData = new ArrayList<>();
|
||||
|
||||
List<RecordDefinition> weatherDefinitions = new ArrayList<>(3);
|
||||
|
@ -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());
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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<String, String> query = HttpUtils.urlQueryParameters(url);
|
||||
final Map<String, String> requestHeaders = headersToMap(rawRequest.getHeaderList());
|
||||
|
||||
final byte[] responseBody;
|
||||
final List<GdiHttpService.HttpService.Header> 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<String, String> headersToMap(final List<GdiHttpService.HttpService.Header> headers) {
|
||||
final Map<String, String> ret = new HashMap<>();
|
||||
for (final GdiHttpService.HttpService.Header header : headers) {
|
||||
ret.put(header.getKey().toLowerCase(Locale.ROOT), header.getValue());
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
@ -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<String, String> 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<WeatherForecastDay> 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<WeatherForecastHour> 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<String, String> 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<String, String> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<GarminCapability> OUR_CAPABILITIES = GarminCapability.ALL_CAPABILITIES;
|
||||
private final byte[] incomingConfigurationPayload;
|
||||
private final Set<GarminCapability> 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<GarminCapability> 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<GBDeviceEvent> getGBDeviceEvent() {
|
||||
final Set<Object> 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);
|
||||
|
@ -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<GBDeviceEvent> 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() {
|
||||
|
@ -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<GBDeviceEvent> getGBDeviceEvent() {
|
||||
final GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
|
||||
findPhoneEvent.event = GBDeviceEventFindPhone.Event.STOP;
|
||||
return findPhoneEvent;
|
||||
return Collections.singletonList(findPhoneEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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<GBDeviceEvent> getGBDeviceEvent() {
|
||||
final GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone();
|
||||
findPhoneEvent.event = GBDeviceEventFindPhone.Event.START;
|
||||
return findPhoneEvent;
|
||||
return Collections.singletonList(findPhoneEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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<GBDeviceEvent> getGBDeviceEvent() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public byte[] getAckBytestream() {
|
||||
|
@ -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<GBDeviceEvent> getGBDeviceEvent() {
|
||||
return Collections.singletonList(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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<GBDeviceEvent> getGBDeviceEvent() {
|
||||
return Collections.singletonList(deviceEvent);
|
||||
}
|
||||
|
||||
public NotificationsHandler.LegacyNotificationAction getLegacyNotificationAction() {
|
||||
|
@ -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<GBDeviceEvent> getGBDeviceEvent() {
|
||||
NotificationSubscriptionDeviceEvent notificationSubscriptionDeviceEvent = new NotificationSubscriptionDeviceEvent();
|
||||
notificationSubscriptionDeviceEvent.enable = this.enable;
|
||||
return notificationSubscriptionDeviceEvent;
|
||||
return Collections.singletonList(notificationSubscriptionDeviceEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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<GBDeviceEvent> getGBDeviceEvent() {
|
||||
return Collections.singletonList(weatherRequestDeviceEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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<GBDeviceEvent> getGBDeviceEvent() {
|
||||
return Collections.singletonList(new SupportedFileTypesDeviceEvent(fileTypeInfoList));
|
||||
}
|
||||
}
|
||||
|
@ -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<String, String> query = urlQueryParameters(url);
|
||||
final Map<String, String> 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<String, String> urlQueryParameters(final URL url) {
|
||||
final Map<String, String> 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);
|
||||
|
||||
|
@ -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<String, String> urlQueryParameters(final URL url) {
|
||||
final String query = url.getQuery();
|
||||
if (StringUtils.isBlank(query)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
final Map<String, String> 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;
|
||||
}
|
||||
}
|
46
app/src/main/proto/garmin_vivomovehr/gdi_http_service.proto
Normal file
46
app/src/main/proto/garmin_vivomovehr/gdi_http_service.proto
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user