Garmin: Add support for http weather requests

This commit is contained in:
José Rebelo 2024-04-22 21:02:06 +01:00 committed by Daniele Gobbetti
parent c5a94d2927
commit 3386e86158
21 changed files with 687 additions and 48 deletions

View File

@ -0,0 +1,5 @@
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
public class GarminPreferences {
public static final String PREF_GARMIN_CAPABILITIES = "garmin_capabilities";
}

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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());

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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);

View File

@ -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() {

View File

@ -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

View File

@ -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

View File

@ -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() {

View File

@ -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

View File

@ -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() {

View File

@ -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

View File

@ -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

View File

@ -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));
}
}

View File

@ -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);

View File

@ -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;
}
}

View 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;
}
}

View File

@ -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;